Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"@release-it/conventional-changelog": "^10.0.3",
"@swc/core": "1.15.3",
"@types/dlv": "^1.1.5",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.6",
"@types/luxon": "^3.7.1",
"@types/node": "^25.0.0",
"benchmark": "^2.1.4",
Expand Down
23 changes: 23 additions & 0 deletions src/schema/any/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { BaseLiteralType } from '../base/literal.js'
import type { FieldOptions, Validation } from '../../types.js'
import { SUBTYPE } from '../../symbols.js'
import { type JSONSchema7 } from 'json-schema'

/**
* VineAny represents a value that can be anything
Expand All @@ -31,4 +32,26 @@ export class VineAny extends BaseLiteralType<any, any, any> {
clone(): this {
return new VineAny(this.cloneOptions(), this.cloneValidations()) as this
}

/**
* Transforms into JSONSchema.
*/
toJSONSchema(): JSONSchema7 {
const schema: JSONSchema7 = {
anyOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean' },
{ type: 'array' },
{ type: 'object' },
],
}

for (const validation of this.validations) {
if (!validation.rule.toJSONSchema) continue
validation.rule.toJSONSchema(schema, validation.options)
}

return schema
}
}
35 changes: 29 additions & 6 deletions src/schema/array/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import {
UNIQUE_NAME,
IS_OF_TYPE,
} from '../../symbols.js'
import type { FieldOptions, ParserOptions, SchemaTypes, Validation } from '../../types.js'
import type {
FieldOptions,
ParserOptions,
SchemaTypes,
Validation,
WithJSONSchema,
} from '../../types.js'

import {
compactRule,
Expand All @@ -29,6 +35,7 @@ import {
maxLengthRule,
fixedLengthRule,
} from './rules.js'
import { type JSONSchema7 } from 'json-schema'

/**
* VineArray represents an array schema type in the validation pipeline.
Expand All @@ -48,11 +55,10 @@ import {
* data: ['user1@example.com', 'user2@example.com']
* })
*/
export class VineArray<Schema extends SchemaTypes> extends BaseType<
Schema[typeof ITYPE][],
Schema[typeof OTYPE][],
Schema[typeof COTYPE][]
> {
export class VineArray<Schema extends SchemaTypes>
extends BaseType<Schema[typeof ITYPE][], Schema[typeof OTYPE][], Schema[typeof COTYPE][]>
implements WithJSONSchema
{
/**
* Static collection of all available validation rules for arrays
*/
Expand Down Expand Up @@ -166,6 +172,23 @@ export class VineArray<Schema extends SchemaTypes> extends BaseType<
return new VineArray(this.#schema.clone(), this.cloneOptions(), this.cloneValidations()) as this
}

/**
* Transforms into JSONSchema.
*/
toJSONSchema(): JSONSchema7 {
const schema: JSONSchema7 = {
type: 'array',
items: this.#schema.toJSONSchema?.() ?? {},
}

for (const validation of this.validations) {
if (!validation.rule.toJSONSchema) continue
validation.rule.toJSONSchema(schema, validation.options)
}

return schema
}

/**
* Compiles to array data type for the validation compiler.
*
Expand Down
74 changes: 53 additions & 21 deletions src/schema/array/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,40 @@ import { createRule } from '../../vine/create_rule.js'
/**
* Enforce a minimum length on an array field
*/
export const minLengthRule = createRule<{ min: number }>(function minLength(value, options, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length < options.min) {
field.report(messages['array.minLength'], 'array.minLength', field, options)
export const minLengthRule = createRule<{ min: number }>(
function minLength(value, options, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length < options.min) {
field.report(messages['array.minLength'], 'array.minLength', field, options)
}
},
{
toJSONSchema: (schema, options) => {
schema.minItems = options.min
},
}
})
)

/**
* Enforce a maximum length on an array field
*/
export const maxLengthRule = createRule<{ max: number }>(function maxLength(value, options, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length > options.max) {
field.report(messages['array.maxLength'], 'array.maxLength', field, options)
export const maxLengthRule = createRule<{ max: number }>(
function maxLength(value, options, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length > options.max) {
field.report(messages['array.maxLength'], 'array.maxLength', field, options)
}
},
{
toJSONSchema: (schema, options) => {
schema.maxItems = options.max
},
}
})
)

/**
* Enforce a fixed length on an array field
Expand All @@ -46,20 +60,33 @@ export const fixedLengthRule = createRule<{ size: number }>(
if ((value as unknown[]).length !== options.size) {
field.report(messages['array.fixedLength'], 'array.fixedLength', field, options)
}
},
{
toJSONSchema: (schema, options) => {
schema.minItems = options.size
schema.maxItems = options.size
},
}
)

/**
* Ensure the array is not empty
*/
export const notEmptyRule = createRule<undefined>(function notEmpty(value, _, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length <= 0) {
field.report(messages.notEmpty, 'notEmpty', field)
export const notEmptyRule = createRule<undefined>(
function notEmpty(value, _, field) {
/**
* Value will always be an array if the field is valid.
*/
if ((value as unknown[]).length <= 0) {
field.report(messages.notEmpty, 'notEmpty', field)
}
},
{
toJSONSchema: (schema) => {
schema.minItems = 1
},
}
})
)

/**
* Ensure array elements are distinct/unique
Expand All @@ -72,6 +99,11 @@ export const distinctRule = createRule<{ fields?: string | string[] }>(
if (!helpers.isDistinct(value as any[], options.fields)) {
field.report(messages.distinct, 'distinct', field, options)
}
},
{
toJSONSchema: (schema) => {
schema.uniqueItems = true
},
}
)

Expand Down
Loading