Skip to content

Commit b827e2f

Browse files
authored
feat(next): DEVXP-2564: implement enum validation (remoteoss#138)
1 parent c535db0 commit b827e2f

File tree

5 files changed

+109
-54
lines changed

5 files changed

+109
-54
lines changed

next/src/validation/enum.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ValidationError } from '../form'
2+
import type { NonBooleanJsfSchema, SchemaValue } from '../types'
3+
import { deepEqual } from './util'
4+
5+
/**
6+
* Validate that the value is one of the allowed enum values
7+
* @param value - The value to validate
8+
* @param schema - The schema to validate against
9+
* @param path - The path to the value
10+
* @returns An array of validation errors
11+
*
12+
* @example
13+
* ```ts
14+
* validateEnum('foo', { enum: ['foo', 'bar'] }) // []
15+
* validateEnum('baz', { enum: ['foo', 'bar'] }) // [{ path: [], validation: 'enum', message: 'must be one of ["foo", "bar"]' }]
16+
* ```
17+
* @see https://json-schema.org/understanding-json-schema/reference/enum
18+
* @see https://json-schema.org/draft/2020-12/json-schema-validation#name-enum
19+
*/
20+
export function validateEnum(value: SchemaValue, schema: NonBooleanJsfSchema, path: string[] = []): ValidationError[] {
21+
if (schema.enum === undefined) {
22+
return []
23+
}
24+
25+
if (!schema.enum.some(enumValue => deepEqual(enumValue, value))) {
26+
return [{ path, validation: 'enum', message: `must be one of ${JSON.stringify(schema.enum)}` }]
27+
}
28+
29+
return []
30+
}

next/src/validation/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { StringValidationErrorType } from './string'
44
import { validateAllOf, validateAnyOf, validateNot, validateOneOf } from './composition'
55
import { validateCondition } from './conditions'
66
import { validateConst } from './const'
7+
import { validateEnum } from './enum'
78
import { type NumberValidationErrorType, validateNumber } from './number'
89
import { validateObject } from './object'
910
import { validateString } from './string'
@@ -16,6 +17,7 @@ export type SchemaValidationErrorType =
1617
| 'required'
1718
| 'valid'
1819
| 'const'
20+
| 'enum'
1921

2022
/**
2123
* Schema composition keywords (allOf, anyOf, oneOf, not)
@@ -152,6 +154,9 @@ function validateType(value: SchemaValue, schema: JsfSchema, path: string[] = []
152154
* 3. Validate against base schema constraints:
153155
* - Type validation (if type is specified)
154156
* - Required properties (for objects)
157+
* - Boolean validation
158+
* - Enum validation
159+
* - Const validation
155160
* - Type-specific validations (string, number, object)
156161
* 4. Validate against composition keywords in this order:
157162
* - not (negates the validation of a subschema)
@@ -202,6 +207,7 @@ export function validateSchema(
202207

203208
return [
204209
...validateConst(value, schema, path),
210+
...validateEnum(value, schema, path),
205211
...validateObject(value, schema, path),
206212
...validateString(value, schema, path),
207213
...validateNumber(value, schema, path),

next/src/validation/util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export function deepEqual(a: SchemaValue, b: SchemaValue): boolean {
2828
return true
2929
}
3030

31+
// If both are null, the check above has returned true.
32+
// If one is null, we return false because we know they are not equal
33+
// and since `typeof null === 'object'`, we must not let the null value
34+
// pass through as an object.
3135
if (a === null || b === null) {
3236
return false
3337
}

next/test/json-schema-test-suite/failed-json-schema-test-suite.json

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@
3535
"same $anchor with different base uri": [
3636
"$ref does not resolve to /$defs/A/allOf/0"
3737
],
38-
"nul characters in strings": [
39-
"do not match string lacking nul",
40-
"do not match string lacking nul"
41-
],
4238
"contains keyword validation": [
4339
"array without items matching schema is invalid",
4440
"empty array is invalid"
@@ -143,52 +139,6 @@
143139
"$dynamicRef skips over intermediate resources - direct reference": [
144140
"string property fails"
145141
],
146-
"simple enum validation": [
147-
"something else is invalid"
148-
],
149-
"heterogeneous enum validation": [
150-
"something else is invalid",
151-
"objects are deep compared",
152-
"extra properties in object is invalid"
153-
],
154-
"heterogeneous enum-with-null validation": [
155-
"something else is invalid"
156-
],
157-
"enums in properties": [
158-
"wrong foo value",
159-
"wrong bar value"
160-
],
161-
"enum with escaped characters": [
162-
"another string is invalid"
163-
],
164-
"enum with false does not match 0": [
165-
"integer zero is invalid",
166-
"float zero is invalid"
167-
],
168-
"enum with [false] does not match [0]": [
169-
"[0] is invalid",
170-
"[0.0] is invalid"
171-
],
172-
"enum with true does not match 1": [
173-
"integer one is invalid",
174-
"float one is invalid"
175-
],
176-
"enum with [true] does not match [1]": [
177-
"[1] is invalid",
178-
"[1.0] is invalid"
179-
],
180-
"enum with 0 does not match false": [
181-
"false is invalid"
182-
],
183-
"enum with [0] does not match [false]": [
184-
"[false] is invalid"
185-
],
186-
"enum with 1 does not match true": [
187-
"true is invalid"
188-
],
189-
"enum with [1] does not match [true]": [
190-
"[true] is invalid"
191-
],
192142
"email format": [
193143
"invalid email string is only an annotation by default"
194144
],
@@ -416,10 +366,6 @@
416366
"ref creates new scope when adjacent to keywords": [
417367
"referenced subschema doesn't see annotations from properties"
418368
],
419-
"naive replacement of $ref with its destination is not correct": [
420-
"do not evaluate the $ref inside the enum, matching any string",
421-
"do not evaluate the $ref inside the enum, definition exact match"
422-
],
423369
"refs with relative uris and defs": [
424370
"invalid on inner field",
425371
"invalid on outer field"

next/test/validation/enum.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it } from '@jest/globals'
2+
import { validateSchema } from '../../src/validation/schema'
3+
4+
describe('enum validation', () => {
5+
it('returns no errors for values that are in the enum', () => {
6+
const schema = { enum: [1, 2, 3] }
7+
expect(validateSchema(1, schema)).toEqual([])
8+
expect(validateSchema(2, schema)).toEqual([])
9+
expect(validateSchema(3, schema)).toEqual([])
10+
})
11+
12+
it('returns an error for values that are not in the enum', () => {
13+
const schema = { enum: [1, 2, 3] }
14+
expect(validateSchema(4, schema)).toEqual([{
15+
path: [],
16+
validation: 'enum',
17+
message: 'must be one of [1,2,3]',
18+
}])
19+
})
20+
21+
it('handles null in enums', () => {
22+
const schema = { enum: [1, null, 'test'] }
23+
expect(validateSchema(null, schema)).toEqual([])
24+
expect(validateSchema(undefined, schema)).toEqual([])
25+
})
26+
27+
it('handles objects in enums', () => {
28+
const schema = {
29+
enum: [
30+
{ foo: 'bar' },
31+
{ baz: 123 },
32+
1,
33+
],
34+
}
35+
expect(validateSchema({ foo: 'bar' }, schema)).toEqual([])
36+
expect(validateSchema({ foo: 'baz' }, schema)).toEqual([{
37+
path: [],
38+
validation: 'enum',
39+
message: 'must be one of [{"foo":"bar"},{"baz":123},1]',
40+
}])
41+
expect(validateSchema(1, schema)).toEqual([])
42+
})
43+
44+
it('handles mixed type enums', () => {
45+
const schema = {
46+
enum: [
47+
1,
48+
'test',
49+
null,
50+
{ foo: 'bar' },
51+
],
52+
}
53+
expect(validateSchema(1, schema)).toEqual([])
54+
expect(validateSchema('test', schema)).toEqual([])
55+
expect(validateSchema(null, schema)).toEqual([])
56+
expect(validateSchema({ foo: 'bar' }, schema)).toEqual([])
57+
58+
expect(validateSchema('other', schema)).toEqual([{
59+
path: [],
60+
validation: 'enum',
61+
message: 'must be one of [1,"test",null,{"foo":"bar"}]',
62+
}])
63+
expect(validateSchema({ foo: 'baz' }, schema)).toEqual([{
64+
path: [],
65+
validation: 'enum',
66+
message: 'must be one of [1,"test",null,{"foo":"bar"}]',
67+
}])
68+
})
69+
})

0 commit comments

Comments
 (0)