Skip to content

Commit 57f93e8

Browse files
committed
feat: add json-schema support
2 parents 840e16b + dc5639c commit 57f93e8

File tree

18 files changed

+1058
-144
lines changed

18 files changed

+1058
-144
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@release-it/conventional-changelog": "^10.0.1",
4848
"@swc/core": "1.10.7",
4949
"@types/dlv": "^1.1.5",
50+
"@types/json-schema": "^7.0.15",
5051
"@types/node": "^22.15.2",
5152
"benchmark": "^2.1.4",
5253
"c8": "^10.1.3",

src/schema/any/main.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
*/
99

1010
import { BaseLiteralType } from '../base/literal.js'
11-
import type { FieldOptions, Validation } from '../../types.js'
12-
import { SUBTYPE } from '../../symbols.js'
11+
import type { FieldOptions, ParserOptions, Validation } from '../../types.js'
12+
import { PARSE, SUBTYPE } from '../../symbols.js'
13+
import { RefsStore, LiteralNode } from '@vinejs/compiler/types'
14+
import { JSONSchema7 } from 'json-schema'
1315

1416
/**
1517
* VineAny represents a value that can be anything
@@ -31,4 +33,36 @@ export class VineAny extends BaseLiteralType<any, any, any> {
3133
clone(): this {
3234
return new VineAny(this.cloneOptions(), this.cloneValidations()) as this
3335
}
36+
37+
protected compileJsonSchema(): JSONSchema7 {
38+
const schema: JSONSchema7 = {
39+
anyOf: [
40+
{ type: 'string' },
41+
{ type: 'number' },
42+
{ type: 'boolean' },
43+
{ type: 'array' },
44+
{ type: 'object' },
45+
],
46+
}
47+
48+
for (const validation of this.validations) {
49+
if (!validation.rule.jsonSchema) continue
50+
validation.rule.jsonSchema(schema, validation.options)
51+
}
52+
53+
return schema
54+
}
55+
56+
/**
57+
* Compiles the schema type to a compiler node
58+
*/
59+
[PARSE](
60+
propertyName: string,
61+
refs: RefsStore,
62+
options: ParserOptions
63+
): LiteralNode & { subtype: string; json: JSONSchema7 } {
64+
const schema = super[PARSE](propertyName, refs, options)
65+
schema.json = this.compileJsonSchema()
66+
return schema
67+
}
3468
}

src/schema/array/main.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import { RefsStore, ArrayNode } from '@vinejs/compiler/types'
1212

1313
import { BaseType } from '../base/main.js'
1414
import { ITYPE, OTYPE, COTYPE, PARSE, UNIQUE_NAME, IS_OF_TYPE } from '../../symbols.js'
15-
import type { FieldOptions, ParserOptions, SchemaTypes, Validation } from '../../types.js'
15+
import type {
16+
CompilerNodes,
17+
FieldOptions,
18+
ParserOptions,
19+
SchemaTypes,
20+
Validation,
21+
} from '../../types.js'
1622

1723
import {
1824
compactRule,
@@ -22,6 +28,7 @@ import {
2228
maxLengthRule,
2329
fixedLengthRule,
2430
} from './rules.js'
31+
import { JSONSchema7 } from 'json-schema'
2532

2633
/**
2734
* VineArray represents an array schema type in the validation
@@ -114,20 +121,46 @@ export class VineArray<Schema extends SchemaTypes> extends BaseType<
114121
return new VineArray(this.#schema.clone(), this.cloneOptions(), this.cloneValidations()) as this
115122
}
116123

124+
/**
125+
* Compiles JSON Schema.
126+
*/
127+
protected compileJsonSchema(node: CompilerNodes) {
128+
const schema: JSONSchema7 = {
129+
type: 'array',
130+
}
131+
132+
if ('json' in node) {
133+
schema.items = node.json
134+
}
135+
136+
for (const validation of this.validations) {
137+
if (!validation.rule.jsonSchema) continue
138+
validation.rule.jsonSchema(schema, validation.options)
139+
}
140+
141+
return schema
142+
}
143+
117144
/**
118145
* Compiles to array data type
119146
*/
120-
[PARSE](propertyName: string, refs: RefsStore, options: ParserOptions): ArrayNode {
147+
[PARSE](
148+
propertyName: string,
149+
refs: RefsStore,
150+
options: ParserOptions
151+
): ArrayNode & { json: JSONSchema7 } {
152+
const parsed = this.#schema[PARSE]('*', refs, options)
121153
return {
122154
type: 'array',
123155
fieldName: propertyName,
124156
propertyName: options.toCamelCase ? camelcase(propertyName) : propertyName,
125157
bail: this.options.bail,
126158
allowNull: this.options.allowNull,
127159
isOptional: this.options.isOptional,
128-
each: this.#schema[PARSE]('*', refs, options),
160+
each: parsed,
129161
parseFnId: this.options.parse ? refs.trackParser(this.options.parse) : undefined,
130162
validations: this.compileValidations(refs),
163+
json: this.compileJsonSchema(parsed),
131164
}
132165
}
133166
}

src/schema/array/rules.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,40 @@ import { createRule } from '../../vine/create_rule.js'
1414
/**
1515
* Enforce a minimum length on an array field
1616
*/
17-
export const minLengthRule = createRule<{ min: number }>(function minLength(value, options, field) {
18-
/**
19-
* Value will always be an array if the field is valid.
20-
*/
21-
if ((value as unknown[]).length < options.min) {
22-
field.report(messages['array.minLength'], 'array.minLength', field, options)
17+
export const minLengthRule = createRule<{ min: number }>(
18+
function minLength(value, options, field) {
19+
/**
20+
* Value will always be an array if the field is valid.
21+
*/
22+
if ((value as unknown[]).length < options.min) {
23+
field.report(messages['array.minLength'], 'array.minLength', field, options)
24+
}
25+
},
26+
{
27+
json: (schema, options) => {
28+
schema.minItems = options.min
29+
},
2330
}
24-
})
31+
)
2532

2633
/**
2734
* Enforce a maximum length on an array field
2835
*/
29-
export const maxLengthRule = createRule<{ max: number }>(function maxLength(value, options, field) {
30-
/**
31-
* Value will always be an array if the field is valid.
32-
*/
33-
if ((value as unknown[]).length > options.max) {
34-
field.report(messages['array.maxLength'], 'array.maxLength', field, options)
36+
export const maxLengthRule = createRule<{ max: number }>(
37+
function maxLength(value, options, field) {
38+
/**
39+
* Value will always be an array if the field is valid.
40+
*/
41+
if ((value as unknown[]).length > options.max) {
42+
field.report(messages['array.maxLength'], 'array.maxLength', field, options)
43+
}
44+
},
45+
{
46+
json: (schema, options) => {
47+
schema.maxItems = options.max
48+
},
3549
}
36-
})
50+
)
3751

3852
/**
3953
* Enforce a fixed length on an array field
@@ -46,20 +60,33 @@ export const fixedLengthRule = createRule<{ size: number }>(
4660
if ((value as unknown[]).length !== options.size) {
4761
field.report(messages['array.fixedLength'], 'array.fixedLength', field, options)
4862
}
63+
},
64+
{
65+
json: (schema, options) => {
66+
schema.minItems = options.size
67+
schema.maxItems = options.size
68+
},
4969
}
5070
)
5171

5272
/**
5373
* Ensure the array is not empty
5474
*/
55-
export const notEmptyRule = createRule<undefined>(function notEmpty(value, _, field) {
56-
/**
57-
* Value will always be an array if the field is valid.
58-
*/
59-
if ((value as unknown[]).length <= 0) {
60-
field.report(messages.notEmpty, 'notEmpty', field)
75+
export const notEmptyRule = createRule<undefined>(
76+
function notEmpty(value, _, field) {
77+
/**
78+
* Value will always be an array if the field is valid.
79+
*/
80+
if ((value as unknown[]).length <= 0) {
81+
field.report(messages.notEmpty, 'notEmpty', field)
82+
}
83+
},
84+
{
85+
json: (schema) => {
86+
schema.minItems = 1
87+
},
6188
}
62-
})
89+
)
6390

6491
/**
6592
* Ensure array elements are distinct/unique
@@ -72,6 +99,11 @@ export const distinctRule = createRule<{ fields?: string | string[] }>(
7299
if (!helpers.isDistinct(value as any[], options.fields)) {
73100
field.report(messages.distinct, 'distinct', field, options)
74101
}
102+
},
103+
{
104+
json: (schema) => {
105+
schema.uniqueItems = true
106+
},
75107
}
76108
)
77109

0 commit comments

Comments
 (0)