@cms0/shared 0.1.0 → 0.2.14
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/cjs/descriptor-capabilities.js +404 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/union.js +170 -0
- package/dist/cjs/validation.js +60 -0
- package/dist/esm/descriptor-capabilities.js +385 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/union.js +159 -0
- package/dist/esm/validation.js +60 -0
- package/dist/types/descriptor-capabilities.d.ts +90 -0
- package/dist/types/descriptor-capabilities.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/union.d.ts +25 -0
- package/dist/types/union.d.ts.map +1 -0
- package/dist/types/validation.d.ts +38 -4
- package/dist/types/validation.d.ts.map +1 -1
- package/package.json +13 -10
- /package/{src → dist}/responsive-break.css +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { createTaggedUnionValue, getUnionBranchKeyAt, getUnionBranchKeys, isTaggedUnionValue, } from "./union.js";
|
|
2
|
+
function isPlainRecord(value) {
|
|
3
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function isPrimitiveDescriptor(descriptor) {
|
|
6
|
+
if (!descriptor)
|
|
7
|
+
return false;
|
|
8
|
+
const type = descriptor.type;
|
|
9
|
+
return (descriptor.kind === "primitive" ||
|
|
10
|
+
(!descriptor.kind && type !== "object" && type !== "array"));
|
|
11
|
+
}
|
|
12
|
+
export function isEnumDescriptor(descriptor) {
|
|
13
|
+
return descriptor?.kind === "enum";
|
|
14
|
+
}
|
|
15
|
+
export function isUnionDescriptor(descriptor) {
|
|
16
|
+
return descriptor?.kind === "union";
|
|
17
|
+
}
|
|
18
|
+
export function isModelRefDescriptor(descriptor) {
|
|
19
|
+
return descriptor?.kind === "modelRef";
|
|
20
|
+
}
|
|
21
|
+
export function isObjectDescriptor(descriptor) {
|
|
22
|
+
return !!descriptor && descriptor.type === "object";
|
|
23
|
+
}
|
|
24
|
+
export function isArrayDescriptor(descriptor) {
|
|
25
|
+
return !!descriptor && descriptor.type === "array";
|
|
26
|
+
}
|
|
27
|
+
export function getModelDescriptor(fullDescriptor, modelName) {
|
|
28
|
+
if (!fullDescriptor || !modelName)
|
|
29
|
+
return undefined;
|
|
30
|
+
return fullDescriptor.models?.[modelName];
|
|
31
|
+
}
|
|
32
|
+
export function getDescriptorPropertyEntries(descriptor) {
|
|
33
|
+
const properties = descriptor.properties ?? {};
|
|
34
|
+
const order = Array.isArray(descriptor.propertyOrder)
|
|
35
|
+
? descriptor.propertyOrder
|
|
36
|
+
: [];
|
|
37
|
+
const ordered = order
|
|
38
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(properties, key))
|
|
39
|
+
.map((key) => [key, properties[key]]);
|
|
40
|
+
const remaining = Object.entries(properties)
|
|
41
|
+
.filter(([key]) => !order.includes(key))
|
|
42
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
43
|
+
return [...ordered, ...remaining];
|
|
44
|
+
}
|
|
45
|
+
export function resolveAssetKindFromModelDescriptor(modelName, modelDescriptor) {
|
|
46
|
+
const presentation = modelDescriptor?.presentation;
|
|
47
|
+
if (presentation?.kind === "asset") {
|
|
48
|
+
if (presentation.assetKind === "image")
|
|
49
|
+
return "image";
|
|
50
|
+
if (presentation.assetKind === "video")
|
|
51
|
+
return "video";
|
|
52
|
+
return "file";
|
|
53
|
+
}
|
|
54
|
+
if (modelName === "Image")
|
|
55
|
+
return "image";
|
|
56
|
+
if (modelName === "Video")
|
|
57
|
+
return "video";
|
|
58
|
+
if (modelName === "File")
|
|
59
|
+
return "file";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function resolveDescriptorCapability(descriptor, fullDescriptor) {
|
|
63
|
+
if (isUnionDescriptor(descriptor)) {
|
|
64
|
+
const keys = getUnionBranchKeys(descriptor);
|
|
65
|
+
const branches = Array.isArray(descriptor.anyOf) ? descriptor.anyOf : [];
|
|
66
|
+
return {
|
|
67
|
+
kind: "union",
|
|
68
|
+
discriminatorKey: descriptor.discriminator?.key,
|
|
69
|
+
branches: branches.map((branch, index) => ({
|
|
70
|
+
index,
|
|
71
|
+
key: keys[index] ?? getUnionBranchKeyAt(descriptor, index) ?? `branch_${index + 1}`,
|
|
72
|
+
descriptor: branch,
|
|
73
|
+
capability: resolveDescriptorCapability(branch, fullDescriptor),
|
|
74
|
+
label: getDescriptorDisplayName(branch, index),
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (isModelRefDescriptor(descriptor)) {
|
|
79
|
+
const modelDescriptor = getModelDescriptor(fullDescriptor, descriptor.model);
|
|
80
|
+
const assetKind = resolveAssetKindFromModelDescriptor(descriptor.model, modelDescriptor);
|
|
81
|
+
if (assetKind) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "asset-ref",
|
|
84
|
+
modelName: descriptor.model,
|
|
85
|
+
assetKind,
|
|
86
|
+
modelDescriptor,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
kind: "model-ref",
|
|
91
|
+
modelName: descriptor.model,
|
|
92
|
+
modelDescriptor,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (isEnumDescriptor(descriptor)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "enum",
|
|
98
|
+
valueType: descriptor.valueType,
|
|
99
|
+
options: Array.isArray(descriptor.values) ? descriptor.values.slice() : [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (isObjectDescriptor(descriptor)) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "object-inline",
|
|
105
|
+
properties: getDescriptorPropertyEntries(descriptor),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (isArrayDescriptor(descriptor)) {
|
|
109
|
+
return {
|
|
110
|
+
kind: "collection-inline",
|
|
111
|
+
itemDescriptor: descriptor.items,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!isPrimitiveDescriptor(descriptor)) {
|
|
115
|
+
return {
|
|
116
|
+
kind: "unsupported",
|
|
117
|
+
reason: "Unsupported descriptor kind.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (descriptor.customType === "LocalizedString") {
|
|
121
|
+
return { kind: "localized-string" };
|
|
122
|
+
}
|
|
123
|
+
if (descriptor.customType === "RichText") {
|
|
124
|
+
return { kind: "rich-text" };
|
|
125
|
+
}
|
|
126
|
+
if (descriptor.customType === "LocalizedRichText") {
|
|
127
|
+
return { kind: "localized-rich-text" };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
kind: "scalar",
|
|
131
|
+
primitiveType: descriptor.type,
|
|
132
|
+
customType: descriptor.customType,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function getDescriptorDefaultValue(descriptor, options = {}) {
|
|
136
|
+
const capability = resolveDescriptorCapability(descriptor);
|
|
137
|
+
if (capability.kind === "localized-string") {
|
|
138
|
+
const locales = options.locales?.length ? [...options.locales] : [options.defaultLocale ?? "en"];
|
|
139
|
+
const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
|
|
140
|
+
return {
|
|
141
|
+
defaultLocale,
|
|
142
|
+
locales: Object.fromEntries(locales.map((locale) => [locale, ""])),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (capability.kind === "rich-text") {
|
|
146
|
+
return { value: {}, html: "" };
|
|
147
|
+
}
|
|
148
|
+
if (capability.kind === "localized-rich-text") {
|
|
149
|
+
const locales = options.locales?.length ? [...options.locales] : [options.defaultLocale ?? "en"];
|
|
150
|
+
const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
|
|
151
|
+
return {
|
|
152
|
+
defaultLocale,
|
|
153
|
+
locales: Object.fromEntries(locales.map((locale) => [locale, { value: {}, html: "" }])),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (capability.kind === "model-ref" || capability.kind === "asset-ref") {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (capability.kind === "enum") {
|
|
160
|
+
return capability.options[0] ?? null;
|
|
161
|
+
}
|
|
162
|
+
if (capability.kind === "union") {
|
|
163
|
+
const firstBranch = capability.branches[0];
|
|
164
|
+
if (!firstBranch)
|
|
165
|
+
return null;
|
|
166
|
+
return createTaggedUnionValue(descriptor, 0, getDescriptorDefaultValue(firstBranch.descriptor, options));
|
|
167
|
+
}
|
|
168
|
+
if (capability.kind === "object-inline") {
|
|
169
|
+
return Object.fromEntries(capability.properties.map(([key, propertyDescriptor]) => [
|
|
170
|
+
key,
|
|
171
|
+
getDescriptorDefaultValue(propertyDescriptor, options),
|
|
172
|
+
]));
|
|
173
|
+
}
|
|
174
|
+
if (capability.kind === "collection-inline") {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
if (capability.kind === "scalar") {
|
|
178
|
+
if (capability.primitiveType === "string")
|
|
179
|
+
return "";
|
|
180
|
+
if (capability.primitiveType === "number")
|
|
181
|
+
return 0;
|
|
182
|
+
if (capability.primitiveType === "boolean")
|
|
183
|
+
return false;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
export function normalizeEnumValue(descriptor, value) {
|
|
189
|
+
const valueType = (descriptor.valueType ?? "string");
|
|
190
|
+
const values = Array.isArray(descriptor.values) ? descriptor.values : [];
|
|
191
|
+
let normalized = value;
|
|
192
|
+
if (valueType === "string") {
|
|
193
|
+
normalized = value == null ? "" : String(value);
|
|
194
|
+
}
|
|
195
|
+
else if (valueType === "number") {
|
|
196
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
197
|
+
normalized = value;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const parsed = Number(value);
|
|
201
|
+
normalized = Number.isFinite(parsed) ? parsed : null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (valueType === "boolean") {
|
|
205
|
+
if (value === true || value === "true" || value === 1 || value === "1") {
|
|
206
|
+
normalized = true;
|
|
207
|
+
}
|
|
208
|
+
else if (value === false || value === "false" || value === 0 || value === "0") {
|
|
209
|
+
normalized = false;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
normalized = Boolean(value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!values.length)
|
|
216
|
+
return normalized;
|
|
217
|
+
if (values.some((entry) => Object.is(entry, normalized))) {
|
|
218
|
+
return normalized;
|
|
219
|
+
}
|
|
220
|
+
return values[0];
|
|
221
|
+
}
|
|
222
|
+
export function formatEnumOptionValue(value) {
|
|
223
|
+
return JSON.stringify(value);
|
|
224
|
+
}
|
|
225
|
+
export function parseEnumOptionValue(raw, valueType) {
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(raw);
|
|
228
|
+
if (valueType === "string")
|
|
229
|
+
return String(parsed ?? "");
|
|
230
|
+
if (valueType === "number") {
|
|
231
|
+
const num = Number(parsed);
|
|
232
|
+
return Number.isFinite(num) ? num : null;
|
|
233
|
+
}
|
|
234
|
+
return Boolean(parsed);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
if (valueType === "number") {
|
|
238
|
+
const num = Number(raw);
|
|
239
|
+
return Number.isFinite(num) ? num : null;
|
|
240
|
+
}
|
|
241
|
+
if (valueType === "boolean") {
|
|
242
|
+
return raw === "true";
|
|
243
|
+
}
|
|
244
|
+
return raw;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function isUrlLikeString(input) {
|
|
248
|
+
return (typeof input === "string" &&
|
|
249
|
+
(input.includes("://") || input.startsWith("/") || input.startsWith("./")));
|
|
250
|
+
}
|
|
251
|
+
function isUuidLikeId(input) {
|
|
252
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input);
|
|
253
|
+
}
|
|
254
|
+
function normalizeModelRefValue(value) {
|
|
255
|
+
if (typeof value === "string" || typeof value === "number")
|
|
256
|
+
return value;
|
|
257
|
+
if (!isPlainRecord(value))
|
|
258
|
+
return null;
|
|
259
|
+
const id = value.id;
|
|
260
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
261
|
+
}
|
|
262
|
+
function descriptorMatchScore(descriptor, value) {
|
|
263
|
+
if (isPrimitiveDescriptor(descriptor)) {
|
|
264
|
+
if (descriptor.type === "json")
|
|
265
|
+
return 1;
|
|
266
|
+
if (descriptor.type === "string")
|
|
267
|
+
return typeof value === "string" ? 4 : 0;
|
|
268
|
+
if (descriptor.type === "number")
|
|
269
|
+
return typeof value === "number" ? 4 : 0;
|
|
270
|
+
if (descriptor.type === "boolean")
|
|
271
|
+
return typeof value === "boolean" ? 4 : 0;
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
if (isEnumDescriptor(descriptor)) {
|
|
275
|
+
const values = Array.isArray(descriptor.values) ? descriptor.values : [];
|
|
276
|
+
if (!values.length)
|
|
277
|
+
return 2;
|
|
278
|
+
return values.some((entry) => Object.is(entry, value)) ? 6 : 0;
|
|
279
|
+
}
|
|
280
|
+
if (isModelRefDescriptor(descriptor)) {
|
|
281
|
+
if (typeof value === "string") {
|
|
282
|
+
const normalized = value.trim();
|
|
283
|
+
if (!normalized)
|
|
284
|
+
return 0;
|
|
285
|
+
if (isUrlLikeString(normalized))
|
|
286
|
+
return 1;
|
|
287
|
+
return isUuidLikeId(normalized) ? 6 : 2;
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === "number") {
|
|
290
|
+
return Number.isFinite(value) && value !== 0 ? 4 : 0;
|
|
291
|
+
}
|
|
292
|
+
const candidate = normalizeModelRefValue(value);
|
|
293
|
+
if (candidate == null)
|
|
294
|
+
return 0;
|
|
295
|
+
return typeof candidate === "string" && isUuidLikeId(candidate) ? 5 : 2;
|
|
296
|
+
}
|
|
297
|
+
if (isArrayDescriptor(descriptor)) {
|
|
298
|
+
return Array.isArray(value) ? 3 : 0;
|
|
299
|
+
}
|
|
300
|
+
if (isObjectDescriptor(descriptor)) {
|
|
301
|
+
if (!isPlainRecord(value))
|
|
302
|
+
return 0;
|
|
303
|
+
const keys = Object.keys(descriptor.properties ?? {});
|
|
304
|
+
const matched = keys.filter((key) => Object.prototype.hasOwnProperty.call(value, key)).length;
|
|
305
|
+
return 2 + matched;
|
|
306
|
+
}
|
|
307
|
+
if (isUnionDescriptor(descriptor)) {
|
|
308
|
+
const branches = Array.isArray(descriptor.anyOf) ? descriptor.anyOf : [];
|
|
309
|
+
if (!branches.length)
|
|
310
|
+
return 0;
|
|
311
|
+
return Math.max(...branches.map((branch) => descriptorMatchScore(branch, value)));
|
|
312
|
+
}
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
export function resolveUnionBranchIndex(descriptor, value) {
|
|
316
|
+
const branches = Array.isArray(descriptor.anyOf) ? descriptor.anyOf : [];
|
|
317
|
+
if (!branches.length)
|
|
318
|
+
return 0;
|
|
319
|
+
const discriminatorKey = descriptor.discriminator?.key;
|
|
320
|
+
if (discriminatorKey && isPlainRecord(value)) {
|
|
321
|
+
const discriminatorValue = value[discriminatorKey];
|
|
322
|
+
for (let index = 0; index < branches.length; index += 1) {
|
|
323
|
+
const branch = branches[index];
|
|
324
|
+
const expected = branch?.properties?.[discriminatorKey]?.values?.[0];
|
|
325
|
+
if (expected !== undefined && Object.is(expected, discriminatorValue)) {
|
|
326
|
+
return index;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let bestIndex = 0;
|
|
331
|
+
let bestScore = -1;
|
|
332
|
+
for (let index = 0; index < branches.length; index += 1) {
|
|
333
|
+
const score = descriptorMatchScore(branches[index], value);
|
|
334
|
+
if (score > bestScore) {
|
|
335
|
+
bestScore = score;
|
|
336
|
+
bestIndex = index;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return bestIndex;
|
|
340
|
+
}
|
|
341
|
+
export function normalizeUnionValue(descriptor, value, options = {}) {
|
|
342
|
+
if (value == null) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (isTaggedUnionValue(value)) {
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
const branches = Array.isArray(descriptor.anyOf) ? descriptor.anyOf : [];
|
|
349
|
+
if (!branches.length)
|
|
350
|
+
return null;
|
|
351
|
+
const branchIndex = resolveUnionBranchIndex(descriptor, value);
|
|
352
|
+
const branchKey = getUnionBranchKeyAt(descriptor, branchIndex);
|
|
353
|
+
if (!branchKey)
|
|
354
|
+
return null;
|
|
355
|
+
const branchDescriptor = branches[branchIndex];
|
|
356
|
+
const branchValue = value == null
|
|
357
|
+
? getDescriptorDefaultValue(branchDescriptor, options)
|
|
358
|
+
: value;
|
|
359
|
+
return createTaggedUnionValue(descriptor, branchIndex, branchValue);
|
|
360
|
+
}
|
|
361
|
+
export function getDescriptorDisplayName(descriptor, index = 0) {
|
|
362
|
+
const customType = descriptor?.customType;
|
|
363
|
+
if (typeof customType === "string" && customType.length > 0) {
|
|
364
|
+
return humanizeVisualText(customType);
|
|
365
|
+
}
|
|
366
|
+
if (isModelRefDescriptor(descriptor)) {
|
|
367
|
+
return humanizeVisualText(descriptor.model || `Branch ${index + 1}`);
|
|
368
|
+
}
|
|
369
|
+
if (isEnumDescriptor(descriptor))
|
|
370
|
+
return "Enum";
|
|
371
|
+
if (isUnionDescriptor(descriptor))
|
|
372
|
+
return "Union";
|
|
373
|
+
const descriptorType = descriptor?.type;
|
|
374
|
+
if (typeof descriptorType === "string") {
|
|
375
|
+
return humanizeVisualText(descriptorType);
|
|
376
|
+
}
|
|
377
|
+
return `Branch ${index + 1}`;
|
|
378
|
+
}
|
|
379
|
+
function humanizeVisualText(value) {
|
|
380
|
+
return value
|
|
381
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
382
|
+
.replace(/[-_]+/g, " ")
|
|
383
|
+
.trim()
|
|
384
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
385
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export const CMS0_UNION_META_KEY = "__cms0Union";
|
|
2
|
+
function isObjectRecord(value) {
|
|
3
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function normalizeBranchShape(descriptor) {
|
|
6
|
+
if (descriptor?.kind === "modelRef") {
|
|
7
|
+
return {
|
|
8
|
+
kind: "modelRef",
|
|
9
|
+
model: descriptor.model,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (descriptor?.kind === "enum") {
|
|
13
|
+
return {
|
|
14
|
+
kind: "enum",
|
|
15
|
+
valueType: descriptor.valueType ?? "string",
|
|
16
|
+
values: Array.isArray(descriptor.values)
|
|
17
|
+
? [...descriptor.values]
|
|
18
|
+
: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (descriptor?.kind === "union") {
|
|
22
|
+
const branches = Array.isArray(descriptor?.anyOf)
|
|
23
|
+
? descriptor.anyOf
|
|
24
|
+
: [];
|
|
25
|
+
return {
|
|
26
|
+
kind: "union",
|
|
27
|
+
anyOf: branches.map((branch) => normalizeBranchShape(branch)),
|
|
28
|
+
...(typeof descriptor?.discriminator?.key === "string"
|
|
29
|
+
? { discriminator: { key: descriptor.discriminator.key } }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (descriptor?.type === "array") {
|
|
34
|
+
return {
|
|
35
|
+
kind: "array",
|
|
36
|
+
items: normalizeBranchShape(descriptor.items),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (descriptor?.type === "object") {
|
|
40
|
+
const properties = descriptor?.properties &&
|
|
41
|
+
typeof descriptor.properties === "object"
|
|
42
|
+
? descriptor.properties
|
|
43
|
+
: {};
|
|
44
|
+
const normalizedProperties = Object.fromEntries(Object.keys(properties)
|
|
45
|
+
.sort((a, b) => a.localeCompare(b))
|
|
46
|
+
.map((key) => [key, normalizeBranchShape(properties[key])]));
|
|
47
|
+
return {
|
|
48
|
+
kind: "object",
|
|
49
|
+
properties: normalizedProperties,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "primitive",
|
|
54
|
+
type: descriptor?.type ?? "json",
|
|
55
|
+
...(typeof descriptor?.customType === "string"
|
|
56
|
+
? { customType: descriptor.customType }
|
|
57
|
+
: {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function stableStringify(value) {
|
|
61
|
+
if (value === null)
|
|
62
|
+
return "null";
|
|
63
|
+
if (typeof value === "string")
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
66
|
+
return String(value);
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
69
|
+
}
|
|
70
|
+
if (!isObjectRecord(value))
|
|
71
|
+
return JSON.stringify(value);
|
|
72
|
+
const entries = Object.keys(value)
|
|
73
|
+
.sort((a, b) => a.localeCompare(b))
|
|
74
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
75
|
+
return `{${entries.join(",")}}`;
|
|
76
|
+
}
|
|
77
|
+
function fnv1a32(input) {
|
|
78
|
+
let hash = 0x811c9dc5;
|
|
79
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
80
|
+
hash ^= input.charCodeAt(i);
|
|
81
|
+
hash = Math.imul(hash, 0x01000193);
|
|
82
|
+
}
|
|
83
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
84
|
+
}
|
|
85
|
+
export function computeUnionBranchKeys(branches) {
|
|
86
|
+
const used = new Set();
|
|
87
|
+
return branches.map((branch, index) => {
|
|
88
|
+
const signature = stableStringify(normalizeBranchShape(branch));
|
|
89
|
+
const base = `branch_${fnv1a32(signature)}`;
|
|
90
|
+
let key = base;
|
|
91
|
+
let suffix = 1;
|
|
92
|
+
while (used.has(key)) {
|
|
93
|
+
suffix += 1;
|
|
94
|
+
key = `${base}_${suffix}`;
|
|
95
|
+
}
|
|
96
|
+
used.add(key);
|
|
97
|
+
return key || `branch_${index + 1}`;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function getUnionBranchKeys(descriptor) {
|
|
101
|
+
const branches = Array.isArray(descriptor?.anyOf)
|
|
102
|
+
? descriptor.anyOf
|
|
103
|
+
: [];
|
|
104
|
+
if (!branches.length)
|
|
105
|
+
return [];
|
|
106
|
+
const explicit = Array.isArray(descriptor?.branchKeys)
|
|
107
|
+
? descriptor.branchKeys
|
|
108
|
+
: [];
|
|
109
|
+
const normalizedExplicit = explicit
|
|
110
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
111
|
+
.filter((entry) => entry.length > 0);
|
|
112
|
+
if (normalizedExplicit.length === branches.length &&
|
|
113
|
+
new Set(normalizedExplicit).size === normalizedExplicit.length) {
|
|
114
|
+
return normalizedExplicit;
|
|
115
|
+
}
|
|
116
|
+
return computeUnionBranchKeys(branches);
|
|
117
|
+
}
|
|
118
|
+
export function getUnionBranchKeyAt(descriptor, index) {
|
|
119
|
+
const keys = getUnionBranchKeys(descriptor);
|
|
120
|
+
return keys[index];
|
|
121
|
+
}
|
|
122
|
+
export function isTaggedUnionValue(value) {
|
|
123
|
+
if (!isObjectRecord(value))
|
|
124
|
+
return false;
|
|
125
|
+
const meta = value[CMS0_UNION_META_KEY];
|
|
126
|
+
if (!isObjectRecord(meta))
|
|
127
|
+
return false;
|
|
128
|
+
if (typeof meta.branchKey !== "string" || !meta.branchKey.trim())
|
|
129
|
+
return false;
|
|
130
|
+
return Object.prototype.hasOwnProperty.call(value, "value");
|
|
131
|
+
}
|
|
132
|
+
export function encodeTaggedUnionValue(branchKey, value) {
|
|
133
|
+
return {
|
|
134
|
+
[CMS0_UNION_META_KEY]: { branchKey },
|
|
135
|
+
value,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function decodeTaggedUnionValue(value) {
|
|
139
|
+
if (!isTaggedUnionValue(value))
|
|
140
|
+
return null;
|
|
141
|
+
const meta = value[CMS0_UNION_META_KEY];
|
|
142
|
+
return {
|
|
143
|
+
branchKey: meta.branchKey.trim(),
|
|
144
|
+
value: value.value,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function createTaggedUnionValue(descriptor, branchIndex, branchValue) {
|
|
148
|
+
const branchKey = getUnionBranchKeyAt(descriptor, branchIndex);
|
|
149
|
+
if (!branchKey)
|
|
150
|
+
return null;
|
|
151
|
+
return encodeTaggedUnionValue(branchKey, branchValue);
|
|
152
|
+
}
|
|
153
|
+
export function resolveTaggedUnionBranchIndex(descriptor, value) {
|
|
154
|
+
const decoded = decodeTaggedUnionValue(value);
|
|
155
|
+
if (!decoded)
|
|
156
|
+
return -1;
|
|
157
|
+
const keys = getUnionBranchKeys(descriptor);
|
|
158
|
+
return keys.findIndex((key) => key === decoded.branchKey);
|
|
159
|
+
}
|
package/dist/esm/validation.js
CHANGED
|
@@ -26,6 +26,66 @@ function convertDescriptorToZod(desc, modelZodSchemas, opts = {}) {
|
|
|
26
26
|
return applyOptionality(z.any());
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
if (desc.kind === "enum") {
|
|
30
|
+
const literals = Array.isArray(desc.values) ? desc.values : [];
|
|
31
|
+
if (!literals.length) {
|
|
32
|
+
switch (desc.valueType) {
|
|
33
|
+
case "number":
|
|
34
|
+
return applyOptionality(z.number());
|
|
35
|
+
case "boolean":
|
|
36
|
+
return applyOptionality(z.boolean());
|
|
37
|
+
case "string":
|
|
38
|
+
default:
|
|
39
|
+
return applyOptionality(z.string());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (desc.valueType === "string") {
|
|
43
|
+
const values = literals
|
|
44
|
+
.filter((value) => typeof value === "string")
|
|
45
|
+
.map((value) => value.trim())
|
|
46
|
+
.filter((value) => value.length > 0);
|
|
47
|
+
if (!values.length) {
|
|
48
|
+
return applyOptionality(z.string());
|
|
49
|
+
}
|
|
50
|
+
const unique = Array.from(new Set(values));
|
|
51
|
+
const [first, ...rest] = unique;
|
|
52
|
+
if (!first)
|
|
53
|
+
return applyOptionality(z.string());
|
|
54
|
+
return applyOptionality(z.enum([first, ...rest]));
|
|
55
|
+
}
|
|
56
|
+
if (desc.valueType === "number") {
|
|
57
|
+
const values = literals.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
58
|
+
if (!values.length)
|
|
59
|
+
return applyOptionality(z.number());
|
|
60
|
+
const unique = Array.from(new Set(values));
|
|
61
|
+
const [first, ...rest] = unique;
|
|
62
|
+
if (first === undefined)
|
|
63
|
+
return applyOptionality(z.number());
|
|
64
|
+
const schema = rest.length
|
|
65
|
+
? z.union([z.literal(first), ...rest.map((value) => z.literal(value))])
|
|
66
|
+
: z.literal(first);
|
|
67
|
+
return applyOptionality(schema);
|
|
68
|
+
}
|
|
69
|
+
const values = literals.filter((value) => typeof value === "boolean");
|
|
70
|
+
if (!values.length)
|
|
71
|
+
return applyOptionality(z.boolean());
|
|
72
|
+
if (values.includes(true) && values.includes(false)) {
|
|
73
|
+
return applyOptionality(z.boolean());
|
|
74
|
+
}
|
|
75
|
+
return applyOptionality(z.literal(values[0]));
|
|
76
|
+
}
|
|
77
|
+
if (desc.kind === "union") {
|
|
78
|
+
const branches = (Array.isArray(desc.anyOf) ? desc.anyOf : [])
|
|
79
|
+
.map((branch) => convertDescriptorToZod(branch, modelZodSchemas, opts))
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
if (!branches.length)
|
|
82
|
+
return applyOptionality(z.any());
|
|
83
|
+
if (branches.length === 1)
|
|
84
|
+
return applyOptionality(branches[0]);
|
|
85
|
+
const [first, second, ...rest] = branches;
|
|
86
|
+
const schema = z.union([first, second, ...rest]);
|
|
87
|
+
return applyOptionality(schema);
|
|
88
|
+
}
|
|
29
89
|
if (desc.kind === "modelRef") {
|
|
30
90
|
const schema = modelZodSchemas[desc.model];
|
|
31
91
|
const base = schema ?? z.any();
|