@fougere/schema 0.12.1-alpha.0 → 0.12.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FougereFieldAxes.d.ts.map +1 -0
- package/dist/FougereFieldAxes.js.map +1 -0
- package/dist/Schema.d.ts +4 -3
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +3 -0
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaConstraints.d.ts +13 -10
- package/dist/SchemaConstraints.d.ts.map +1 -1
- package/dist/SchemaConstraints.js +32 -20
- package/dist/SchemaConstraints.js.map +1 -1
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +6 -5
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaError.d.ts +4 -1
- package/dist/SchemaError.d.ts.map +1 -1
- package/dist/SchemaError.js +5 -2
- package/dist/SchemaError.js.map +1 -1
- package/dist/SchemaView.d.ts +3 -2
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/Axes.d.ts +1 -1
- package/dist/axis/Axes.js +3 -3
- package/dist/axis/Axis.d.ts +12 -2
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +9 -2
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +6 -2
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/Decoder.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +13 -4
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +8 -4
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/apply.js +1 -1
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Role.d.ts +17 -10
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +30 -10
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +32 -9
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +43 -12
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/axis/shape/ShapeFormat.d.ts +10 -0
- package/dist/axis/shape/ShapeFormat.d.ts.map +1 -0
- package/dist/axis/shape/ShapeFormat.js +32 -0
- package/dist/axis/shape/ShapeFormat.js.map +1 -0
- package/dist/entity/EntityDeclarations.d.ts +3 -2
- package/dist/entity/EntityDeclarations.d.ts.map +1 -1
- package/dist/entity/FieldGroups.d.ts +9 -0
- package/dist/entity/FieldGroups.d.ts.map +1 -0
- package/dist/entity/FieldGroups.js +2 -0
- package/dist/entity/FieldGroups.js.map +1 -0
- package/dist/field/Field.d.ts +35 -16
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +53 -8
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldFormat.d.ts +1 -1
- package/dist/field/FieldFormat.js +4 -4
- package/dist/field/FieldFormat.js.map +1 -1
- package/dist/field/FieldSet.d.ts +12 -3
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +22 -15
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/Format.d.ts +21 -2
- package/dist/lib/Format.d.ts.map +1 -1
- package/dist/lib/Format.js +31 -3
- package/dist/lib/Format.js.map +1 -1
- package/dist/lib/UnionToIntersection.d.ts +7 -0
- package/dist/lib/UnionToIntersection.d.ts.map +1 -0
- package/dist/lib/UnionToIntersection.js +2 -0
- package/dist/lib/UnionToIntersection.js.map +1 -0
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/projection/Cases.d.ts +1 -1
- package/dist/projection/Cases.d.ts.map +1 -1
- package/dist/projection/Cases.js +20 -8
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/Visibility.d.ts.map +1 -1
- package/dist/projection/Visibility.js +6 -6
- package/dist/projection/Visibility.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +21 -30
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/CardForms.js +1 -1
- package/dist/projection/card/Diff.js.map +1 -1
- package/dist/projection/card/FieldExtension.d.ts +2 -2
- package/dist/projection/card/FieldExtension.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.d.ts +7 -2
- package/dist/projection/card/RoleDescriptor.d.ts.map +1 -1
- package/dist/projection/card/RoleDescriptor.js +2 -2
- package/dist/projection/card/RoleDescriptor.js.map +1 -1
- package/dist/projection/card/ShapeKeywords.d.ts +1 -1
- package/dist/projection/card/ShapeKeywords.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.js +7 -10
- package/dist/validator/FieldDeclarationValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +0 -2
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +7 -1
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +15 -14
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/preset/created.d.ts +2 -2
- package/dist/vocabulary/preset/created.d.ts.map +1 -1
- package/dist/vocabulary/preset/created.js +2 -2
- package/dist/vocabulary/preset/created.js.map +1 -1
- package/dist/vocabulary/preset/updated.d.ts +2 -2
- package/dist/vocabulary/preset/updated.d.ts.map +1 -1
- package/dist/vocabulary/preset/updated.js +2 -2
- package/dist/vocabulary/preset/updated.js.map +1 -1
- package/dist/vocabulary/primitive/bool.d.ts +2 -5
- package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
- package/dist/vocabulary/primitive/bool.js +1 -5
- package/dist/vocabulary/primitive/bool.js.map +1 -1
- package/dist/vocabulary/primitive/date.d.ts +2 -3
- package/dist/vocabulary/primitive/date.d.ts.map +1 -1
- package/dist/vocabulary/primitive/date.js +2 -3
- package/dist/vocabulary/primitive/date.js.map +1 -1
- package/dist/vocabulary/primitive/json.d.ts +3 -3
- package/dist/vocabulary/primitive/json.d.ts.map +1 -1
- package/dist/vocabulary/primitive/json.js +11 -11
- package/dist/vocabulary/primitive/json.js.map +1 -1
- package/dist/vocabulary/primitive/list.d.ts +6 -5
- package/dist/vocabulary/primitive/list.d.ts.map +1 -1
- package/dist/vocabulary/primitive/list.js +13 -7
- package/dist/vocabulary/primitive/list.js.map +1 -1
- package/dist/vocabulary/primitive/many.d.ts +2 -4
- package/dist/vocabulary/primitive/many.d.ts.map +1 -1
- package/dist/vocabulary/primitive/many.js +2 -4
- package/dist/vocabulary/primitive/many.js.map +1 -1
- package/dist/vocabulary/primitive/number.d.ts +2 -4
- package/dist/vocabulary/primitive/number.d.ts.map +1 -1
- package/dist/vocabulary/primitive/number.js +1 -3
- package/dist/vocabulary/primitive/number.js.map +1 -1
- package/dist/vocabulary/primitive/oneOf.d.ts +4 -7
- package/dist/vocabulary/primitive/oneOf.d.ts.map +1 -1
- package/dist/vocabulary/primitive/oneOf.js +19 -11
- package/dist/vocabulary/primitive/oneOf.js.map +1 -1
- package/dist/vocabulary/primitive/primary.d.ts +2 -2
- package/dist/vocabulary/primitive/primary.d.ts.map +1 -1
- package/dist/vocabulary/primitive/primary.js +2 -3
- package/dist/vocabulary/primitive/primary.js.map +1 -1
- package/dist/vocabulary/primitive/ref.d.ts +2 -6
- package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
- package/dist/vocabulary/primitive/ref.js +1 -1
- package/dist/vocabulary/primitive/ref.js.map +1 -1
- package/dist/vocabulary/primitive/text.d.ts +2 -4
- package/dist/vocabulary/primitive/text.d.ts.map +1 -1
- package/dist/vocabulary/primitive/text.js +1 -3
- package/dist/vocabulary/primitive/text.js.map +1 -1
- package/dist/vocabulary/rule/nullable.d.ts +0 -1
- package/dist/vocabulary/rule/nullable.d.ts.map +1 -1
- package/dist/vocabulary/rule/nullable.js +0 -1
- package/dist/vocabulary/rule/nullable.js.map +1 -1
- package/dist/vocabulary/rule/optional.d.ts +0 -1
- package/dist/vocabulary/rule/optional.d.ts.map +1 -1
- package/dist/vocabulary/rule/optional.js +0 -1
- package/dist/vocabulary/rule/optional.js.map +1 -1
- package/dist/vocabulary/rule/readOnly.d.ts +0 -2
- package/dist/vocabulary/rule/readOnly.d.ts.map +1 -1
- package/dist/vocabulary/rule/readOnly.js +0 -2
- package/dist/vocabulary/rule/readOnly.js.map +1 -1
- package/dist/vocabulary/rule/writeOnly.d.ts +0 -2
- package/dist/vocabulary/rule/writeOnly.d.ts.map +1 -1
- package/dist/vocabulary/rule/writeOnly.js +0 -2
- package/dist/vocabulary/rule/writeOnly.js.map +1 -1
- package/dist/vocabulary/vocabulary.d.ts +2 -3
- package/dist/vocabulary/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary/vocabulary.js +17 -21
- package/dist/vocabulary/vocabulary.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +11 -8
- package/src/SchemaConstraints.ts +40 -22
- package/src/SchemaDefinition.ts +7 -5
- package/src/SchemaError.ts +8 -2
- package/src/SchemaView.ts +3 -2
- package/src/axis/Axes.ts +3 -3
- package/src/axis/Axis.ts +13 -2
- package/src/axis/boundary/Boundary.ts +16 -2
- package/src/axis/boundary/Decoder.ts +2 -0
- package/src/axis/lifecycle/Lifecycle.ts +15 -4
- package/src/axis/lifecycle/apply.ts +1 -1
- package/src/axis/role/Role.ts +41 -9
- package/src/axis/shape/Shape.ts +89 -23
- package/src/axis/shape/ShapeFormat.ts +37 -0
- package/src/entity/EntityDeclarations.ts +3 -2
- package/src/entity/FieldGroups.ts +9 -0
- package/src/field/Field.ts +84 -21
- package/src/field/FieldFormat.ts +5 -5
- package/src/field/FieldSet.ts +27 -19
- package/src/index.ts +1 -0
- package/src/lib/Format.ts +36 -3
- package/src/lib/UnionToIntersection.ts +10 -0
- package/src/lib/utils.ts +5 -1
- package/src/projection/Cases.ts +25 -8
- package/src/projection/Visibility.ts +9 -6
- package/src/projection/card/Bundle.ts +2 -0
- package/src/projection/card/Card.ts +26 -27
- package/src/projection/card/CardForms.ts +1 -1
- package/src/projection/card/Diff.ts +4 -0
- package/src/projection/card/FieldExtension.ts +2 -2
- package/src/projection/card/RoleDescriptor.ts +9 -4
- package/src/projection/card/ShapeKeywords.ts +1 -1
- package/src/validator/FieldDeclarationValidator.ts +7 -10
- package/src/validator/FieldValueValidator.ts +3 -1
- package/src/validator/InputValidator.ts +17 -12
- package/src/vocabulary/preset/created.ts +3 -3
- package/src/vocabulary/preset/updated.ts +4 -3
- package/src/vocabulary/primitive/bool.ts +3 -8
- package/src/vocabulary/primitive/date.ts +5 -4
- package/src/vocabulary/primitive/json.ts +27 -12
- package/src/vocabulary/primitive/list.ts +16 -9
- package/src/vocabulary/primitive/many.ts +3 -4
- package/src/vocabulary/primitive/number.ts +3 -8
- package/src/vocabulary/primitive/oneOf.ts +40 -24
- package/src/vocabulary/primitive/primary.ts +9 -5
- package/src/vocabulary/primitive/ref.ts +3 -7
- package/src/vocabulary/primitive/text.ts +3 -8
- package/src/vocabulary/rule/nullable.ts +0 -1
- package/src/vocabulary/rule/optional.ts +0 -1
- package/src/vocabulary/rule/readOnly.ts +0 -2
- package/src/vocabulary/rule/writeOnly.ts +0 -2
- package/src/vocabulary/vocabulary.ts +18 -22
- package/dist/entity/CompositeUnique.d.ts +0 -4
- package/dist/entity/CompositeUnique.d.ts.map +0 -1
- package/dist/entity/CompositeUnique.js +0 -2
- package/dist/entity/CompositeUnique.js.map +0 -1
- package/dist/field/FougereFieldAxes.d.ts.map +0 -1
- package/dist/field/FougereFieldAxes.js.map +0 -1
- package/dist/field/Meta.d.ts +0 -6
- package/dist/field/Meta.d.ts.map +0 -1
- package/dist/field/Meta.js +0 -3
- package/dist/field/Meta.js.map +0 -1
- package/src/entity/CompositeUnique.ts +0 -6
- package/src/field/Meta.ts +0 -5
- /package/dist/{field/FougereFieldAxes.d.ts → FougereFieldAxes.d.ts} +0 -0
- /package/dist/{field/FougereFieldAxes.js → FougereFieldAxes.js} +0 -0
- /package/src/{field/FougereFieldAxes.ts → FougereFieldAxes.ts} +0 -0
package/src/axis/shape/Shape.ts
CHANGED
|
@@ -4,27 +4,58 @@ import type { ShapeType } from './ShapeType.js';
|
|
|
4
4
|
|
|
5
5
|
type Nullably<T extends string> = T | readonly [T, 'null'];
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** The sentence a field carries, where JSON Schema already holds one. */
|
|
8
|
+
interface Documented {
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface StringConstraints extends Documented {
|
|
13
|
+
minLength?: number;
|
|
14
|
+
maxLength?: number;
|
|
15
|
+
pattern?: string;
|
|
16
|
+
enum?: readonly (string | null)[];
|
|
17
|
+
format?: StringFormat;
|
|
18
|
+
}
|
|
8
19
|
|
|
9
|
-
interface NumericConstraints
|
|
20
|
+
interface NumericConstraints extends Documented {
|
|
21
|
+
minimum?: number;
|
|
22
|
+
maximum?: number;
|
|
23
|
+
enum?: readonly (number | null)[];
|
|
24
|
+
}
|
|
10
25
|
|
|
11
|
-
interface ArrayConstraints
|
|
26
|
+
interface ArrayConstraints extends Documented {
|
|
27
|
+
items?: Shape;
|
|
28
|
+
minItems?: number;
|
|
29
|
+
maxItems?: number;
|
|
30
|
+
}
|
|
12
31
|
|
|
13
|
-
interface ObjectConstraints
|
|
32
|
+
interface ObjectConstraints extends Documented {
|
|
33
|
+
properties?: Record<string, unknown>;
|
|
34
|
+
required?: readonly string[];
|
|
35
|
+
additionalProperties?: boolean | Shape;
|
|
36
|
+
propertyNames?: Shape;
|
|
37
|
+
}
|
|
14
38
|
|
|
15
39
|
export type Shape =
|
|
16
40
|
| ({ type: Nullably<'string'> } & StringConstraints)
|
|
17
41
|
| ({ type: Nullably<'number'> | Nullably<'integer'> } & NumericConstraints)
|
|
18
|
-
| { type: Nullably<'boolean'> }
|
|
42
|
+
| ({ type: Nullably<'boolean'> } & Documented)
|
|
19
43
|
| ({ type: Nullably<'array'> } & ArrayConstraints)
|
|
20
44
|
| ({ type: Nullably<'object'> } & ObjectConstraints);
|
|
21
45
|
|
|
22
|
-
const SHAPE_TYPES = [
|
|
46
|
+
export const SHAPE_TYPES = [
|
|
47
|
+
'string',
|
|
48
|
+
'number',
|
|
49
|
+
'integer',
|
|
50
|
+
'boolean',
|
|
51
|
+
'array',
|
|
52
|
+
'object',
|
|
53
|
+
] as const;
|
|
23
54
|
|
|
24
55
|
type BaseShape =
|
|
25
56
|
| ({ type: 'string' } & StringConstraints)
|
|
26
57
|
| ({ type: 'number' | 'integer' } & NumericConstraints)
|
|
27
|
-
| { type: 'boolean' }
|
|
58
|
+
| ({ type: 'boolean' } & Documented)
|
|
28
59
|
| ({ type: 'array' } & ArrayConstraints)
|
|
29
60
|
| ({ type: 'object' } & ObjectConstraints);
|
|
30
61
|
|
|
@@ -42,26 +73,21 @@ type _ShapeConformsToJsonSchema = Assert<
|
|
|
42
73
|
>;
|
|
43
74
|
|
|
44
75
|
type _ShapeTypesAreTheStandardsLessNull = Assert<
|
|
45
|
-
[Exclude<Exclude<JSONSchema7TypeName, 'null'>, (typeof SHAPE_TYPES)[number]>] extends [
|
|
76
|
+
[Exclude<Exclude<JSONSchema7TypeName, 'null'>, (typeof SHAPE_TYPES)[number]>] extends [
|
|
77
|
+
never,
|
|
78
|
+
]
|
|
46
79
|
? true
|
|
47
80
|
: false
|
|
48
81
|
>;
|
|
49
82
|
|
|
83
|
+
/** Every type name a shape states, whether it states one or a union of them. */
|
|
84
|
+
const typesOf = (shape: Shape): readonly string[] =>
|
|
85
|
+
(Array.isArray(shape.type) ? shape.type : [shape.type]) as readonly string[];
|
|
86
|
+
|
|
50
87
|
export class Shapes {
|
|
51
88
|
private static readonly cache = new WeakMap<object, ShapeParts>();
|
|
52
89
|
private static readonly none: ShapeParts = { base: undefined, nullable: false };
|
|
53
90
|
|
|
54
|
-
static is(value: unknown): value is Shape {
|
|
55
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
56
|
-
const type = (value as Shape).type;
|
|
57
|
-
const names = Array.isArray(type) ? type : [type];
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
names.some((name) => (SHAPE_TYPES as readonly unknown[]).includes(name)) &&
|
|
61
|
-
names.every((name) => name === 'null' || (SHAPE_TYPES as readonly unknown[]).includes(name))
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
91
|
/**
|
|
66
92
|
* Every `pattern` a shape states, the nested ones included — `items`, `properties`.
|
|
67
93
|
*
|
|
@@ -89,11 +115,28 @@ export class Shapes {
|
|
|
89
115
|
for (const member of Object.values(value)) Shapes.collectPatterns(member, found);
|
|
90
116
|
}
|
|
91
117
|
|
|
118
|
+
/**
|
|
119
|
+
* `null` enters the grammar, and every constraint stays. It is added when it is ABSENT,
|
|
120
|
+
* read off the shape rather than assumed from its form — a shape already stating it comes
|
|
121
|
+
* back as itself, so `optional(nullable(text()))` does the work once.
|
|
122
|
+
*
|
|
123
|
+
* `enum` is widened TOO, because JSON Schema checks it apart from `type`: a shape saying
|
|
124
|
+
* `['string','null']` and `enum: ['draft','live']` calls null a legal type and not a legal
|
|
125
|
+
* value, and the engine answers `Instance does not match any of ["draft","live"]`.
|
|
126
|
+
*
|
|
127
|
+
* `{ type: 'string', minLength: 3 }` → `{ type: ['string','null'], minLength: 3 }`
|
|
128
|
+
* `{ type: 'string', enum: ['draft'] }` → `{ type: ['string','null'], enum: ['draft', null] }`
|
|
129
|
+
*/
|
|
92
130
|
static nullable(shape: Shape): Shape {
|
|
93
|
-
|
|
94
|
-
|
|
131
|
+
const types = typesOf(shape);
|
|
132
|
+
if (types.includes('null')) return shape;
|
|
133
|
+
|
|
134
|
+
const nullable = { ...shape, type: [...types, 'null'] } as unknown as Shape;
|
|
95
135
|
if ('enum' in nullable && nullable.enum && !nullable.enum.includes(null)) {
|
|
96
|
-
(nullable as { enum: readonly (string | null)[] }).enum = [
|
|
136
|
+
(nullable as { enum: readonly (string | number | null)[] }).enum = [
|
|
137
|
+
...nullable.enum,
|
|
138
|
+
null,
|
|
139
|
+
];
|
|
97
140
|
}
|
|
98
141
|
|
|
99
142
|
return nullable;
|
|
@@ -107,7 +150,9 @@ export class Shapes {
|
|
|
107
150
|
const baseType = shape.type.find((t) => t !== 'null');
|
|
108
151
|
const base = { ...shape, type: baseType } as BaseShape;
|
|
109
152
|
if ('enum' in base && base.enum) {
|
|
110
|
-
(base as { enum: readonly (string | null)[] }).enum = base.enum.filter(
|
|
153
|
+
(base as { enum: readonly (string | number | null)[] }).enum = base.enum.filter(
|
|
154
|
+
(v) => v !== null,
|
|
155
|
+
);
|
|
111
156
|
}
|
|
112
157
|
parts = { base, nullable: true };
|
|
113
158
|
} else {
|
|
@@ -115,6 +160,7 @@ export class Shapes {
|
|
|
115
160
|
}
|
|
116
161
|
this.cache.set(shape, parts);
|
|
117
162
|
}
|
|
163
|
+
|
|
118
164
|
return parts;
|
|
119
165
|
}
|
|
120
166
|
|
|
@@ -122,6 +168,26 @@ export class Shapes {
|
|
|
122
168
|
return this.of(shape).nullable;
|
|
123
169
|
}
|
|
124
170
|
|
|
171
|
+
/**
|
|
172
|
+
* A value that came in as text — a form control, a command-line flag — read as the shape
|
|
173
|
+
* declares it. What does not read that way stays as it came, for the judge to refuse by name.
|
|
174
|
+
* `fromText({ type: 'integer' }, '12')` → `12`, and `fromText({ type: 'integer' }, 'abc')` → `'abc'`
|
|
175
|
+
*/
|
|
176
|
+
static fromText(shape: Shape | undefined, value: unknown): unknown {
|
|
177
|
+
const type = this.of(shape).base?.type;
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
(type !== 'number' && type !== 'integer') ||
|
|
181
|
+
typeof value !== 'string' ||
|
|
182
|
+
value === ''
|
|
183
|
+
)
|
|
184
|
+
return value;
|
|
185
|
+
|
|
186
|
+
const number = Number(value);
|
|
187
|
+
|
|
188
|
+
return Number.isFinite(number) ? number : value;
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
/**
|
|
126
192
|
* `shape.type` read off the base, except that a `string` answers `date` or `choice`.
|
|
127
193
|
* FR : `shape.type` lu sur la base, sauf qu'une chaîne répond `date` ou `choice`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Format } from '../../lib/Format.js';
|
|
2
|
+
import { SHAPE_TYPES, type Shape } from './Shape.js';
|
|
3
|
+
|
|
4
|
+
const SHAPE = 'field/shape';
|
|
5
|
+
|
|
6
|
+
/** A word, or that word beside `null` — the second place is what makes a shape nullable. */
|
|
7
|
+
const TYPE = Format.either(
|
|
8
|
+
Format.tokens(SHAPE_TYPES),
|
|
9
|
+
Format.tuple(Format.tokens(SHAPE_TYPES), Format.tokens(['null'])),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What a shape may state — JSON Schema's own keywords, less the ones this package has no word
|
|
14
|
+
* for. `shape` used to be the one key of a field declaration nothing judged, so `minLenght: 1`
|
|
15
|
+
* passed and was dropped where `lifecycle: { craete: 'now' }` had been refused since `Format`.
|
|
16
|
+
* `properties` stays open because a projection folds `meta.description` into it.
|
|
17
|
+
*/
|
|
18
|
+
export const SHAPE_FORMAT = Format.of(SHAPE)
|
|
19
|
+
.key('type', TYPE)
|
|
20
|
+
.key('description', Format.text)
|
|
21
|
+
.key('minLength', Format.count)
|
|
22
|
+
.key('maxLength', Format.count)
|
|
23
|
+
.key('pattern', Format.text)
|
|
24
|
+
.key('format', Format.text)
|
|
25
|
+
.key('enum', Format.listOf(Format.anything))
|
|
26
|
+
.key('minimum', Format.number)
|
|
27
|
+
.key('maximum', Format.number)
|
|
28
|
+
.key('items', Format.ref(SHAPE))
|
|
29
|
+
.key('minItems', Format.count)
|
|
30
|
+
.key('maxItems', Format.count)
|
|
31
|
+
.key('properties', Format.anything)
|
|
32
|
+
.key('required', Format.listOf(Format.text))
|
|
33
|
+
.key('additionalProperties', Format.anything)
|
|
34
|
+
.key('propertyNames', Format.ref(SHAPE))
|
|
35
|
+
.needs('type')
|
|
36
|
+
.closed()
|
|
37
|
+
.as<Shape>();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Fields } from '../field/Fields.js';
|
|
2
2
|
import type { EntityAdapters } from './EntityAdapters.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { FieldGroups } from './FieldGroups.js';
|
|
4
4
|
import type { PreviousNames } from './PreviousNames.js';
|
|
5
5
|
|
|
6
6
|
export interface EntityDeclarations<TFields extends Fields> {
|
|
7
7
|
adapters?: EntityAdapters<TFields>;
|
|
8
8
|
previous?: PreviousNames<TFields>;
|
|
9
|
-
unique?:
|
|
9
|
+
unique?: FieldGroups<TFields>;
|
|
10
|
+
index?: FieldGroups<TFields>;
|
|
10
11
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FieldName } from '../field/FieldName.js';
|
|
2
|
+
import type { Fields } from '../field/Fields.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Field names grouped, as `unique` and `index` both are — `[['listId', 'docId'], ['slug']]`.
|
|
6
|
+
* The ORDER inside a group counts for both: a unique constraint on `(a, b)` is not the one on
|
|
7
|
+
* `(b, a)`, and an index on `(a, b)` serves a filter on `a` alone and never on `b` alone.
|
|
8
|
+
*/
|
|
9
|
+
export type FieldGroups<TFields extends Fields> = readonly (readonly FieldName<TFields>[])[];
|
package/src/field/Field.ts
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
import type { Shape } from '../axis/shape/Shape.js';
|
|
2
|
-
import type { RoleRules } from '../axis/role/Role.js';
|
|
3
|
-
import type { LifecycleRules } from '../axis/lifecycle/Lifecycle.js';
|
|
4
|
-
import type { BoundaryRef } from '../axis/boundary/Boundary.js';
|
|
5
|
-
import type { Meta } from './Meta.js';
|
|
6
2
|
import { Axes } from '../axis/Axes.js';
|
|
7
|
-
import type { FougereFieldAxes } from '
|
|
3
|
+
import type { FougereFieldAxes } from '../FougereFieldAxes.js';
|
|
8
4
|
import { FieldDeclarationValidator } from '../validator/FieldDeclarationValidator.js';
|
|
9
5
|
import { FieldValueValidator } from '../validator/FieldValueValidator.js';
|
|
6
|
+
import { isObject } from '../lib/utils.js';
|
|
10
7
|
import { dotted } from '../lib/ValidationResult.js';
|
|
11
8
|
import { SchemaError } from '../SchemaError.js';
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
/** The sentence any word admits, whatever it shapes — a relation and a date carry one too. */
|
|
11
|
+
export interface Described {
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** And a value it is born with, for the words that shape one. `many()` has none to hold. */
|
|
16
|
+
export interface Shared<T> extends Described {
|
|
17
|
+
default?: T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FieldDeclaration extends FougereFieldAxes {
|
|
14
21
|
shape: Shape;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every word ends on `setShared`, so the options are read in ONE place and refused there.
|
|
26
|
+
* `text(42)` used to pass for a bare `text()`, and nine of the twelve words took it.
|
|
27
|
+
*/
|
|
28
|
+
const refuseLooseOptions = (opts: unknown): void => {
|
|
29
|
+
if (!isObject(opts))
|
|
30
|
+
throw new SchemaError('Options must go in an object, like { max: 200 }', { received: opts });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The registry answers strings, and a member of a declaration is what they name. */
|
|
34
|
+
const registered = (): (keyof FieldDeclaration)[] => Axes.names as (keyof FieldDeclaration)[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The axes it carries are `FougereFieldAxes`, which each axis writes its own line in — so the
|
|
38
|
+
* three of the core are typed the way a fourth declared outside is, and the class names what
|
|
39
|
+
* the registry decides only once, in the constructor.
|
|
40
|
+
*/
|
|
41
|
+
export interface Field<T = unknown> extends FougereFieldAxes {
|
|
42
|
+
readonly _type?: T;
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
export class Field<T = unknown> {
|
|
22
46
|
readonly shape: Shape;
|
|
23
|
-
readonly role?: RoleRules;
|
|
24
|
-
readonly lifecycle?: LifecycleRules;
|
|
25
|
-
readonly boundary?: BoundaryRef;
|
|
26
|
-
readonly meta?: Meta;
|
|
27
|
-
|
|
28
|
-
declare readonly _type?: T;
|
|
29
47
|
|
|
30
48
|
constructor(init: FieldDeclaration, key?: string) {
|
|
31
49
|
const verdict = FieldDeclarationValidator.of(init).verdict;
|
|
@@ -38,12 +56,11 @@ export class Field<T = unknown> {
|
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
this.shape = init.shape;
|
|
41
|
-
this.meta = init.meta;
|
|
42
59
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
for (const name of registered()) {
|
|
61
|
+
Object.defineProperty(this, name, {
|
|
62
|
+
value: init[name], writable: true, enumerable: true, configurable: true,
|
|
63
|
+
});
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
const create = this.lifecycle?.create;
|
|
@@ -61,7 +78,53 @@ export class Field<T = unknown> {
|
|
|
61
78
|
return FieldDeclarationValidator.of(value).verdict.success;
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
/**
|
|
82
|
+
* What this field declares on ONE axis, a fourth one registered outside included. An axis is
|
|
83
|
+
* a member, so it is read by NAME, and three callers reached for it through a cast of their
|
|
84
|
+
* own before this existed.
|
|
85
|
+
*/
|
|
86
|
+
axis(name: keyof FieldDeclaration): unknown {
|
|
87
|
+
return (this satisfies FieldDeclaration)[name];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Every axis it declares, and nothing for the ones it says nothing on. */
|
|
91
|
+
get axes(): Record<string, unknown> {
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
registered()
|
|
94
|
+
.map((name) => [name, this.axis(name)])
|
|
95
|
+
.filter(([, declared]) => declared !== undefined),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether it states anything at all beside its shape. */
|
|
100
|
+
hasAxes(): boolean {
|
|
101
|
+
return registered().some((name) => this.axis(name) !== undefined);
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
with<U = T>(overrides: Partial<FieldDeclaration>): Field<U> {
|
|
65
105
|
return new Field<U>({ ...this, ...overrides });
|
|
66
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a word admits whatever its shape, and where it lands: `description` IS a JSON Schema
|
|
110
|
+
* keyword, `default` IS a lifecycle rule. Stated here alone, so no word writes the conversion.
|
|
111
|
+
* FR : ce que tout mot admet quelle que soit sa forme, et où ça atterrit.
|
|
112
|
+
* `text({ max: 200 }).setShared({ description: 'The title' })` → `shape.description`
|
|
113
|
+
*/
|
|
114
|
+
setShared(opts?: Shared<T>): Field<T> {
|
|
115
|
+
if (opts === undefined) return this;
|
|
116
|
+
|
|
117
|
+
refuseLooseOptions(opts);
|
|
118
|
+
|
|
119
|
+
const overrides: Partial<FieldDeclaration> = {
|
|
120
|
+
...(opts.description !== undefined
|
|
121
|
+
? { shape: { ...this.shape, description: opts.description } }
|
|
122
|
+
: {}),
|
|
123
|
+
...(opts.default !== undefined
|
|
124
|
+
? { lifecycle: { ...this.lifecycle, create: { value: opts.default } } }
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return Object.keys(overrides).length ? this.with<T>(overrides) : this;
|
|
129
|
+
}
|
|
67
130
|
}
|
package/src/field/FieldFormat.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type { Axis } from '../axis/Axis.js';
|
|
2
2
|
import { Format } from '../lib/Format.js';
|
|
3
3
|
import type { JsonSchema } from '../lib/JsonSchema.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SHAPE_FORMAT } from '../axis/shape/ShapeFormat.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* What a field may state, composed from the keys that state a format: each is carried whole and
|
|
8
8
|
* cited by its `$id`, so the document travels alone and a key keeps its own identity. `shape` is
|
|
9
|
-
* JSON Schema
|
|
9
|
+
* cited like the rest — it is JSON Schema, and JSON Schema is what states its keywords.
|
|
10
10
|
*/
|
|
11
11
|
export function fieldFormat(axes: readonly [string, Axis][]): JsonSchema {
|
|
12
12
|
const document = axes.reduce(
|
|
13
|
-
(held, [
|
|
14
|
-
Format.of('field').
|
|
13
|
+
(held, [name, axis]) => held.cites(name, axis.format),
|
|
14
|
+
Format.of('field').cites('shape', SHAPE_FORMAT),
|
|
15
15
|
);
|
|
16
16
|
|
|
17
|
-
return document.
|
|
17
|
+
return document.closed().schema as JsonSchema;
|
|
18
18
|
}
|
package/src/field/FieldSet.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Role } from '../axis/role/Role.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FieldGroups } from '../entity/FieldGroups.js';
|
|
3
3
|
import { Field } from './Field.js';
|
|
4
4
|
import { type FieldName } from './FieldName.js';
|
|
5
5
|
import { type Fields } from './Fields.js';
|
|
@@ -17,39 +17,47 @@ export class FieldSet<TFields extends Fields = Fields> {
|
|
|
17
17
|
* FR : partage un `unique` déclaré : un groupe d'un va au champ, plusieurs restent au schéma.
|
|
18
18
|
* `declaring(fields, [['email'], ['listId', 'docId']])` → `email` carries it, the pair is a group
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* A group of ONE is the word on the field — `unique: [['slug']]` is `unique(text())` — so it
|
|
22
|
+
* is written there and never kept as a group, which is what lets a derivation drop it with
|
|
23
|
+
* its field. A group of several is what no single field can state, and it is kept whole.
|
|
24
|
+
*/
|
|
20
25
|
static declaring<TFields extends Fields>(
|
|
21
26
|
declared: TFields,
|
|
22
|
-
unique?:
|
|
23
|
-
): { fields: TFields;
|
|
27
|
+
groups: { unique?: FieldGroups<TFields>; index?: FieldGroups<TFields> } = {},
|
|
28
|
+
): { fields: TFields; unique: FieldGroups<TFields>; index: FieldGroups<TFields> } {
|
|
24
29
|
const fields: Fields = {};
|
|
25
30
|
for (const [key, field] of Object.entries(declared))
|
|
26
31
|
fields[key] = new Field(field, key);
|
|
27
32
|
|
|
28
|
-
const composite: FieldName<TFields>[][]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const composite = { unique: [] as FieldName<TFields>[][], index: [] as FieldName<TFields>[][] };
|
|
34
|
+
|
|
35
|
+
for (const kind of ['unique', 'index'] as const) {
|
|
36
|
+
for (const group of groups[kind] ?? []) {
|
|
37
|
+
const missing = group.filter((key) => !Object.hasOwn(fields, key));
|
|
38
|
+
if (missing.length)
|
|
39
|
+
throw new SchemaError(
|
|
40
|
+
`${kind}: [${group.join(', ')}] names ` +
|
|
41
|
+
`${missing.map((key) => `'${key}'`).join(', ')}, which the entity does not declare.`,
|
|
42
|
+
);
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
if (group.length === 1) {
|
|
45
|
+
const key = group[0]!;
|
|
46
|
+
const alone = fields[key]!;
|
|
47
|
+
fields[key] = alone.with({ role: { ...alone.role, [kind]: true } });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
composite[kind].push([...group]);
|
|
42
51
|
}
|
|
43
|
-
composite.push([...group]);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
return { fields: fields as TFields,
|
|
54
|
+
return { fields: fields as TFields, unique: composite.unique, index: composite.index };
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/** A second `primary` is refused, naming both, rather than the first winning silently. */
|
|
50
58
|
get primary(): FieldName<TFields> | undefined {
|
|
51
59
|
const primaries = Object.entries(this.fields)
|
|
52
|
-
.filter(([, field]) => Role.of(field).isPrimary)
|
|
60
|
+
.filter(([, field]) => Role.of(field).isPrimary())
|
|
53
61
|
.map(([name]) => name);
|
|
54
62
|
|
|
55
63
|
if (primaries.length > 1) {
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { StringFormats } from './axis/shape/StringFormats.js';
|
|
|
41
41
|
export { Generators } from './axis/lifecycle/Generators.js';
|
|
42
42
|
export { Boundaries } from './axis/boundary/Decoder.js';
|
|
43
43
|
export { type FougereEntityAdapters } from './entity/FougereEntityAdapters.js';
|
|
44
|
+
export { type FougereFieldAxes } from './FougereFieldAxes.js';
|
|
44
45
|
export { InputValidator } from './validator/InputValidator.js';
|
|
45
46
|
export { FieldValueValidator } from './validator/FieldValueValidator.js';
|
|
46
47
|
export { JsonSchemaValidator } from './validator/JsonSchemaValidator.js';
|
package/src/lib/Format.ts
CHANGED
|
@@ -21,6 +21,8 @@ export class Format<T = unknown> {
|
|
|
21
21
|
static readonly anything = new Format<unknown>(true);
|
|
22
22
|
static readonly text = new Format<string>({ type: 'string' });
|
|
23
23
|
static readonly flag = new Format<boolean>({ type: 'boolean' });
|
|
24
|
+
static readonly number = new Format<number>({ type: 'number' });
|
|
25
|
+
static readonly count = new Format<number>({ type: 'integer', minimum: 0 });
|
|
24
26
|
|
|
25
27
|
declare readonly _admits?: T;
|
|
26
28
|
|
|
@@ -34,12 +36,43 @@ export class Format<T = unknown> {
|
|
|
34
36
|
return new Format({ enum: [...words] });
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
/**
|
|
38
|
-
static
|
|
39
|
+
/** `enum`, `required` — a list of one format, so `enum: 'draft'` is refused as the scalar it is. */
|
|
40
|
+
static listOf<Value>(format: Format<Value>): Format<Value[]> {
|
|
41
|
+
return new Format({ type: 'array', items: format.schema });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A list of a FIXED length, each place stating its own format — `['string', 'null']` and
|
|
46
|
+
* never `['string', 'number']`, which a list of one format cannot tell apart.
|
|
47
|
+
*/
|
|
48
|
+
static tuple<const Places extends readonly Format<unknown>[]>(
|
|
49
|
+
...places: Places
|
|
50
|
+
): Format<{ -readonly [K in keyof Places]: Accepted<Places[K]> }> {
|
|
51
|
+
return new Format({
|
|
52
|
+
type: 'array',
|
|
53
|
+
prefixItems: places.map((place) => place.schema),
|
|
54
|
+
minItems: places.length,
|
|
55
|
+
maxItems: places.length,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A format naming ITSELF, which no value can do while it is being built: `items` holds a shape,
|
|
61
|
+
* and the shape is what declares `items`. The `$id` `of()` already posts is what it points at.
|
|
62
|
+
*/
|
|
63
|
+
static ref(id: string): Format<unknown> {
|
|
64
|
+
return new Format({ $ref: absolute(id) as string });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A word or anything else, told apart by TYPE, so a refusal names the half the value belongs
|
|
69
|
+
* to — `create: 'nawak'` reads against the words, `type: ['string', 'null']` against the list.
|
|
70
|
+
*/
|
|
71
|
+
static either<Word, Other>(word: Format<Word>, otherwise: Format<Other>): Format<Word | Other> {
|
|
39
72
|
return new Format({
|
|
40
73
|
if: { type: 'string' },
|
|
41
74
|
then: word.schema as JsonSchema,
|
|
42
|
-
else:
|
|
75
|
+
else: otherwise.schema as JsonSchema,
|
|
43
76
|
});
|
|
44
77
|
}
|
|
45
78
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a union becomes when every member must hold at once — `A | B` → `A & B`, through the
|
|
3
|
+
* contravariance of a parameter position, which is the only place TypeScript folds a union.
|
|
4
|
+
* `Merged<[Post, Draft]>` and `ShapeKeywords` both need it, and each had written it out.
|
|
5
|
+
*/
|
|
6
|
+
export type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never) extends (
|
|
7
|
+
member: infer Held,
|
|
8
|
+
) => void
|
|
9
|
+
? Held
|
|
10
|
+
: never;
|
package/src/lib/utils.ts
CHANGED
|
@@ -15,4 +15,8 @@ export function clean<T extends Record<string, unknown>>(obj: T): T {
|
|
|
15
15
|
export const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
16
16
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const isArrayOf = <T>(
|
|
19
|
+
values: readonly unknown[],
|
|
20
|
+
is: (value: unknown) => value is T,
|
|
21
|
+
): values is readonly T[] => values.every(is);
|
|
22
|
+
|
package/src/projection/Cases.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ValidationCase } from './ValidationCase.js';
|
|
|
16
16
|
function wrongTypeFor(field: Field): unknown {
|
|
17
17
|
const declared = (field.shape as { type?: string | readonly string[] }).type;
|
|
18
18
|
const names = Array.isArray(declared) ? declared : [declared];
|
|
19
|
+
|
|
19
20
|
return names.includes('string') ? 42 : 'not-a-value-of-this-shape';
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -42,6 +43,11 @@ function outOfBoundsFor(field: Field): { why: string; value: unknown }[] {
|
|
|
42
43
|
cases.push({ why: 'below minimum', value: shape.minimum - 1 });
|
|
43
44
|
if (typeof shape.maximum === 'number')
|
|
44
45
|
cases.push({ why: 'above maximum', value: shape.maximum + 1 });
|
|
46
|
+
if (shape.enum) {
|
|
47
|
+
const members = shape.enum.filter((value) => typeof value === 'number');
|
|
48
|
+
|
|
49
|
+
cases.push({ why: 'outside the stated set', value: Math.max(...members) + 1 });
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
if (shape?.type === 'array') {
|
|
47
53
|
if (typeof shape.minItems === 'number' && shape.minItems > 0)
|
|
@@ -52,6 +58,7 @@ function outOfBoundsFor(field: Field): { why: string; value: unknown }[] {
|
|
|
52
58
|
value: Array.from({ length: shape.maxItems + 1 }, () => null),
|
|
53
59
|
});
|
|
54
60
|
}
|
|
61
|
+
|
|
55
62
|
return cases;
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -64,8 +71,8 @@ function outOfBoundsFor(field: Field): { why: string; value: unknown }[] {
|
|
|
64
71
|
export class Cases {
|
|
65
72
|
private constructor(readonly all: readonly ValidationCase[]) {}
|
|
66
73
|
|
|
67
|
-
static of(
|
|
68
|
-
return new Cases(enumerate(
|
|
74
|
+
static of(schema: SchemaView, valid: Record<string, unknown>): Cases {
|
|
75
|
+
return new Cases(enumerate(schema, valid));
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
static holds(
|
|
@@ -74,6 +81,7 @@ export class Cases {
|
|
|
74
81
|
): boolean {
|
|
75
82
|
if (expected === 'accept') return result.success;
|
|
76
83
|
if (result.success) return false;
|
|
84
|
+
|
|
77
85
|
return (result.errors ?? []).some((error) => rejected(error.path) === expected.reject);
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -92,8 +100,8 @@ export class Cases {
|
|
|
92
100
|
* FR : lit les quatre axes plutôt qu'une liste tenue à la main : un cas existe parce qu'un
|
|
93
101
|
* champ déclare la règle qu'il casse.
|
|
94
102
|
*/
|
|
95
|
-
function enumerate(
|
|
96
|
-
const fields =
|
|
103
|
+
function enumerate(schema: SchemaView, valid: Record<string, unknown>): ValidationCase[] {
|
|
104
|
+
const fields = schema.getFields();
|
|
97
105
|
const validator = InputValidator.of(fields);
|
|
98
106
|
|
|
99
107
|
return [
|
|
@@ -124,13 +132,13 @@ function aboutField(
|
|
|
124
132
|
const withField = (value: unknown) => ({ ...valid, [name]: value });
|
|
125
133
|
const cases: ValidationCase[] = [];
|
|
126
134
|
|
|
127
|
-
if (validator.
|
|
135
|
+
if (validator.requires(field) && name in valid) {
|
|
128
136
|
const input = { ...valid };
|
|
129
137
|
delete input[name];
|
|
130
138
|
cases.push({ why: `${name} absent`, input, patch: false, expect: { reject: name } });
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
if (Boundary.of(field).readOnly) {
|
|
141
|
+
if (Boundary.of(field).readOnly()) {
|
|
134
142
|
cases.push({
|
|
135
143
|
why: `${name} supplied although read-only`,
|
|
136
144
|
input: withField(wrongTypeFor(field)),
|
|
@@ -139,7 +147,16 @@ function aboutField(
|
|
|
139
147
|
});
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
if (
|
|
150
|
+
if (Role.of(field).isCollection()) {
|
|
151
|
+
cases.push({
|
|
152
|
+
why: `${name} supplied although the other side carries it`,
|
|
153
|
+
input: withField([]),
|
|
154
|
+
patch: false,
|
|
155
|
+
expect: { reject: name },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (Lifecycle.of(field).immutable() && !Role.of(field).isPrimary()) {
|
|
143
160
|
cases.push({
|
|
144
161
|
why: `${name} supplied on an update`,
|
|
145
162
|
input: { [name]: valid[name] ?? wrongTypeFor(field) },
|
|
@@ -150,7 +167,7 @@ function aboutField(
|
|
|
150
167
|
|
|
151
168
|
// A reference names a row that must exist; the caller supplied its id and we do not
|
|
152
169
|
// get to invent a second one, so the only case we can state about it is the bound one.
|
|
153
|
-
if (name in valid && !Role.of(field).isReference) {
|
|
170
|
+
if (name in valid && !Role.of(field).isReference()) {
|
|
154
171
|
cases.push({
|
|
155
172
|
why: `${name} of the wrong type`,
|
|
156
173
|
input: withField(wrongTypeFor(field)),
|