@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.cjs CHANGED
@@ -101,11 +101,35 @@ function normalizeDeclarationPolicy(input) {
101
101
  displayName: normalizeScalarPolicy(input?.displayName)
102
102
  };
103
103
  }
104
+ function normalizeEnumMemberDisplayNamePolicy(input) {
105
+ if (input?.mode === "infer-if-missing") {
106
+ return {
107
+ mode: "infer-if-missing",
108
+ infer: input.infer
109
+ };
110
+ }
111
+ if (input?.mode === "require-explicit") {
112
+ return {
113
+ mode: "require-explicit",
114
+ infer: () => ""
115
+ };
116
+ }
117
+ return {
118
+ mode: "disabled",
119
+ infer: () => ""
120
+ };
121
+ }
122
+ function normalizeEnumMemberPolicy(input) {
123
+ return {
124
+ displayName: normalizeEnumMemberDisplayNamePolicy(input?.displayName)
125
+ };
126
+ }
104
127
  function normalizeMetadataPolicy(input) {
105
128
  return {
106
129
  type: normalizeDeclarationPolicy(input?.type),
107
130
  field: normalizeDeclarationPolicy(input?.field),
108
- method: normalizeDeclarationPolicy(input?.method)
131
+ method: normalizeDeclarationPolicy(input?.method),
132
+ enumMember: normalizeEnumMemberPolicy(input?.enumMember)
109
133
  };
110
134
  }
111
135
  function getDeclarationMetadataPolicy(policy, declarationKind) {
@@ -202,6 +226,134 @@ function resolveResolvedMetadata(current, policy, context) {
202
226
  ...displayNamePlural !== void 0 && { displayNamePlural }
203
227
  };
204
228
  }
229
+ function resolveEnumMemberDisplayName(current, policy, context) {
230
+ if (current !== void 0) {
231
+ return current;
232
+ }
233
+ if (policy.mode === "require-explicit") {
234
+ throw new Error(
235
+ `Metadata policy requires explicit displayName for enum member "${context.logicalName}" on the ${context.surface} surface.`
236
+ );
237
+ }
238
+ if (policy.mode !== "infer-if-missing") {
239
+ return void 0;
240
+ }
241
+ const inferredValue = policy.infer(context).trim();
242
+ return inferredValue !== "" ? inferredValue : void 0;
243
+ }
244
+ function resolveEnumTypeMetadata(type, options) {
245
+ const members = type.members.map((member) => {
246
+ const displayName = resolveEnumMemberDisplayName(
247
+ member.displayName,
248
+ options.policy.enumMember.displayName,
249
+ {
250
+ surface: options.surface,
251
+ logicalName: String(member.value),
252
+ memberValue: member.value,
253
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
254
+ }
255
+ );
256
+ if (displayName === member.displayName) {
257
+ return member;
258
+ }
259
+ return displayName === void 0 ? { value: member.value } : { value: member.value, displayName };
260
+ });
261
+ return members.some((member, index) => member !== type.members[index]) ? { ...type, members } : type;
262
+ }
263
+ function resolveTypeNodeMetadata(type, options) {
264
+ switch (type.kind) {
265
+ case "array":
266
+ return {
267
+ ...type,
268
+ items: resolveTypeNodeMetadata(type.items, options)
269
+ };
270
+ case "object":
271
+ return {
272
+ ...type,
273
+ properties: type.properties.map((property) => resolveObjectPropertyMetadata(property, options))
274
+ };
275
+ case "record":
276
+ return {
277
+ ...type,
278
+ valueType: resolveTypeNodeMetadata(type.valueType, options)
279
+ };
280
+ case "union":
281
+ return {
282
+ ...type,
283
+ members: type.members.map((member) => resolveTypeNodeMetadata(member, options))
284
+ };
285
+ case "enum":
286
+ return resolveEnumTypeMetadata(type, options);
287
+ case "reference":
288
+ case "primitive":
289
+ case "dynamic":
290
+ case "custom":
291
+ return type;
292
+ default: {
293
+ const _exhaustive = type;
294
+ return _exhaustive;
295
+ }
296
+ }
297
+ }
298
+ function resolveObjectPropertyMetadata(property, options) {
299
+ const metadata = resolveResolvedMetadata(property.metadata, options.policy.field, {
300
+ surface: options.surface,
301
+ declarationKind: "field",
302
+ logicalName: property.name,
303
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
304
+ });
305
+ return {
306
+ ...property,
307
+ ...metadata !== void 0 && { metadata },
308
+ type: resolveTypeNodeMetadata(property.type, options)
309
+ };
310
+ }
311
+ function resolveFieldMetadataNode(field, options) {
312
+ const metadata = resolveResolvedMetadata(field.metadata, options.policy.field, {
313
+ surface: options.surface,
314
+ declarationKind: "field",
315
+ logicalName: field.name,
316
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
317
+ });
318
+ return {
319
+ ...field,
320
+ ...metadata !== void 0 && { metadata },
321
+ type: resolveTypeNodeMetadata(field.type, options)
322
+ };
323
+ }
324
+ function resolveFormElementMetadata(element, options) {
325
+ switch (element.kind) {
326
+ case "field":
327
+ return resolveFieldMetadataNode(element, options);
328
+ case "group":
329
+ return {
330
+ ...element,
331
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
332
+ };
333
+ case "conditional":
334
+ return {
335
+ ...element,
336
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
337
+ };
338
+ default: {
339
+ const _exhaustive = element;
340
+ return _exhaustive;
341
+ }
342
+ }
343
+ }
344
+ function resolveTypeDefinitionMetadata(typeDefinition, options) {
345
+ const metadata = resolveResolvedMetadata(typeDefinition.metadata, options.policy.type, {
346
+ surface: options.surface,
347
+ declarationKind: "type",
348
+ logicalName: typeDefinition.name,
349
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
350
+ });
351
+ return {
352
+ ...typeDefinition,
353
+ ...metadata !== void 0 && { metadata },
354
+ type: resolveTypeNodeMetadata(typeDefinition.type, options)
355
+ };
356
+ }
205
357
  function resolveMetadata(explicit, policy, context) {
206
358
  return resolveResolvedMetadata(toExplicitResolvedMetadata(explicit), policy, context);
207
359
  }
@@ -211,6 +363,25 @@ function getSerializedName(logicalName, metadata) {
211
363
  function getDisplayName(metadata) {
212
364
  return metadata?.displayName?.value;
213
365
  }
366
+ function resolveFormIRMetadata(ir, options) {
367
+ const metadata = options.resolveRootTypeMetadata === false ? ir.metadata : resolveResolvedMetadata(ir.metadata, options.policy.type, {
368
+ surface: options.surface,
369
+ declarationKind: "type",
370
+ logicalName: options.rootLogicalName ?? ir.name ?? "FormSpec",
371
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
372
+ });
373
+ return {
374
+ ...ir,
375
+ ...metadata !== void 0 && { metadata },
376
+ elements: ir.elements.map((element) => resolveFormElementMetadata(element, options)),
377
+ typeRegistry: Object.fromEntries(
378
+ Object.entries(ir.typeRegistry).map(([name, definition]) => [
379
+ name,
380
+ resolveTypeDefinitionMetadata(definition, options)
381
+ ])
382
+ )
383
+ };
384
+ }
214
385
 
215
386
  // src/canonicalize/chain-dsl-canonicalizer.ts
216
387
  var CHAIN_DSL_PROVENANCE = {
@@ -232,7 +403,7 @@ function canonicalizeChainDSL(form, options) {
232
403
  const metadataPolicy = normalizeMetadataPolicy(
233
404
  options?.metadata ?? (0, import_internals._getFormSpecMetadataPolicy)(form)
234
405
  );
235
- return {
406
+ const ir = {
236
407
  kind: "form-ir",
237
408
  irVersion: import_internals.IR_VERSION,
238
409
  elements: canonicalizeElements(form.elements, metadataPolicy),
@@ -240,6 +411,13 @@ function canonicalizeChainDSL(form, options) {
240
411
  typeRegistry: {},
241
412
  provenance: CHAIN_DSL_PROVENANCE
242
413
  };
414
+ return resolveFormIRMetadata(ir, {
415
+ policy: metadataPolicy,
416
+ surface: "chain-dsl",
417
+ // Chain DSL has no root/type-metadata authoring surface, so only resolve
418
+ // field/type-registry metadata and enum-member labels here.
419
+ resolveRootTypeMetadata: false
420
+ });
243
421
  }
244
422
  function canonicalizeElements(elements, metadataPolicy) {
245
423
  return elements.map((element) => canonicalizeElement(element, metadataPolicy));
@@ -698,17 +876,25 @@ function assertNoSerializedNameCollisions(ir) {
698
876
  // src/json-schema/ir-generator.ts
699
877
  function makeContext(options) {
700
878
  const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
879
+ const rawEnumSerialization = options?.enumSerialization;
701
880
  if (!vendorPrefix.startsWith("x-")) {
702
881
  throw new Error(
703
882
  `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
704
883
  );
705
884
  }
885
+ if (rawEnumSerialization !== void 0 && rawEnumSerialization !== "enum" && rawEnumSerialization !== "oneOf") {
886
+ throw new Error(
887
+ `Invalid enumSerialization "${rawEnumSerialization}". Expected "enum" or "oneOf".`
888
+ );
889
+ }
890
+ const enumSerialization = rawEnumSerialization ?? "enum";
706
891
  return {
707
892
  defs: {},
708
893
  typeNameMap: {},
709
894
  typeRegistry: {},
710
895
  extensionRegistry: options?.extensionRegistry,
711
- vendorPrefix
896
+ vendorPrefix,
897
+ enumSerialization
712
898
  };
713
899
  }
714
900
  function generateJsonSchemaFromIR(ir, options) {
@@ -882,7 +1068,7 @@ function generateTypeNode(type, ctx) {
882
1068
  case "primitive":
883
1069
  return generatePrimitiveType(type);
884
1070
  case "enum":
885
- return generateEnumType(type);
1071
+ return generateEnumType(type, ctx);
886
1072
  case "array":
887
1073
  return generateArrayType(type, ctx);
888
1074
  case "object":
@@ -908,20 +1094,37 @@ function generatePrimitiveType(type) {
908
1094
  type: type.primitiveKind === "integer" || type.primitiveKind === "bigint" ? "integer" : type.primitiveKind
909
1095
  };
910
1096
  }
911
- function generateEnumType(type) {
912
- const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
913
- if (hasDisplayNames) {
1097
+ function generateEnumType(type, ctx) {
1098
+ if (ctx.enumSerialization === "oneOf") {
914
1099
  return {
915
- oneOf: type.members.map((m) => {
916
- const entry = { const: m.value };
917
- if (m.displayName !== void 0) {
918
- entry.title = m.displayName;
919
- }
920
- return entry;
921
- })
1100
+ oneOf: type.members.map((m) => ({
1101
+ const: m.value,
1102
+ title: m.displayName ?? String(m.value)
1103
+ }))
922
1104
  };
923
1105
  }
924
- return { enum: type.members.map((m) => m.value) };
1106
+ const schema = { enum: type.members.map((m) => m.value) };
1107
+ const displayNames = buildEnumDisplayNameExtension(type);
1108
+ if (displayNames !== void 0) {
1109
+ schema[`${ctx.vendorPrefix}-display-names`] = displayNames;
1110
+ }
1111
+ return schema;
1112
+ }
1113
+ function buildEnumDisplayNameExtension(type) {
1114
+ if (!type.members.some((member) => member.displayName !== void 0)) {
1115
+ return void 0;
1116
+ }
1117
+ const displayNames = /* @__PURE__ */ Object.create(null);
1118
+ for (const member of type.members) {
1119
+ const key = String(member.value);
1120
+ if (Object.hasOwn(displayNames, key)) {
1121
+ throw new Error(
1122
+ `Enum display-name key "${key}" is ambiguous after stringification. Use oneOf serialization for mixed string/number enum values that collide.`
1123
+ );
1124
+ }
1125
+ displayNames[key] = member.displayName ?? key;
1126
+ }
1127
+ return displayNames;
925
1128
  }
926
1129
  function generateArrayType(type, ctx) {
927
1130
  return {
@@ -1270,6 +1473,66 @@ function generateCustomType(type, ctx) {
1270
1473
  }
1271
1474
  return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1272
1475
  }
1476
+ var JSON_SCHEMA_STRUCTURAL_KEYWORDS = /* @__PURE__ */ new Set([
1477
+ "$schema",
1478
+ "$ref",
1479
+ "$defs",
1480
+ "$id",
1481
+ "$anchor",
1482
+ "$dynamicRef",
1483
+ "$dynamicAnchor",
1484
+ "$vocabulary",
1485
+ "$comment",
1486
+ "type",
1487
+ "enum",
1488
+ "const",
1489
+ "properties",
1490
+ "patternProperties",
1491
+ "additionalProperties",
1492
+ "required",
1493
+ "items",
1494
+ "prefixItems",
1495
+ "additionalItems",
1496
+ "contains",
1497
+ "allOf",
1498
+ "oneOf",
1499
+ "anyOf",
1500
+ "not",
1501
+ "if",
1502
+ "then",
1503
+ "else",
1504
+ "minimum",
1505
+ "maximum",
1506
+ "exclusiveMinimum",
1507
+ "exclusiveMaximum",
1508
+ "multipleOf",
1509
+ "minLength",
1510
+ "maxLength",
1511
+ "pattern",
1512
+ "minItems",
1513
+ "maxItems",
1514
+ "uniqueItems",
1515
+ "minProperties",
1516
+ "maxProperties",
1517
+ "minContains",
1518
+ "maxContains",
1519
+ "format",
1520
+ "title",
1521
+ "description",
1522
+ "default",
1523
+ "deprecated",
1524
+ "readOnly",
1525
+ "writeOnly",
1526
+ "examples",
1527
+ "dependentRequired",
1528
+ "dependentSchemas",
1529
+ "propertyNames",
1530
+ "unevaluatedItems",
1531
+ "unevaluatedProperties",
1532
+ "contentEncoding",
1533
+ "contentMediaType",
1534
+ "contentSchema"
1535
+ ]);
1273
1536
  function applyCustomConstraint(schema, constraint, ctx) {
1274
1537
  const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1275
1538
  if (registration === void 0) {
@@ -1277,12 +1540,25 @@ function applyCustomConstraint(schema, constraint, ctx) {
1277
1540
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1278
1541
  );
1279
1542
  }
1280
- assignVendorPrefixedExtensionKeywords(
1281
- schema,
1282
- registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
1283
- ctx.vendorPrefix,
1284
- `custom constraint "${constraint.constraintId}"`
1285
- );
1543
+ const extensionSchema = registration.toJsonSchema(constraint.payload, ctx.vendorPrefix);
1544
+ if (registration.emitsVocabularyKeywords) {
1545
+ const target = schema;
1546
+ for (const [key, value] of Object.entries(extensionSchema)) {
1547
+ if (JSON_SCHEMA_STRUCTURAL_KEYWORDS.has(key)) {
1548
+ throw new Error(
1549
+ `Custom constraint "${constraint.constraintId}" with emitsVocabularyKeywords must not overwrite standard JSON Schema keyword "${key}"`
1550
+ );
1551
+ }
1552
+ target[key] = value;
1553
+ }
1554
+ } else {
1555
+ assignVendorPrefixedExtensionKeywords(
1556
+ schema,
1557
+ extensionSchema,
1558
+ ctx.vendorPrefix,
1559
+ `custom constraint "${constraint.constraintId}"`
1560
+ );
1561
+ }
1286
1562
  }
1287
1563
  function applyCustomAnnotation(schema, annotation, ctx) {
1288
1564
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -1314,11 +1590,17 @@ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPr
1314
1590
 
1315
1591
  // src/json-schema/generator.ts
1316
1592
  function generateJsonSchema(form, options) {
1593
+ const metadata = options?.metadata;
1594
+ const vendorPrefix = options?.vendorPrefix;
1595
+ const enumSerialization = options?.enumSerialization;
1317
1596
  const ir = canonicalizeChainDSL(
1318
1597
  form,
1319
- options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
1598
+ metadata !== void 0 ? { metadata } : void 0
1320
1599
  );
1321
- const internalOptions = options?.vendorPrefix === void 0 ? void 0 : { vendorPrefix: options.vendorPrefix };
1600
+ const internalOptions = vendorPrefix === void 0 && enumSerialization === void 0 ? void 0 : {
1601
+ ...vendorPrefix !== void 0 && { vendorPrefix },
1602
+ ...enumSerialization !== void 0 && { enumSerialization }
1603
+ };
1322
1604
  return generateJsonSchemaFromIR(ir, internalOptions);
1323
1605
  }
1324
1606