@formspec/build 0.1.0-alpha.40 → 0.1.0-alpha.42

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/browser.js CHANGED
@@ -51,11 +51,35 @@ function normalizeDeclarationPolicy(input) {
51
51
  displayName: normalizeScalarPolicy(input?.displayName)
52
52
  };
53
53
  }
54
+ function normalizeEnumMemberDisplayNamePolicy(input) {
55
+ if (input?.mode === "infer-if-missing") {
56
+ return {
57
+ mode: "infer-if-missing",
58
+ infer: input.infer
59
+ };
60
+ }
61
+ if (input?.mode === "require-explicit") {
62
+ return {
63
+ mode: "require-explicit",
64
+ infer: () => ""
65
+ };
66
+ }
67
+ return {
68
+ mode: "disabled",
69
+ infer: () => ""
70
+ };
71
+ }
72
+ function normalizeEnumMemberPolicy(input) {
73
+ return {
74
+ displayName: normalizeEnumMemberDisplayNamePolicy(input?.displayName)
75
+ };
76
+ }
54
77
  function normalizeMetadataPolicy(input) {
55
78
  return {
56
79
  type: normalizeDeclarationPolicy(input?.type),
57
80
  field: normalizeDeclarationPolicy(input?.field),
58
- method: normalizeDeclarationPolicy(input?.method)
81
+ method: normalizeDeclarationPolicy(input?.method),
82
+ enumMember: normalizeEnumMemberPolicy(input?.enumMember)
59
83
  };
60
84
  }
61
85
  function getDeclarationMetadataPolicy(policy, declarationKind) {
@@ -152,6 +176,134 @@ function resolveResolvedMetadata(current, policy, context) {
152
176
  ...displayNamePlural !== void 0 && { displayNamePlural }
153
177
  };
154
178
  }
179
+ function resolveEnumMemberDisplayName(current, policy, context) {
180
+ if (current !== void 0) {
181
+ return current;
182
+ }
183
+ if (policy.mode === "require-explicit") {
184
+ throw new Error(
185
+ `Metadata policy requires explicit displayName for enum member "${context.logicalName}" on the ${context.surface} surface.`
186
+ );
187
+ }
188
+ if (policy.mode !== "infer-if-missing") {
189
+ return void 0;
190
+ }
191
+ const inferredValue = policy.infer(context).trim();
192
+ return inferredValue !== "" ? inferredValue : void 0;
193
+ }
194
+ function resolveEnumTypeMetadata(type, options) {
195
+ const members = type.members.map((member) => {
196
+ const displayName = resolveEnumMemberDisplayName(
197
+ member.displayName,
198
+ options.policy.enumMember.displayName,
199
+ {
200
+ surface: options.surface,
201
+ logicalName: String(member.value),
202
+ memberValue: member.value,
203
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
204
+ }
205
+ );
206
+ if (displayName === member.displayName) {
207
+ return member;
208
+ }
209
+ return displayName === void 0 ? { value: member.value } : { value: member.value, displayName };
210
+ });
211
+ return members.some((member, index) => member !== type.members[index]) ? { ...type, members } : type;
212
+ }
213
+ function resolveTypeNodeMetadata(type, options) {
214
+ switch (type.kind) {
215
+ case "array":
216
+ return {
217
+ ...type,
218
+ items: resolveTypeNodeMetadata(type.items, options)
219
+ };
220
+ case "object":
221
+ return {
222
+ ...type,
223
+ properties: type.properties.map((property) => resolveObjectPropertyMetadata(property, options))
224
+ };
225
+ case "record":
226
+ return {
227
+ ...type,
228
+ valueType: resolveTypeNodeMetadata(type.valueType, options)
229
+ };
230
+ case "union":
231
+ return {
232
+ ...type,
233
+ members: type.members.map((member) => resolveTypeNodeMetadata(member, options))
234
+ };
235
+ case "enum":
236
+ return resolveEnumTypeMetadata(type, options);
237
+ case "reference":
238
+ case "primitive":
239
+ case "dynamic":
240
+ case "custom":
241
+ return type;
242
+ default: {
243
+ const _exhaustive = type;
244
+ return _exhaustive;
245
+ }
246
+ }
247
+ }
248
+ function resolveObjectPropertyMetadata(property, options) {
249
+ const metadata = resolveResolvedMetadata(property.metadata, options.policy.field, {
250
+ surface: options.surface,
251
+ declarationKind: "field",
252
+ logicalName: property.name,
253
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
254
+ });
255
+ return {
256
+ ...property,
257
+ ...metadata !== void 0 && { metadata },
258
+ type: resolveTypeNodeMetadata(property.type, options)
259
+ };
260
+ }
261
+ function resolveFieldMetadataNode(field, options) {
262
+ const metadata = resolveResolvedMetadata(field.metadata, options.policy.field, {
263
+ surface: options.surface,
264
+ declarationKind: "field",
265
+ logicalName: field.name,
266
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
267
+ });
268
+ return {
269
+ ...field,
270
+ ...metadata !== void 0 && { metadata },
271
+ type: resolveTypeNodeMetadata(field.type, options)
272
+ };
273
+ }
274
+ function resolveFormElementMetadata(element, options) {
275
+ switch (element.kind) {
276
+ case "field":
277
+ return resolveFieldMetadataNode(element, options);
278
+ case "group":
279
+ return {
280
+ ...element,
281
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
282
+ };
283
+ case "conditional":
284
+ return {
285
+ ...element,
286
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
287
+ };
288
+ default: {
289
+ const _exhaustive = element;
290
+ return _exhaustive;
291
+ }
292
+ }
293
+ }
294
+ function resolveTypeDefinitionMetadata(typeDefinition, options) {
295
+ const metadata = resolveResolvedMetadata(typeDefinition.metadata, options.policy.type, {
296
+ surface: options.surface,
297
+ declarationKind: "type",
298
+ logicalName: typeDefinition.name,
299
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
300
+ });
301
+ return {
302
+ ...typeDefinition,
303
+ ...metadata !== void 0 && { metadata },
304
+ type: resolveTypeNodeMetadata(typeDefinition.type, options)
305
+ };
306
+ }
155
307
  function resolveMetadata(explicit, policy, context) {
156
308
  return resolveResolvedMetadata(toExplicitResolvedMetadata(explicit), policy, context);
157
309
  }
@@ -161,6 +313,25 @@ function getSerializedName(logicalName, metadata) {
161
313
  function getDisplayName(metadata) {
162
314
  return metadata?.displayName?.value;
163
315
  }
316
+ function resolveFormIRMetadata(ir, options) {
317
+ const metadata = options.resolveRootTypeMetadata === false ? ir.metadata : resolveResolvedMetadata(ir.metadata, options.policy.type, {
318
+ surface: options.surface,
319
+ declarationKind: "type",
320
+ logicalName: options.rootLogicalName ?? ir.name ?? "FormSpec",
321
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
322
+ });
323
+ return {
324
+ ...ir,
325
+ ...metadata !== void 0 && { metadata },
326
+ elements: ir.elements.map((element) => resolveFormElementMetadata(element, options)),
327
+ typeRegistry: Object.fromEntries(
328
+ Object.entries(ir.typeRegistry).map(([name, definition]) => [
329
+ name,
330
+ resolveTypeDefinitionMetadata(definition, options)
331
+ ])
332
+ )
333
+ };
334
+ }
164
335
 
165
336
  // src/canonicalize/chain-dsl-canonicalizer.ts
166
337
  var CHAIN_DSL_PROVENANCE = {
@@ -182,7 +353,7 @@ function canonicalizeChainDSL(form, options) {
182
353
  const metadataPolicy = normalizeMetadataPolicy(
183
354
  options?.metadata ?? _getFormSpecMetadataPolicy(form)
184
355
  );
185
- return {
356
+ const ir = {
186
357
  kind: "form-ir",
187
358
  irVersion: IR_VERSION,
188
359
  elements: canonicalizeElements(form.elements, metadataPolicy),
@@ -190,6 +361,13 @@ function canonicalizeChainDSL(form, options) {
190
361
  typeRegistry: {},
191
362
  provenance: CHAIN_DSL_PROVENANCE
192
363
  };
364
+ return resolveFormIRMetadata(ir, {
365
+ policy: metadataPolicy,
366
+ surface: "chain-dsl",
367
+ // Chain DSL has no root/type-metadata authoring surface, so only resolve
368
+ // field/type-registry metadata and enum-member labels here.
369
+ resolveRootTypeMetadata: false
370
+ });
193
371
  }
194
372
  function canonicalizeElements(elements, metadataPolicy) {
195
373
  return elements.map((element) => canonicalizeElement(element, metadataPolicy));
@@ -648,17 +826,25 @@ function assertNoSerializedNameCollisions(ir) {
648
826
  // src/json-schema/ir-generator.ts
649
827
  function makeContext(options) {
650
828
  const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
829
+ const rawEnumSerialization = options?.enumSerialization;
651
830
  if (!vendorPrefix.startsWith("x-")) {
652
831
  throw new Error(
653
832
  `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
654
833
  );
655
834
  }
835
+ if (rawEnumSerialization !== void 0 && rawEnumSerialization !== "enum" && rawEnumSerialization !== "oneOf") {
836
+ throw new Error(
837
+ `Invalid enumSerialization "${rawEnumSerialization}". Expected "enum" or "oneOf".`
838
+ );
839
+ }
840
+ const enumSerialization = rawEnumSerialization ?? "enum";
656
841
  return {
657
842
  defs: {},
658
843
  typeNameMap: {},
659
844
  typeRegistry: {},
660
845
  extensionRegistry: options?.extensionRegistry,
661
- vendorPrefix
846
+ vendorPrefix,
847
+ enumSerialization
662
848
  };
663
849
  }
664
850
  function generateJsonSchemaFromIR(ir, options) {
@@ -832,7 +1018,7 @@ function generateTypeNode(type, ctx) {
832
1018
  case "primitive":
833
1019
  return generatePrimitiveType(type);
834
1020
  case "enum":
835
- return generateEnumType(type);
1021
+ return generateEnumType(type, ctx);
836
1022
  case "array":
837
1023
  return generateArrayType(type, ctx);
838
1024
  case "object":
@@ -858,20 +1044,37 @@ function generatePrimitiveType(type) {
858
1044
  type: type.primitiveKind === "integer" || type.primitiveKind === "bigint" ? "integer" : type.primitiveKind
859
1045
  };
860
1046
  }
861
- function generateEnumType(type) {
862
- const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
863
- if (hasDisplayNames) {
1047
+ function generateEnumType(type, ctx) {
1048
+ if (ctx.enumSerialization === "oneOf") {
864
1049
  return {
865
- oneOf: type.members.map((m) => {
866
- const entry = { const: m.value };
867
- if (m.displayName !== void 0) {
868
- entry.title = m.displayName;
869
- }
870
- return entry;
871
- })
1050
+ oneOf: type.members.map((m) => ({
1051
+ const: m.value,
1052
+ title: m.displayName ?? String(m.value)
1053
+ }))
872
1054
  };
873
1055
  }
874
- return { enum: type.members.map((m) => m.value) };
1056
+ const schema = { enum: type.members.map((m) => m.value) };
1057
+ const displayNames = buildEnumDisplayNameExtension(type);
1058
+ if (displayNames !== void 0) {
1059
+ schema[`${ctx.vendorPrefix}-display-names`] = displayNames;
1060
+ }
1061
+ return schema;
1062
+ }
1063
+ function buildEnumDisplayNameExtension(type) {
1064
+ if (!type.members.some((member) => member.displayName !== void 0)) {
1065
+ return void 0;
1066
+ }
1067
+ const displayNames = /* @__PURE__ */ Object.create(null);
1068
+ for (const member of type.members) {
1069
+ const key = String(member.value);
1070
+ if (Object.hasOwn(displayNames, key)) {
1071
+ throw new Error(
1072
+ `Enum display-name key "${key}" is ambiguous after stringification. Use oneOf serialization for mixed string/number enum values that collide.`
1073
+ );
1074
+ }
1075
+ displayNames[key] = member.displayName ?? key;
1076
+ }
1077
+ return displayNames;
875
1078
  }
876
1079
  function generateArrayType(type, ctx) {
877
1080
  return {
@@ -1220,6 +1423,66 @@ function generateCustomType(type, ctx) {
1220
1423
  }
1221
1424
  return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1222
1425
  }
1426
+ var JSON_SCHEMA_STRUCTURAL_KEYWORDS = /* @__PURE__ */ new Set([
1427
+ "$schema",
1428
+ "$ref",
1429
+ "$defs",
1430
+ "$id",
1431
+ "$anchor",
1432
+ "$dynamicRef",
1433
+ "$dynamicAnchor",
1434
+ "$vocabulary",
1435
+ "$comment",
1436
+ "type",
1437
+ "enum",
1438
+ "const",
1439
+ "properties",
1440
+ "patternProperties",
1441
+ "additionalProperties",
1442
+ "required",
1443
+ "items",
1444
+ "prefixItems",
1445
+ "additionalItems",
1446
+ "contains",
1447
+ "allOf",
1448
+ "oneOf",
1449
+ "anyOf",
1450
+ "not",
1451
+ "if",
1452
+ "then",
1453
+ "else",
1454
+ "minimum",
1455
+ "maximum",
1456
+ "exclusiveMinimum",
1457
+ "exclusiveMaximum",
1458
+ "multipleOf",
1459
+ "minLength",
1460
+ "maxLength",
1461
+ "pattern",
1462
+ "minItems",
1463
+ "maxItems",
1464
+ "uniqueItems",
1465
+ "minProperties",
1466
+ "maxProperties",
1467
+ "minContains",
1468
+ "maxContains",
1469
+ "format",
1470
+ "title",
1471
+ "description",
1472
+ "default",
1473
+ "deprecated",
1474
+ "readOnly",
1475
+ "writeOnly",
1476
+ "examples",
1477
+ "dependentRequired",
1478
+ "dependentSchemas",
1479
+ "propertyNames",
1480
+ "unevaluatedItems",
1481
+ "unevaluatedProperties",
1482
+ "contentEncoding",
1483
+ "contentMediaType",
1484
+ "contentSchema"
1485
+ ]);
1223
1486
  function applyCustomConstraint(schema, constraint, ctx) {
1224
1487
  const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1225
1488
  if (registration === void 0) {
@@ -1227,12 +1490,25 @@ function applyCustomConstraint(schema, constraint, ctx) {
1227
1490
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1228
1491
  );
1229
1492
  }
1230
- assignVendorPrefixedExtensionKeywords(
1231
- schema,
1232
- registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
1233
- ctx.vendorPrefix,
1234
- `custom constraint "${constraint.constraintId}"`
1235
- );
1493
+ const extensionSchema = registration.toJsonSchema(constraint.payload, ctx.vendorPrefix);
1494
+ if (registration.emitsVocabularyKeywords) {
1495
+ const target = schema;
1496
+ for (const [key, value] of Object.entries(extensionSchema)) {
1497
+ if (JSON_SCHEMA_STRUCTURAL_KEYWORDS.has(key)) {
1498
+ throw new Error(
1499
+ `Custom constraint "${constraint.constraintId}" with emitsVocabularyKeywords must not overwrite standard JSON Schema keyword "${key}"`
1500
+ );
1501
+ }
1502
+ target[key] = value;
1503
+ }
1504
+ } else {
1505
+ assignVendorPrefixedExtensionKeywords(
1506
+ schema,
1507
+ extensionSchema,
1508
+ ctx.vendorPrefix,
1509
+ `custom constraint "${constraint.constraintId}"`
1510
+ );
1511
+ }
1236
1512
  }
1237
1513
  function applyCustomAnnotation(schema, annotation, ctx) {
1238
1514
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -1264,11 +1540,17 @@ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPr
1264
1540
 
1265
1541
  // src/json-schema/generator.ts
1266
1542
  function generateJsonSchema(form, options) {
1543
+ const metadata = options?.metadata;
1544
+ const vendorPrefix = options?.vendorPrefix;
1545
+ const enumSerialization = options?.enumSerialization;
1267
1546
  const ir = canonicalizeChainDSL(
1268
1547
  form,
1269
- options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
1548
+ metadata !== void 0 ? { metadata } : void 0
1270
1549
  );
1271
- const internalOptions = options?.vendorPrefix === void 0 ? void 0 : { vendorPrefix: options.vendorPrefix };
1550
+ const internalOptions = vendorPrefix === void 0 && enumSerialization === void 0 ? void 0 : {
1551
+ ...vendorPrefix !== void 0 && { vendorPrefix },
1552
+ ...enumSerialization !== void 0 && { enumSerialization }
1553
+ };
1272
1554
  return generateJsonSchemaFromIR(ir, internalOptions);
1273
1555
  }
1274
1556