@fougere/schema 0.8.2-alpha.0 → 0.8.4-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/Schema.d.ts +3 -6
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +9 -17
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaDefinition.d.ts +24 -6
- package/dist/SchemaDefinition.d.ts.map +1 -1
- package/dist/SchemaDefinition.js +76 -44
- package/dist/SchemaDefinition.js.map +1 -1
- package/dist/SchemaView.d.ts +1 -1
- package/dist/SchemaView.d.ts.map +1 -1
- package/dist/axis/Axis.d.ts +1 -1
- package/dist/axis/Axis.d.ts.map +1 -1
- package/dist/axis/boundary/Boundaries.d.ts +5 -0
- package/dist/axis/boundary/Boundaries.d.ts.map +1 -1
- package/dist/axis/boundary/Boundaries.js.map +1 -1
- package/dist/axis/boundary/Boundary.d.ts +5 -4
- package/dist/axis/boundary/Boundary.d.ts.map +1 -1
- package/dist/axis/boundary/Boundary.js +4 -5
- package/dist/axis/boundary/Boundary.js.map +1 -1
- package/dist/axis/boundary/BoundaryAxis.js +2 -2
- package/dist/axis/boundary/BoundaryAxis.js.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.d.ts +5 -3
- package/dist/axis/lifecycle/Lifecycle.d.ts.map +1 -1
- package/dist/axis/lifecycle/Lifecycle.js +5 -1
- package/dist/axis/lifecycle/Lifecycle.js.map +1 -1
- package/dist/axis/lifecycle/LifecycleAxis.js +6 -6
- package/dist/axis/lifecycle/LifecycleAxis.js.map +1 -1
- package/dist/axis/lifecycle/apply.d.ts +6 -6
- package/dist/axis/lifecycle/apply.d.ts.map +1 -1
- package/dist/axis/lifecycle/apply.js +10 -9
- package/dist/axis/lifecycle/apply.js.map +1 -1
- package/dist/axis/role/Role.d.ts +8 -5
- package/dist/axis/role/Role.d.ts.map +1 -1
- package/dist/axis/role/Role.js +7 -0
- package/dist/axis/role/Role.js.map +1 -1
- package/dist/axis/role/RoleAxis.js +9 -9
- package/dist/axis/role/RoleAxis.js.map +1 -1
- package/dist/axis/shape/Shape.d.ts +14 -0
- package/dist/axis/shape/Shape.d.ts.map +1 -1
- package/dist/axis/shape/Shape.js +43 -34
- package/dist/axis/shape/Shape.js.map +1 -1
- package/dist/entity/EntityAdapterSet.d.ts +2 -2
- package/dist/entity/EntityAdapterSet.d.ts.map +1 -1
- package/dist/entity/EntityAdapterSet.js +2 -2
- package/dist/entity/EntityAdapterSet.js.map +1 -1
- package/dist/entity.d.ts +2 -2
- package/dist/entity.js +2 -2
- package/dist/field/Field.d.ts.map +1 -1
- package/dist/field/Field.js +2 -1
- package/dist/field/Field.js.map +1 -1
- package/dist/field/FieldSet.d.ts +4 -7
- package/dist/field/FieldSet.d.ts.map +1 -1
- package/dist/field/FieldSet.js +4 -15
- package/dist/field/FieldSet.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/validation.d.ts +32 -0
- package/dist/lib/validation.d.ts.map +1 -0
- package/dist/lib/validation.js +6 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/projection/Cases.d.ts +1 -1
- package/dist/projection/Cases.d.ts.map +1 -1
- package/dist/projection/Cases.js +11 -7
- package/dist/projection/Cases.js.map +1 -1
- package/dist/projection/Visibility.js +1 -1
- package/dist/projection/Visibility.js.map +1 -1
- package/dist/projection/card/Bundle.d.ts.map +1 -1
- package/dist/projection/card/Bundle.js +1 -4
- package/dist/projection/card/Bundle.js.map +1 -1
- package/dist/projection/card/Card.d.ts +7 -1
- package/dist/projection/card/Card.d.ts.map +1 -1
- package/dist/projection/card/Card.js +25 -133
- package/dist/projection/card/Card.js.map +1 -1
- package/dist/projection/card/Descriptor.d.ts +2 -2
- package/dist/projection/card/Descriptor.d.ts.map +1 -1
- package/dist/projection/card/admission.d.ts +12 -11
- package/dist/projection/card/admission.d.ts.map +1 -1
- package/dist/projection/card/admission.js +13 -11
- package/dist/projection/card/admission.js.map +1 -1
- package/dist/projection/card/diff.d.ts +7 -1
- package/dist/projection/card/diff.d.ts.map +1 -1
- package/dist/projection/card/diff.js +111 -1
- package/dist/projection/card/diff.js.map +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts +1 -1
- package/dist/validator/FieldDeclarationValidator.d.ts.map +1 -1
- package/dist/validator/FieldDeclarationValidator.js +4 -4
- package/dist/validator/FieldDeclarationValidator.js.map +1 -1
- package/dist/validator/FieldValueValidator.d.ts +6 -1
- package/dist/validator/FieldValueValidator.d.ts.map +1 -1
- package/dist/validator/FieldValueValidator.js +36 -7
- package/dist/validator/FieldValueValidator.js.map +1 -1
- package/dist/validator/InputValidator.d.ts +1 -1
- package/dist/validator/InputValidator.d.ts.map +1 -1
- package/dist/validator/InputValidator.js +8 -18
- package/dist/validator/InputValidator.js.map +1 -1
- package/dist/vocabulary/preset/created.d.ts +0 -5
- package/dist/vocabulary/preset/created.d.ts.map +1 -1
- package/dist/vocabulary/preset/created.js +0 -5
- package/dist/vocabulary/preset/created.js.map +1 -1
- package/dist/vocabulary/preset/email.d.ts +0 -5
- package/dist/vocabulary/preset/email.d.ts.map +1 -1
- package/dist/vocabulary/preset/email.js +0 -5
- package/dist/vocabulary/preset/email.js.map +1 -1
- package/dist/vocabulary/preset/updated.d.ts +0 -5
- package/dist/vocabulary/preset/updated.d.ts.map +1 -1
- package/dist/vocabulary/preset/updated.js +0 -5
- package/dist/vocabulary/preset/updated.js.map +1 -1
- package/dist/vocabulary/preset/url.d.ts +0 -5
- package/dist/vocabulary/preset/url.d.ts.map +1 -1
- package/dist/vocabulary/preset/url.js +0 -5
- package/dist/vocabulary/preset/url.js.map +1 -1
- package/dist/vocabulary/primitive/bool.d.ts +0 -5
- package/dist/vocabulary/primitive/bool.d.ts.map +1 -1
- package/dist/vocabulary/primitive/bool.js +0 -5
- package/dist/vocabulary/primitive/bool.js.map +1 -1
- package/dist/vocabulary/primitive/date.d.ts +3 -3
- package/dist/vocabulary/primitive/date.js +3 -3
- package/dist/vocabulary/primitive/json.js +3 -3
- package/dist/vocabulary/primitive/list.d.ts +3 -3
- package/dist/vocabulary/primitive/list.js +3 -3
- package/dist/vocabulary/primitive/many.d.ts +3 -3
- package/dist/vocabulary/primitive/many.js +3 -3
- package/dist/vocabulary/primitive/number.d.ts +0 -5
- package/dist/vocabulary/primitive/number.d.ts.map +1 -1
- package/dist/vocabulary/primitive/number.js +0 -5
- package/dist/vocabulary/primitive/number.js.map +1 -1
- package/dist/vocabulary/primitive/oneOf.js +3 -3
- package/dist/vocabulary/primitive/primary.js +3 -3
- package/dist/vocabulary/primitive/ref.d.ts +0 -5
- package/dist/vocabulary/primitive/ref.d.ts.map +1 -1
- package/dist/vocabulary/primitive/ref.js +0 -5
- package/dist/vocabulary/primitive/ref.js.map +1 -1
- package/dist/vocabulary/primitive/text.d.ts +0 -5
- package/dist/vocabulary/primitive/text.d.ts.map +1 -1
- package/dist/vocabulary/primitive/text.js +0 -5
- package/dist/vocabulary/primitive/text.js.map +1 -1
- package/dist/vocabulary/rule/immutable.d.ts +0 -5
- package/dist/vocabulary/rule/immutable.d.ts.map +1 -1
- package/dist/vocabulary/rule/immutable.js +0 -5
- package/dist/vocabulary/rule/immutable.js.map +1 -1
- package/dist/vocabulary/rule/indexed.d.ts +0 -5
- package/dist/vocabulary/rule/indexed.d.ts.map +1 -1
- package/dist/vocabulary/rule/indexed.js +0 -5
- package/dist/vocabulary/rule/indexed.js.map +1 -1
- package/dist/vocabulary/rule/nullable.d.ts +3 -3
- package/dist/vocabulary/rule/nullable.js +3 -3
- package/dist/vocabulary/rule/optional.d.ts +3 -3
- package/dist/vocabulary/rule/optional.js +3 -3
- package/dist/vocabulary/rule/readOnly.d.ts +3 -3
- package/dist/vocabulary/rule/readOnly.js +4 -4
- package/dist/vocabulary/rule/readOnly.js.map +1 -1
- package/dist/vocabulary/rule/unique.d.ts +0 -5
- package/dist/vocabulary/rule/unique.d.ts.map +1 -1
- package/dist/vocabulary/rule/unique.js +0 -5
- package/dist/vocabulary/rule/unique.js.map +1 -1
- package/dist/vocabulary/rule/writeOnly.d.ts +2 -2
- package/dist/vocabulary/rule/writeOnly.js +3 -3
- package/dist/vocabulary/rule/writeOnly.js.map +1 -1
- package/dist/vocabulary/vocabulary.d.ts +2 -2
- package/dist/vocabulary/vocabulary.js +5 -5
- package/package.json +1 -1
- package/src/Schema.ts +14 -20
- package/src/SchemaDefinition.ts +74 -48
- package/src/SchemaView.ts +1 -1
- package/src/axis/Axis.ts +1 -1
- package/src/axis/boundary/Boundaries.ts +5 -0
- package/src/axis/boundary/Boundary.ts +7 -9
- package/src/axis/boundary/BoundaryAxis.ts +2 -2
- package/src/axis/lifecycle/Lifecycle.ts +9 -4
- package/src/axis/lifecycle/LifecycleAxis.ts +7 -7
- package/src/axis/lifecycle/apply.ts +10 -9
- package/src/axis/role/Role.ts +14 -5
- package/src/axis/role/RoleAxis.ts +10 -10
- package/src/axis/shape/Shape.ts +61 -37
- package/src/entity/EntityAdapterSet.ts +2 -2
- package/src/entity.ts +2 -2
- package/src/field/Field.ts +2 -1
- package/src/field/FieldSet.ts +6 -20
- package/src/index.ts +2 -2
- package/src/lib/validation.ts +25 -0
- package/src/projection/Cases.ts +13 -8
- package/src/projection/Visibility.ts +1 -1
- package/src/projection/card/Bundle.ts +1 -4
- package/src/projection/card/Card.ts +26 -159
- package/src/projection/card/Descriptor.ts +2 -10
- package/src/projection/card/admission.ts +14 -12
- package/src/projection/card/diff.ts +142 -1
- package/src/validator/FieldDeclarationValidator.ts +5 -5
- package/src/validator/FieldValueValidator.ts +45 -12
- package/src/validator/InputValidator.ts +8 -18
- package/src/vocabulary/preset/created.ts +0 -5
- package/src/vocabulary/preset/email.ts +0 -5
- package/src/vocabulary/preset/updated.ts +0 -5
- package/src/vocabulary/preset/url.ts +0 -5
- package/src/vocabulary/primitive/bool.ts +0 -5
- package/src/vocabulary/primitive/date.ts +3 -3
- package/src/vocabulary/primitive/json.ts +3 -3
- package/src/vocabulary/primitive/list.ts +3 -3
- package/src/vocabulary/primitive/many.ts +3 -3
- package/src/vocabulary/primitive/number.ts +0 -5
- package/src/vocabulary/primitive/oneOf.ts +3 -3
- package/src/vocabulary/primitive/primary.ts +3 -3
- package/src/vocabulary/primitive/ref.ts +0 -5
- package/src/vocabulary/primitive/text.ts +0 -5
- package/src/vocabulary/rule/immutable.ts +0 -5
- package/src/vocabulary/rule/indexed.ts +0 -5
- package/src/vocabulary/rule/nullable.ts +3 -3
- package/src/vocabulary/rule/optional.ts +3 -3
- package/src/vocabulary/rule/readOnly.ts +4 -4
- package/src/vocabulary/rule/unique.ts +0 -5
- package/src/vocabulary/rule/writeOnly.ts +3 -3
- package/src/vocabulary/vocabulary.ts +5 -5
- package/dist/validation.d.ts +0 -17
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -2
- package/dist/validation.js.map +0 -1
- package/src/validation.ts +0 -10
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { EXTENSION_AXES,
|
|
2
|
-
import { dequal } from 'dequal';
|
|
1
|
+
import { EXTENSION_AXES, type Resolver } from '../../axis/Axis.js';
|
|
3
2
|
import { clean, isObject } from '../../lib/utils.js';
|
|
4
3
|
import { Field, type Fields } from '../../field/Field.js';
|
|
5
|
-
import {
|
|
4
|
+
import { SchemaConstraints } from '../../SchemaDefinition.js';
|
|
6
5
|
import { InputValidator } from '../../validator/InputValidator.js';
|
|
7
6
|
import { Schema, type SchemaConstructor } from '../../Schema.js';
|
|
8
7
|
import type { Values, SchemaView } from '../../SchemaView.js';
|
|
@@ -13,7 +12,7 @@ import type {
|
|
|
13
12
|
FieldExtension,
|
|
14
13
|
SchemaDescriptor,
|
|
15
14
|
} from './Descriptor.js';
|
|
16
|
-
import
|
|
15
|
+
import { compare, type Diff, type DiffOptions } from './diff.js';
|
|
17
16
|
|
|
18
17
|
type FieldsOf<T> = { [K in keyof T]-?: Field<T[K]> };
|
|
19
18
|
|
|
@@ -42,7 +41,7 @@ export class Card<T = Values<Fields>> {
|
|
|
42
41
|
'x-fougere-version': 1,
|
|
43
42
|
'x-fougere-vendor': 'fougere',
|
|
44
43
|
};
|
|
45
|
-
const title = name ??
|
|
44
|
+
const title = name ?? Card.titleOf(schema);
|
|
46
45
|
if (title) descriptor.title = title;
|
|
47
46
|
if (required.length) descriptor.required = required;
|
|
48
47
|
|
|
@@ -55,6 +54,14 @@ export class Card<T = Values<Fields>> {
|
|
|
55
54
|
return new Card<T>(descriptor);
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A derivation answers the name it was cut from — `Post.pick('title')` is `Post`, since
|
|
59
|
+
* a derived class carries `Schema`.
|
|
60
|
+
*/
|
|
61
|
+
static titleOf(schema: SchemaView): string {
|
|
62
|
+
return schema.derivation?.sourceName ?? schema.name;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
get origin(): DerivedFrom | undefined {
|
|
59
66
|
return this.descriptor['x-fougere-derived'];
|
|
60
67
|
}
|
|
@@ -91,79 +98,22 @@ export class Card<T = Values<Fields>> {
|
|
|
91
98
|
for (const group of property['x-fougere']?.role?.unique ?? [])
|
|
92
99
|
if (group.length > 1) groups.push([...group]);
|
|
93
100
|
}
|
|
94
|
-
const schema = Schema.of({ fields, constraints:
|
|
101
|
+
const schema = Schema.of({ fields, constraints: SchemaConstraints.of(groups) });
|
|
95
102
|
const title = name ?? descriptor.title;
|
|
96
103
|
if (title)
|
|
97
104
|
Object.defineProperty(schema, 'name', { value: title, configurable: true });
|
|
98
105
|
return schema as unknown as SchemaConstructor<FieldsOf<T>>;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/** What changed between two descriptions of the same entity — see {@link compare}. */
|
|
101
109
|
diff(other: Card, options: DiffOptions = {}): Diff {
|
|
102
|
-
|
|
103
|
-
const renamed = options.renamed ?? {};
|
|
104
|
-
const before = this.descriptor.properties ?? {};
|
|
105
|
-
const after = other.descriptor.properties ?? {};
|
|
106
|
-
const requiredBefore = new Set(this.descriptor.required ?? []);
|
|
107
|
-
const requiredAfter = new Set(other.descriptor.required ?? []);
|
|
108
|
-
|
|
109
|
-
// Apply a declared rename first so subsequent differences use the new field name.
|
|
110
|
-
const nameAfter = (field: string): string => renamed[field] ?? field;
|
|
111
|
-
const removed: string[] = [];
|
|
112
|
-
for (const [field, descriptor] of Object.entries(before)) {
|
|
113
|
-
const now = nameAfter(field);
|
|
114
|
-
const target = after[now];
|
|
115
|
-
if (target === undefined) {
|
|
116
|
-
removed.push(field);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (now !== field)
|
|
121
|
-
changes.push({ kind: 'renamed', from: field, to: now, field: target });
|
|
122
|
-
|
|
123
|
-
const wasType = typesOf(descriptor);
|
|
124
|
-
const isType = typesOf(target);
|
|
125
|
-
if (!dequal(wasType, isType))
|
|
126
|
-
changes.push({ kind: 'retyped', field: now, from: wasType, to: isType });
|
|
127
|
-
else if (!dequal(boundsOf(descriptor), boundsOf(target))) {
|
|
128
|
-
changes.push({ kind: 'reshaped', field: now, from: descriptor, to: target });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const wasRequired = requiredBefore.has(field);
|
|
132
|
-
const isRequired = requiredAfter.has(now);
|
|
133
|
-
if (wasRequired !== isRequired) {
|
|
134
|
-
changes.push({ kind: 'required', field: now, from: wasRequired, to: isRequired });
|
|
135
|
-
}
|
|
136
|
-
changes.push(...restated(now, descriptor['x-fougere'], target['x-fougere']));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const claimed = new Set(Object.values(renamed));
|
|
140
|
-
const added = Object.keys(after).filter(
|
|
141
|
-
(field) => !(field in before) && !claimed.has(field),
|
|
142
|
-
);
|
|
143
|
-
for (const field of removed) {
|
|
144
|
-
changes.push({
|
|
145
|
-
kind: 'removed',
|
|
146
|
-
field,
|
|
147
|
-
from: before[field],
|
|
148
|
-
required: requiredBefore.has(field),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
for (const field of added) {
|
|
152
|
-
changes.push({
|
|
153
|
-
kind: 'added',
|
|
154
|
-
field,
|
|
155
|
-
to: after[field],
|
|
156
|
-
required: requiredAfter.has(field),
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return { changes, ambiguous: candidates(removed, added, before, after) };
|
|
110
|
+
return compare(this.descriptor, other.descriptor, options);
|
|
161
111
|
}
|
|
162
112
|
}
|
|
163
113
|
|
|
164
114
|
/**
|
|
165
|
-
*
|
|
166
|
-
* FR :
|
|
115
|
+
* Each axis writes its own wire form under `x-fougere`; `shape` stays outside, plain.
|
|
116
|
+
* FR : chaque axe écrit sa forme de fil sous `x-fougere` ; `shape` reste dehors, nu.
|
|
167
117
|
* `ref(User)` → `{ role: { relation: { to: 'user', kind: 'one' } } }`
|
|
168
118
|
*/
|
|
169
119
|
function describeExtension(field: Field, key: string): FieldExtension | undefined {
|
|
@@ -178,11 +128,6 @@ function describeExtension(field: Field, key: string): FieldExtension | undefine
|
|
|
178
128
|
return Object.keys(extension).length ? (extension as FieldExtension) : undefined;
|
|
179
129
|
}
|
|
180
130
|
|
|
181
|
-
/**
|
|
182
|
-
* So the shape stays plain JSON Schema, and what is ours sits under one key.
|
|
183
|
-
* FR : pour que la forme reste du JSON Schema, ce qui est à nous tenant sous une clé.
|
|
184
|
-
* `text({ max: 200 })` → `{ type: 'string', maxLength: 200 }`
|
|
185
|
-
*/
|
|
186
131
|
function describeField(field: Field, key: string): FieldDescriptor {
|
|
187
132
|
// The shape IS JSON Schema, so it lands whole rather than key by key — which is what let
|
|
188
133
|
// it be copied under a computed key into a type that names its own.
|
|
@@ -194,8 +139,8 @@ function describeField(field: Field, key: string): FieldDescriptor {
|
|
|
194
139
|
}
|
|
195
140
|
|
|
196
141
|
/**
|
|
197
|
-
*
|
|
198
|
-
* FR :
|
|
142
|
+
* What a derived card says of its root, and of every field the cut kept or dropped.
|
|
143
|
+
* FR : ce qu'une carte dérivée dit de sa racine, et de chaque champ gardé ou perdu.
|
|
199
144
|
* `Post.pick('title')` → `{ from: 'Post', nameOf: { title: 'title' } }`
|
|
200
145
|
*/
|
|
201
146
|
function originOf(schema: SchemaView): DerivedFrom | undefined {
|
|
@@ -208,9 +153,9 @@ function originOf(schema: SchemaView): DerivedFrom | undefined {
|
|
|
208
153
|
}
|
|
209
154
|
|
|
210
155
|
/**
|
|
211
|
-
*
|
|
212
|
-
* FR :
|
|
213
|
-
* `{ type: 'string', maxLength: 200 }` → the same shape
|
|
156
|
+
* Rebuilds the shape a card carries, refusing a type the standard does not name.
|
|
157
|
+
* FR : reconstruit la forme que porte une carte, refusant un type hors du standard.
|
|
158
|
+
* `{ type: 'string', maxLength: 200 }` → the same shape `text({ max: 200 })` states
|
|
214
159
|
*/
|
|
215
160
|
function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined {
|
|
216
161
|
const types = Array.isArray(property.type) ? property.type : property.type ? [property.type] : [];
|
|
@@ -225,8 +170,8 @@ function reconstructShape(property: FieldDescriptor): Field['shape'] | undefined
|
|
|
225
170
|
}
|
|
226
171
|
|
|
227
172
|
/**
|
|
228
|
-
*
|
|
229
|
-
* FR :
|
|
173
|
+
* Rebuilds a field, each axis reading back what it wrote — the dual of `describeField`.
|
|
174
|
+
* FR : reconstruit un champ, chaque axe relisant ce qu'il a écrit — le dual de `describeField`.
|
|
230
175
|
* `{ 'x-fougere': { lifecycle: { create: 'now' } } }` → a field stamped at create
|
|
231
176
|
*/
|
|
232
177
|
function reconstructField(
|
|
@@ -259,86 +204,8 @@ function reconstructField(
|
|
|
259
204
|
}
|
|
260
205
|
|
|
261
206
|
/**
|
|
262
|
-
*
|
|
263
|
-
* FR :
|
|
264
|
-
* `{ kind: 'restated', field: 'body', axis: 'boundary', … }`
|
|
265
|
-
*/
|
|
266
|
-
function restated(
|
|
267
|
-
field: string,
|
|
268
|
-
before: FieldExtension | undefined,
|
|
269
|
-
after: FieldExtension | undefined,
|
|
270
|
-
): Change[] {
|
|
271
|
-
return EXTENSION_SLOTS.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
|
|
272
|
-
(axis) =>
|
|
273
|
-
({
|
|
274
|
-
kind: 'restated',
|
|
275
|
-
field,
|
|
276
|
-
axis,
|
|
277
|
-
from: before?.[axis],
|
|
278
|
-
to: after?.[axis],
|
|
279
|
-
}) as Change,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* So two shapes are compared without what is not shape getting in the way.
|
|
285
|
-
* FR : pour que deux formes se comparent sans que le reste s'en mêle.
|
|
286
|
-
* `{ type: 'string', description: 'x' }` → `{ type: 'string' }`
|
|
287
|
-
*/
|
|
288
|
-
function shapeOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
289
|
-
const { 'x-fougere': _extension, description: _description, ...shape } = descriptor;
|
|
290
|
-
return shape as Record<string, unknown>;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* So `['string','null']` and `['null','string']` are the same type, not a change.
|
|
295
|
-
* FR : pour que `['string','null']` et `['null','string']` soient un même type.
|
|
296
|
-
* `typesOf({ type: ['null', 'string'] })` → `['null', 'string']`
|
|
297
|
-
*/
|
|
298
|
-
function typesOf(descriptor: FieldDescriptor): TypeSet {
|
|
299
|
-
const type = descriptor.type;
|
|
300
|
-
if (type === undefined) return [];
|
|
301
|
-
return (Array.isArray(type) ? [...type] : [type]).sort();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* So a bound that moved is a `reshaped`, told apart from a type that changed.
|
|
306
|
-
* FR : pour qu'une borne déplacée soit un `reshaped`, distinct d'un type changé.
|
|
307
|
-
* `maxLength: 200` → `maxLength: 100` → one `reshaped`, never a `retyped`
|
|
308
|
-
*/
|
|
309
|
-
function boundsOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
310
|
-
const { type: _type, ...rest } = shapeOf(descriptor);
|
|
311
|
-
return rest;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* So a removal plus an addition of the same shape is a question, never a guess.
|
|
316
|
-
* FR : pour qu'une suppression plus un ajout de même forme soit une question, pas un pari.
|
|
317
|
-
* `body` gone, `content` appeared → `ambiguous: [{ removed: 'body', added: 'content' }]`
|
|
318
|
-
*/
|
|
319
|
-
function candidates(
|
|
320
|
-
removed: string[],
|
|
321
|
-
added: string[],
|
|
322
|
-
before: Record<string, FieldDescriptor>,
|
|
323
|
-
after: Record<string, FieldDescriptor>,
|
|
324
|
-
): RenameCandidate[] {
|
|
325
|
-
const found: RenameCandidate[] = [];
|
|
326
|
-
for (const gone of removed) {
|
|
327
|
-
for (const appeared of added) {
|
|
328
|
-
if (dequal(shapeOf(before[gone]), shapeOf(after[appeared])))
|
|
329
|
-
found.push({ removed: gone, added: appeared });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const was = Object.keys(before);
|
|
333
|
-
const now = Object.keys(after);
|
|
334
|
-
const apart = ({ removed: gone, added: appeared }: RenameCandidate): number =>
|
|
335
|
-
Math.abs(now.indexOf(appeared) - was.indexOf(gone));
|
|
336
|
-
return found.sort((a, b) => apart(a) - apart(b));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* So a group spanning several fields reaches a reader that only ever sees one field.
|
|
341
|
-
* FR : pour qu'un groupe couvrant plusieurs champs atteigne un lecteur qui n'en voit qu'un.
|
|
207
|
+
* Writes a group onto EVERY member — a wire reader sees one field at a time.
|
|
208
|
+
* FR : écrit un groupe sur CHAQUE membre — un lecteur du fil ne voit qu'un champ.
|
|
342
209
|
* `carryGroup(properties.listId, ['listId', 'docId'])` → the pair lands under its `role`
|
|
343
210
|
*/
|
|
344
211
|
function carryGroup(property: FieldDescriptor | undefined, group: readonly string[]): void {
|
|
@@ -2,24 +2,16 @@ import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
|
|
|
2
2
|
import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
|
|
3
3
|
import type { Relation } from '../../axis/role/Relation.js';
|
|
4
4
|
import type { RoleRules } from '../../axis/role/Role.js';
|
|
5
|
+
import type { JSONSchema7TypeName } from 'json-schema';
|
|
5
6
|
import type { Shape } from '../../axis/shape/Shape.js';
|
|
6
7
|
|
|
7
|
-
type JsonSchemaType =
|
|
8
|
-
| 'string'
|
|
9
|
-
| 'number'
|
|
10
|
-
| 'integer'
|
|
11
|
-
| 'boolean'
|
|
12
|
-
| 'object'
|
|
13
|
-
| 'array'
|
|
14
|
-
| 'null';
|
|
15
|
-
|
|
16
8
|
type ShapeKeywords = UnionToIntersection<KeywordsOf<Shape>>;
|
|
17
9
|
|
|
18
10
|
type KeywordsOf<S> = S extends unknown ? Partial<Omit<S, 'type' | 'items' | 'properties'>> : never;
|
|
19
11
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
20
12
|
|
|
21
13
|
export type FieldDescriptor = ShapeKeywords & {
|
|
22
|
-
type?:
|
|
14
|
+
type?: JSONSchema7TypeName | JSONSchema7TypeName[];
|
|
23
15
|
items?: FieldDescriptor;
|
|
24
16
|
properties?: Record<string, FieldDescriptor>;
|
|
25
17
|
description?: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ValidationError } from '../../validation.js';
|
|
1
|
+
import type { ValidationError } from '../../lib/validation.js';
|
|
2
2
|
import { Shapes } from '../../axis/shape/Shape.js';
|
|
3
|
+
import { dotted } from '../../lib/validation.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* FR :
|
|
6
|
+
* Throws with what is wrong AND what to write: a card's author is in another process.
|
|
7
|
+
* FR : lève avec ce qui cloche ET quoi écrire : l'auteur d'une carte est ailleurs.
|
|
7
8
|
* `refuse('role.relation.kind is "maybe"', 'Expected one of one, many.')`
|
|
8
9
|
*/
|
|
9
10
|
export function refuse(what: string, fix: string): never {
|
|
@@ -11,9 +12,10 @@ export function refuse(what: string, fix: string): never {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Runs a card's axis through the validator a hand-written field goes through, so there is
|
|
16
|
+
* one judge and not two.
|
|
17
|
+
* FR : passe l'axe d'une carte par le juge d'un champ écrit à la main : un juge, pas deux.
|
|
18
|
+
* `{ update: 'maybe' }` → `lifecycle is malformed — lifecycle.update: Expected 'now' or 'forbidden'`
|
|
17
19
|
*/
|
|
18
20
|
export function admitWire(
|
|
19
21
|
validator: (value: unknown, errors: ValidationError[]) => void,
|
|
@@ -24,15 +26,15 @@ export function admitWire(
|
|
|
24
26
|
validator(value, errors);
|
|
25
27
|
if (errors.length) {
|
|
26
28
|
refuse(
|
|
27
|
-
`${slot} is malformed — ${errors.map((e) => `${e.path}: ${e.message}`).join('; ')}`,
|
|
29
|
+
`${slot} is malformed — ${errors.map((e) => `${dotted(e.path)}: ${e.message}`).join('; ')}`,
|
|
28
30
|
'A card states an axis the way a declaration does.',
|
|
29
31
|
);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
* FR :
|
|
36
|
+
* Checks a token against the list that declares it, never against a copy written here.
|
|
37
|
+
* FR : vérifie un mot contre la liste qui le déclare, jamais contre une copie locale.
|
|
36
38
|
* `oneOfTokens('many', RELATION_KINDS)` → `true`
|
|
37
39
|
*/
|
|
38
40
|
export const oneOfTokens = <T extends readonly string[]>(
|
|
@@ -41,9 +43,9 @@ export const oneOfTokens = <T extends readonly string[]>(
|
|
|
41
43
|
): value is T[number] => typeof value === 'string' && (tokens as readonly string[]).includes(value);
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
|
-
*
|
|
45
|
-
* FR :
|
|
46
|
-
* `{ type: 'string', pattern: '(' }` → Field 'code' states
|
|
46
|
+
* Compiles every `pattern` at the door, so a bad one is refused here and not at the first row.
|
|
47
|
+
* FR : compile chaque `pattern` à la porte : un mauvais est refusé ici, pas au premier row.
|
|
48
|
+
* `{ type: 'string', pattern: '(' }` → `Field 'code' states pattern: "(", which is not a regular expression`
|
|
47
49
|
*/
|
|
48
50
|
export function admitPatterns(shape: unknown, subject: string): void {
|
|
49
51
|
for (const pattern of Shapes.patterns(shape)) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { BoundaryRef } from '../../axis/boundary/Boundary.js';
|
|
2
2
|
import type { LifecycleRules } from '../../axis/lifecycle/Lifecycle.js';
|
|
3
|
-
import
|
|
3
|
+
import { dequal } from 'dequal';
|
|
4
|
+
import { EXTENSION_SLOTS } from '../../axis/Axis.js';
|
|
5
|
+
import type { FieldDescriptor, FieldExtension, RoleDescriptor, SchemaDescriptor } from './Descriptor.js';
|
|
4
6
|
|
|
5
7
|
/** One named difference, at one place. Each kind exists because a reader asks for it. */
|
|
6
8
|
export type Change =
|
|
@@ -54,3 +56,142 @@ export interface SetDiffOptions {
|
|
|
54
56
|
/** Declared renames, per entity: `{ post: { body: 'content' } }`. */
|
|
55
57
|
renamed?: Record<string, Record<string, string>>;
|
|
56
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What changed between two descriptions of the same entity, read from the wire form alone.
|
|
62
|
+
* FR : ce qui a changé entre deux descriptions d'une même entité, lu du seul format du fil.
|
|
63
|
+
* `compare(v1, v2, { renamed: { body: 'content' } })` → one `renamed`, no `ambiguous`
|
|
64
|
+
*/
|
|
65
|
+
export function compare(
|
|
66
|
+
was: SchemaDescriptor,
|
|
67
|
+
is: SchemaDescriptor,
|
|
68
|
+
options: DiffOptions = {},
|
|
69
|
+
): Diff {
|
|
70
|
+
const changes: Change[] = [];
|
|
71
|
+
const renamed = options.renamed ?? {};
|
|
72
|
+
const before = was.properties ?? {};
|
|
73
|
+
const after = is.properties ?? {};
|
|
74
|
+
const requiredBefore = new Set(was.required ?? []);
|
|
75
|
+
const requiredAfter = new Set(is.required ?? []);
|
|
76
|
+
|
|
77
|
+
// Apply a declared rename first so subsequent differences use the new field name.
|
|
78
|
+
const nameAfter = (field: string): string => renamed[field] ?? field;
|
|
79
|
+
const removed: string[] = [];
|
|
80
|
+
for (const [field, descriptor] of Object.entries(before)) {
|
|
81
|
+
const now = nameAfter(field);
|
|
82
|
+
const target = after[now];
|
|
83
|
+
if (target === undefined) {
|
|
84
|
+
removed.push(field);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (now !== field)
|
|
89
|
+
changes.push({ kind: 'renamed', from: field, to: now, field: target });
|
|
90
|
+
|
|
91
|
+
const wasType = typesOf(descriptor);
|
|
92
|
+
const isType = typesOf(target);
|
|
93
|
+
if (!dequal(wasType, isType))
|
|
94
|
+
changes.push({ kind: 'retyped', field: now, from: wasType, to: isType });
|
|
95
|
+
else if (!dequal(boundsOf(descriptor), boundsOf(target))) {
|
|
96
|
+
changes.push({ kind: 'reshaped', field: now, from: descriptor, to: target });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const wasRequired = requiredBefore.has(field);
|
|
100
|
+
const isRequired = requiredAfter.has(now);
|
|
101
|
+
if (wasRequired !== isRequired) {
|
|
102
|
+
changes.push({ kind: 'required', field: now, from: wasRequired, to: isRequired });
|
|
103
|
+
}
|
|
104
|
+
changes.push(...restated(now, descriptor['x-fougere'], target['x-fougere']));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const claimed = new Set(Object.values(renamed));
|
|
108
|
+
const added = Object.keys(after).filter(
|
|
109
|
+
(field) => !(field in before) && !claimed.has(field),
|
|
110
|
+
);
|
|
111
|
+
for (const field of removed) {
|
|
112
|
+
changes.push({
|
|
113
|
+
kind: 'removed',
|
|
114
|
+
field,
|
|
115
|
+
from: before[field],
|
|
116
|
+
required: requiredBefore.has(field),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const field of added) {
|
|
120
|
+
changes.push({
|
|
121
|
+
kind: 'added',
|
|
122
|
+
field,
|
|
123
|
+
to: after[field],
|
|
124
|
+
required: requiredAfter.has(field),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { changes, ambiguous: candidates(removed, added, before, after) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function restated(
|
|
132
|
+
field: string,
|
|
133
|
+
before: FieldExtension | undefined,
|
|
134
|
+
after: FieldExtension | undefined,
|
|
135
|
+
): Change[] {
|
|
136
|
+
return EXTENSION_SLOTS.filter((axis) => !dequal(before?.[axis], after?.[axis])).map(
|
|
137
|
+
(axis) =>
|
|
138
|
+
({
|
|
139
|
+
kind: 'restated',
|
|
140
|
+
field,
|
|
141
|
+
axis,
|
|
142
|
+
from: before?.[axis],
|
|
143
|
+
to: after?.[axis],
|
|
144
|
+
}) as Change,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function shapeOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
149
|
+
const { 'x-fougere': _extension, description: _description, ...shape } = descriptor;
|
|
150
|
+
return shape as Record<string, unknown>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sorted, so `['string','null']` and `['null','string']` compare equal, not as a change.
|
|
155
|
+
* FR : trié, pour que `['string','null']` et `['null','string']` soient égaux.
|
|
156
|
+
* `typesOf({ type: ['null', 'string'] })` → `['null', 'string']`
|
|
157
|
+
*/
|
|
158
|
+
function typesOf(descriptor: FieldDescriptor): TypeSet {
|
|
159
|
+
const type = descriptor.type;
|
|
160
|
+
if (type === undefined) return [];
|
|
161
|
+
return (Array.isArray(type) ? [...type] : [type]).sort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The shape without its type, which is what tells a `reshaped` from a `retyped`.
|
|
166
|
+
* FR : la forme sans son type, ce qui distingue un `reshaped` d'un `retyped`.
|
|
167
|
+
* `maxLength: 200` → `maxLength: 100` → one `reshaped`, never a `retyped`
|
|
168
|
+
*/
|
|
169
|
+
function boundsOf(descriptor: FieldDescriptor): Record<string, unknown> {
|
|
170
|
+
const { type: _type, ...rest } = shapeOf(descriptor);
|
|
171
|
+
return rest;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A removal plus an addition of one shape is REPORTED, never resolved — only `renamed` decides.
|
|
176
|
+
* FR : une suppression plus un ajout de même forme est RAPPORTÉE, jamais résolue.
|
|
177
|
+
* `body` gone, `content` appeared → `ambiguous: [{ removed: 'body', added: 'content' }]`
|
|
178
|
+
*/
|
|
179
|
+
function candidates(
|
|
180
|
+
removed: string[],
|
|
181
|
+
added: string[],
|
|
182
|
+
before: Record<string, FieldDescriptor>,
|
|
183
|
+
after: Record<string, FieldDescriptor>,
|
|
184
|
+
): RenameCandidate[] {
|
|
185
|
+
const found: RenameCandidate[] = [];
|
|
186
|
+
for (const gone of removed) {
|
|
187
|
+
for (const appeared of added) {
|
|
188
|
+
if (dequal(shapeOf(before[gone]), shapeOf(after[appeared])))
|
|
189
|
+
found.push({ removed: gone, added: appeared });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const was = Object.keys(before);
|
|
193
|
+
const now = Object.keys(after);
|
|
194
|
+
const apart = ({ removed: gone, added: appeared }: RenameCandidate): number =>
|
|
195
|
+
Math.abs(now.indexOf(appeared) - was.indexOf(gone));
|
|
196
|
+
return found.sort((a, b) => apart(a) - apart(b));
|
|
197
|
+
}
|
|
@@ -2,7 +2,7 @@ import { EXTENSION_AXES } from '../axis/Axis.js';
|
|
|
2
2
|
import { Shapes } from '../axis/shape/Shape.js';
|
|
3
3
|
import type { Field } from '../field/Field.js';
|
|
4
4
|
import { isObject, shown } from '../lib/utils.js';
|
|
5
|
-
import type { ValidationError, ValidationResult } from '../validation.js';
|
|
5
|
+
import type { ValidationError, ValidationResult } from '../lib/validation.js';
|
|
6
6
|
|
|
7
7
|
export class FieldDeclarationValidator {
|
|
8
8
|
private constructor(private readonly declaration: unknown) {}
|
|
@@ -19,7 +19,7 @@ export class FieldDeclarationValidator {
|
|
|
19
19
|
success: false,
|
|
20
20
|
errors: [
|
|
21
21
|
{
|
|
22
|
-
path:
|
|
22
|
+
path: [],
|
|
23
23
|
message: `Expected an object — got ${shown(declaration)}`,
|
|
24
24
|
},
|
|
25
25
|
],
|
|
@@ -30,7 +30,7 @@ export class FieldDeclarationValidator {
|
|
|
30
30
|
|
|
31
31
|
if (!Shapes.is(declaration.shape)) {
|
|
32
32
|
errors.push({
|
|
33
|
-
path: 'shape',
|
|
33
|
+
path: ['shape'],
|
|
34
34
|
message: `Every field states a shape — got ${shown(declaration.shape)}`,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -43,14 +43,14 @@ export class FieldDeclarationValidator {
|
|
|
43
43
|
if (declaration.meta !== undefined) {
|
|
44
44
|
if (!isObject(declaration.meta)) {
|
|
45
45
|
errors.push({
|
|
46
|
-
path: 'meta',
|
|
46
|
+
path: ['meta'],
|
|
47
47
|
message: `Expected an object — got ${shown(declaration.meta)}`,
|
|
48
48
|
});
|
|
49
49
|
} else if (
|
|
50
50
|
declaration.meta.description !== undefined &&
|
|
51
51
|
typeof declaration.meta.description !== 'string'
|
|
52
52
|
) {
|
|
53
|
-
errors.push({ path: 'meta
|
|
53
|
+
errors.push({ path: ['meta', 'description'], message: 'Expected a string' });
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Validator, format as engineFormats } from '@cfworker/json-schema';
|
|
2
|
+
import { Boundary } from '../axis/boundary/Boundary.js';
|
|
2
3
|
import { Formats, type FormatPredicate } from '../axis/shape/Formats.js';
|
|
3
4
|
import { Shapes, type Shape } from '../axis/shape/Shape.js';
|
|
4
5
|
import type { Field } from '../field/Field.js';
|
|
5
|
-
import type { Checked } from '../validation.js';
|
|
6
|
+
import type { Checked } from '../lib/validation.js';
|
|
7
|
+
import type { OutputUnit } from '@cfworker/json-schema';
|
|
6
8
|
|
|
7
9
|
interface ShapePlan {
|
|
8
10
|
validator: Validator;
|
|
@@ -22,33 +24,38 @@ export class FieldValueValidator {
|
|
|
22
24
|
|
|
23
25
|
validate(value: unknown): Checked {
|
|
24
26
|
const shape = this.field.shape;
|
|
27
|
+
const type = Shapes.typeOf(shape);
|
|
25
28
|
const base = Shapes.of(shape).base;
|
|
26
29
|
if (value !== null) {
|
|
27
30
|
if (base?.type === 'object' && !base.properties) return { value };
|
|
28
|
-
if (
|
|
29
|
-
base?.type === 'string' &&
|
|
30
|
-
base.format === 'date-time' &&
|
|
31
|
-
value instanceof Date
|
|
32
|
-
) {
|
|
31
|
+
if (type === 'date' && value instanceof Date) {
|
|
33
32
|
return Number.isNaN(value.getTime()) ? { error: 'Invalid date' } : { value };
|
|
34
33
|
}
|
|
35
|
-
if (
|
|
36
|
-
(base?.type === 'number' || base?.type === 'integer') &&
|
|
37
|
-
typeof value === 'number' &&
|
|
38
|
-
Number.isNaN(value)
|
|
39
|
-
) {
|
|
34
|
+
if ((type === 'number' || type === 'integer') && typeof value === 'number' && Number.isNaN(value)) {
|
|
40
35
|
return { error: 'Expected a number' };
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
const plan = FieldValueValidator.planFor(shape);
|
|
44
39
|
const result = plan.validator.validate(value);
|
|
45
|
-
if (!result.valid) return
|
|
40
|
+
if (!result.valid) return refusalOf(result.errors);
|
|
46
41
|
if (plan.custom && typeof value === 'string' && !plan.custom(value)) {
|
|
47
42
|
return { error: `String does not match format "${plan.formatName}".` };
|
|
48
43
|
}
|
|
49
44
|
return { value };
|
|
50
45
|
}
|
|
51
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The value admitted, then handed back in the form the domain writes — an ISO string
|
|
49
|
+
* arrives as a `Date`. `null` passes untouched, and a refusal stops before the codec.
|
|
50
|
+
*/
|
|
51
|
+
parse(value: unknown): Checked {
|
|
52
|
+
const checked = this.validate(value);
|
|
53
|
+
if ('error' in checked) return checked;
|
|
54
|
+
if (checked.value === null) return { value: null };
|
|
55
|
+
|
|
56
|
+
return Boundary.of(this.field).decode(checked.value);
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
private static planFor(shape: Shape): ShapePlan {
|
|
53
60
|
let plan = this.plans.get(shape);
|
|
54
61
|
if (!plan) {
|
|
@@ -75,3 +82,29 @@ export class FieldValueValidator {
|
|
|
75
82
|
return custom;
|
|
76
83
|
}
|
|
77
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The engine states its refusals outermost first, so `errors[0]` on a nested shape is the
|
|
88
|
+
* parent's `Property "street" does not match schema.` — true, and never the reason. The
|
|
89
|
+
* DEEPEST one is the reason, and it is the one that says where.
|
|
90
|
+
*/
|
|
91
|
+
function refusalOf(errors: readonly OutputUnit[]): Checked {
|
|
92
|
+
const deepest = errors.reduce<OutputUnit | undefined>(
|
|
93
|
+
(held, one) => (held && depthOf(held) >= depthOf(one) ? held : one),
|
|
94
|
+
undefined,
|
|
95
|
+
);
|
|
96
|
+
if (!deepest) return { error: 'Invalid value' };
|
|
97
|
+
const path = locationOf(deepest.instanceLocation);
|
|
98
|
+
|
|
99
|
+
return path.length > 0 ? { error: deepest.error, path } : { error: deepest.error };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const depthOf = (unit: OutputUnit): number => locationOf(unit.instanceLocation).length;
|
|
103
|
+
|
|
104
|
+
/** `#/city/zip` — a JSON Pointer fragment, and `~1`/`~0` are how it spells `/` and `~`. */
|
|
105
|
+
const locationOf = (instanceLocation: string): string[] =>
|
|
106
|
+
instanceLocation
|
|
107
|
+
.replace(/^#/, '')
|
|
108
|
+
.split('/')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|