@contractkit/openapi-to-ck 0.10.2 → 0.12.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.
Files changed (41) hide show
  1. package/.turbo/turbo-build$colon$ci.log +7 -7
  2. package/.turbo/turbo-test$colon$ci.log +24 -20
  3. package/CHANGELOG.md +172 -0
  4. package/README.md +30 -8
  5. package/dist/ast-to-ck.d.ts +32 -16
  6. package/dist/ast-to-ck.d.ts.map +1 -1
  7. package/dist/{chunk-JPI3AQ7V.js → chunk-U7V7LFBV.js} +219 -391
  8. package/dist/chunk-U7V7LFBV.js.map +1 -0
  9. package/dist/convert.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/normalize.d.ts +6 -2
  12. package/dist/normalize.d.ts.map +1 -1
  13. package/dist/paths-to-ast.d.ts +2 -0
  14. package/dist/paths-to-ast.d.ts.map +1 -1
  15. package/dist/plugin.d.ts.map +1 -1
  16. package/dist/plugin.js +18 -3
  17. package/dist/plugin.js.map +1 -1
  18. package/dist/schema-to-ast.d.ts +13 -1
  19. package/dist/schema-to-ast.d.ts.map +1 -1
  20. package/dist/tag-splitter.d.ts.map +1 -1
  21. package/dist/types.d.ts +36 -0
  22. package/dist/types.d.ts.map +1 -1
  23. package/package.json +4 -5
  24. package/src/ast-to-ck.ts +29 -453
  25. package/src/convert.ts +57 -3
  26. package/src/normalize.ts +87 -11
  27. package/src/paths-to-ast.ts +92 -11
  28. package/src/plugin.ts +17 -2
  29. package/src/schema-to-ast.ts +71 -7
  30. package/src/tag-splitter.ts +21 -16
  31. package/src/types.ts +36 -0
  32. package/tests/__snapshots__/kitchen-sink.ck +102 -0
  33. package/tests/ast-to-ck.test.ts +34 -17
  34. package/tests/component-refs.test.ts +114 -0
  35. package/tests/coverage.test.ts +246 -0
  36. package/tests/error-responses.test.ts +94 -0
  37. package/tests/fixtures/kitchen-sink-3.1.json +100 -0
  38. package/tests/helpers.ts +40 -0
  39. package/tests/kitchen-sink.test.ts +116 -0
  40. package/tests/schema-to-ast.test.ts +11 -2
  41. package/dist/chunk-JPI3AQ7V.js.map +0 -1
@@ -4,15 +4,65 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
  // src/normalize.ts
5
5
  function normalize(doc, warnings) {
6
6
  const version = detectVersion(doc);
7
- if (version === "2.0") {
8
- return normalizeSwagger2(doc, warnings);
9
- }
10
- if (version === "3.0") {
11
- return normalizeOas30(doc, warnings);
12
- }
13
- return doc;
7
+ const normalized = version === "2.0" ? normalizeSwagger2(doc, warnings) : version === "3.0" ? normalizeOas30(doc, warnings) : doc;
8
+ dereferenceComponents(normalized, warnings);
9
+ return normalized;
14
10
  }
15
11
  __name(normalize, "normalize");
12
+ var DEREF_SECTIONS = [
13
+ "parameters",
14
+ "requestBodies",
15
+ "responses",
16
+ "headers"
17
+ ];
18
+ var MAX_REF_DEPTH = 10;
19
+ function dereferenceComponents(doc, warnings) {
20
+ const components = doc.components;
21
+ const resolve = /* @__PURE__ */ __name((node, path, depth = 0) => {
22
+ if (!node || typeof node !== "object") return node;
23
+ if (Array.isArray(node)) return node.map((item) => resolve(item, path, depth));
24
+ const obj = node;
25
+ const ref = obj.$ref;
26
+ if (typeof ref === "string") {
27
+ const match = /^#\/components\/([^/]+)\/(.+)$/.exec(ref);
28
+ const section = match?.[1];
29
+ if (section && section !== "schemas" && DEREF_SECTIONS.includes(section)) {
30
+ if (depth >= MAX_REF_DEPTH) {
31
+ warnings.warn(path, `$ref chain too deep to resolve: ${ref}`);
32
+ return obj;
33
+ }
34
+ const target = components?.[section]?.[decodeRefToken(match[2])];
35
+ if (target === void 0) {
36
+ warnings.warn(path, `unresolved $ref '${ref}' \u2014 the component is not defined`);
37
+ return obj;
38
+ }
39
+ const siblings = {
40
+ ...obj
41
+ };
42
+ delete siblings.$ref;
43
+ const resolved = resolve(target, path, depth + 1);
44
+ return {
45
+ ...resolved,
46
+ ...siblings
47
+ };
48
+ }
49
+ return obj;
50
+ }
51
+ const out = {};
52
+ for (const [key, value] of Object.entries(obj)) {
53
+ out[key] = key === "schema" || key === "schemas" ? value : resolve(value, `${path}/${key}`, depth);
54
+ }
55
+ return out;
56
+ }, "resolve");
57
+ for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
58
+ doc.paths[path] = resolve(pathItem, `#/paths/${path}`);
59
+ }
60
+ }
61
+ __name(dereferenceComponents, "dereferenceComponents");
62
+ function decodeRefToken(token) {
63
+ return decodeURIComponent(token).replace(/~1/g, "/").replace(/~0/g, "~");
64
+ }
65
+ __name(decodeRefToken, "decodeRefToken");
16
66
  function detectVersion(doc) {
17
67
  if (typeof doc.swagger === "string" && doc.swagger.startsWith("2")) return "2.0";
18
68
  if (typeof doc.openapi === "string") {
@@ -82,7 +132,8 @@ function normalizePathItem2(pathItem, globalConsumes, globalProduces, warnings)
82
132
  "patch",
83
133
  "delete",
84
134
  "head",
85
- "options"
135
+ "options",
136
+ "trace"
86
137
  ];
87
138
  const normalized = {};
88
139
  const pathParams = pathItem.parameters ?? [];
@@ -281,7 +332,8 @@ function normalizePathItemSchemas(pathItem) {
281
332
  "patch",
282
333
  "delete",
283
334
  "head",
284
- "options"
335
+ "options",
336
+ "trace"
285
337
  ];
286
338
  for (const method of methods) {
287
339
  const op = pathItem[method];
@@ -419,14 +471,20 @@ var LOC = {
419
471
  };
420
472
  var FORMAT_TO_SCALAR = {
421
473
  email: "email",
474
+ "idn-email": "email",
422
475
  uri: "url",
476
+ "uri-reference": "url",
477
+ iri: "url",
478
+ "iri-reference": "url",
423
479
  url: "url",
424
480
  uuid: "uuid",
425
481
  date: "date",
426
482
  "date-time": "datetime",
427
483
  time: "time",
484
+ duration: "duration",
428
485
  binary: "binary",
429
- int64: "bigint"
486
+ int64: "bigint",
487
+ decimal: "decimal"
430
488
  };
431
489
  function schemasToModels(schemas, ctx) {
432
490
  const models = [];
@@ -472,6 +530,11 @@ function schemaToModel(name, schema, ctx) {
472
530
  kind: "model",
473
531
  name,
474
532
  fields,
533
+ // `additionalProperties: true` says unknown keys are allowed, which is `mode(loose)`.
534
+ // `false` and absent both match `.ck`'s `strict` default, so neither needs a mode.
535
+ ...schema.additionalProperties === true ? {
536
+ mode: "loose"
537
+ } : {},
475
538
  description,
476
539
  loc: LOC
477
540
  };
@@ -488,10 +551,11 @@ function schemaToModel(name, schema, ctx) {
488
551
  }
489
552
  __name(schemaToModel, "schemaToModel");
490
553
  function schemaToTypeNode(schema, ctx) {
554
+ warnUnrepresentableConstraints(schema, ctx);
491
555
  if (schema.$ref) {
492
556
  const refName = extractRefName(schema.$ref);
493
557
  if (refName) {
494
- if (ctx.circularRefs.has(refName)) {
558
+ if (ctx.insideModel && ctx.circularRefs.has(refName)) {
495
559
  return {
496
560
  kind: "lazy",
497
561
  inner: {
@@ -607,6 +671,17 @@ function schemaToTypeNode(schema, ctx) {
607
671
  }
608
672
  __name(schemaToTypeNode, "schemaToTypeNode");
609
673
  function stringSchemaToType(schema) {
674
+ if (schema.format === "decimal") {
675
+ const mods2 = {};
676
+ if (schema["x-contractkit-min"] !== void 0) mods2.min = schema["x-contractkit-min"];
677
+ if (schema["x-contractkit-max"] !== void 0) mods2.max = schema["x-contractkit-max"];
678
+ if (schema["x-contractkit-scale"] !== void 0) mods2.scale = schema["x-contractkit-scale"];
679
+ return {
680
+ kind: "scalar",
681
+ name: "decimal",
682
+ ...mods2
683
+ };
684
+ }
610
685
  if (schema.format) {
611
686
  const scalarName = FORMAT_TO_SCALAR[schema.format];
612
687
  if (scalarName) {
@@ -623,7 +698,7 @@ function stringSchemaToType(schema) {
623
698
  if (schema.minLength !== void 0) mods.min = schema.minLength;
624
699
  if (schema.maxLength !== void 0) mods.max = schema.maxLength;
625
700
  }
626
- if (schema.pattern) mods.regex = `/${schema.pattern}/`;
701
+ if (schema.pattern) mods.regex = schema.pattern;
627
702
  if (schema.format && !FORMAT_TO_SCALAR[schema.format]) mods.format = schema.format;
628
703
  return {
629
704
  kind: "scalar",
@@ -645,6 +720,16 @@ function integerSchemaToType(schema) {
645
720
  }
646
721
  __name(integerSchemaToType, "integerSchemaToType");
647
722
  function numberSchemaToType(schema) {
723
+ if (schema.format === "decimal") {
724
+ const mods2 = {};
725
+ if (schema.minimum !== void 0) mods2.min = String(schema.minimum);
726
+ if (schema.maximum !== void 0) mods2.max = String(schema.maximum);
727
+ return {
728
+ kind: "scalar",
729
+ name: "decimal",
730
+ ...mods2
731
+ };
732
+ }
648
733
  const mods = {};
649
734
  if (schema.minimum !== void 0) mods.min = schema.minimum;
650
735
  if (schema.maximum !== void 0) mods.max = schema.maximum;
@@ -793,12 +878,24 @@ function toDiscriminatedUnion(schemas, discriminator, ctx) {
793
878
  };
794
879
  }
795
880
  __name(toDiscriminatedUnion, "toDiscriminatedUnion");
881
+ var UNREPRESENTABLE_KEYWORDS = [
882
+ "exclusiveMinimum",
883
+ "exclusiveMaximum",
884
+ "multipleOf",
885
+ "uniqueItems"
886
+ ];
796
887
  function warnUnsupported(schema, ctx) {
797
888
  if (schema.xml) ctx.warnings.warn(ctx.path, "xml metadata is not supported, skipping");
798
889
  if (schema.externalDocs) ctx.warnings.info(ctx.path, "externalDocs is not supported, skipping");
799
890
  if (schema.not) ctx.warnings.warn(ctx.path, "not keyword is not supported, skipping");
800
891
  }
801
892
  __name(warnUnsupported, "warnUnsupported");
893
+ function warnUnrepresentableConstraints(schema, ctx) {
894
+ for (const keyword of UNREPRESENTABLE_KEYWORDS) {
895
+ if (schema[keyword] !== void 0) ctx.warnings.warn(ctx.path, `${keyword} has no .ck equivalent, dropping the constraint`);
896
+ }
897
+ }
898
+ __name(warnUnrepresentableConstraints, "warnUnrepresentableConstraints");
802
899
  function extractInlineModel(schema, suggestedName, ctx) {
803
900
  if (schema.$ref) {
804
901
  return {
@@ -806,7 +903,10 @@ function extractInlineModel(schema, suggestedName, ctx) {
806
903
  };
807
904
  }
808
905
  if (schema.properties || schema.type === "object" && schema.additionalProperties === void 0) {
809
- const fields = schemaPropertiesToFields(schema, ctx);
906
+ const fields = schemaPropertiesToFields(schema, {
907
+ ...ctx,
908
+ insideModel: true
909
+ });
810
910
  const model = {
811
911
  kind: "model",
812
912
  name: suggestedName,
@@ -829,10 +929,11 @@ function extractInlineModel(schema, suggestedName, ctx) {
829
929
  __name(extractInlineModel, "extractInlineModel");
830
930
  function sanitizeName(name, warnings) {
831
931
  const cleaned = name.replace(/[^a-zA-Z0-9_$]/g, " ").split(/\s+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
832
- if (cleaned !== name) {
833
- warnings.info(`#/components/schemas/${name}`, `Schema name sanitized: "${name}" \u2192 "${cleaned}"`);
932
+ const safe = /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
933
+ if (safe !== name) {
934
+ warnings.info(`#/components/schemas/${name}`, `Schema name sanitized: "${name}" \u2192 "${safe}"`);
834
935
  }
835
- return cleaned || "UnnamedSchema";
936
+ return safe || "UnnamedSchema";
836
937
  }
837
938
  __name(sanitizeName, "sanitizeName");
838
939
 
@@ -841,6 +942,11 @@ var LOC2 = {
841
942
  file: "",
842
943
  line: 0
843
944
  };
945
+ var MIME_RE = /^[a-z0-9][a-z0-9.+_-]*\/[a-z0-9][a-z0-9.+_-]*$/i;
946
+ function toNameText(summary) {
947
+ return summary.replace(/[}#]/g, " ").replace(/\s+/g, " ").trim();
948
+ }
949
+ __name(toNameText, "toNameText");
844
950
  var HTTP_METHODS = [
845
951
  "get",
846
952
  "post",
@@ -848,6 +954,11 @@ var HTTP_METHODS = [
848
954
  "patch",
849
955
  "delete"
850
956
  ];
957
+ var UNSUPPORTED_METHODS = [
958
+ "head",
959
+ "options",
960
+ "trace"
961
+ ];
851
962
  function pathsToRoutes(doc, ctx) {
852
963
  const routes = [];
853
964
  const routeTags = /* @__PURE__ */ new Map();
@@ -870,6 +981,11 @@ function pathItemToRoute(path, pathItem, ctx) {
870
981
  const operations = [];
871
982
  let primaryTag = "default";
872
983
  const pathParams = (pathItem.parameters ?? []).filter((p) => p.in === "path");
984
+ for (const method of UNSUPPORTED_METHODS) {
985
+ if (pathItem[method]) {
986
+ ctx.warnings.warn(`#/paths/${encodePathSegment2(path)}/${method}`, `\`${method}\` operations have no .ck equivalent; dropped`);
987
+ }
988
+ }
873
989
  for (const method of HTTP_METHODS) {
874
990
  const op = pathItem[method];
875
991
  if (!op) continue;
@@ -912,6 +1028,11 @@ function operationToNode(method, op, path, ctx) {
912
1028
  if (op.operationId) {
913
1029
  node.sdk = op.operationId;
914
1030
  }
1031
+ if (op.summary) {
1032
+ const name = toNameText(op.summary);
1033
+ if (name) node.name = name;
1034
+ else ctx.warnings.warn(`${pathPrefix}/summary`, "summary has no content `.ck` can carry as a name; dropped");
1035
+ }
915
1036
  if (op.description && ctx.includeComments) {
916
1037
  node.description = op.description;
917
1038
  }
@@ -923,10 +1044,16 @@ function operationToNode(method, op, path, ctx) {
923
1044
  const queryParams = [];
924
1045
  const headerParams = [];
925
1046
  for (const param of op.parameters ?? []) {
1047
+ if (!param?.name) {
1048
+ ctx.warnings.warn(`${pathPrefix}/parameters`, "skipped a parameter with no name (an unresolved $ref?)");
1049
+ continue;
1050
+ }
926
1051
  if (param.in === "query") {
927
1052
  queryParams.push(parameterToNode(param, schemaCtx));
928
1053
  } else if (param.in === "header") {
929
1054
  headerParams.push(parameterToNode(param, schemaCtx));
1055
+ } else if (param.in === "cookie") {
1056
+ ctx.warnings.warn(`${pathPrefix}/parameters/${param.name}`, "cookie parameters have no `.ck` equivalent; dropped");
930
1057
  }
931
1058
  }
932
1059
  if (queryParams.length > 0) {
@@ -946,13 +1073,17 @@ function operationToNode(method, op, path, ctx) {
946
1073
  }
947
1074
  const responses = op.responses ?? {};
948
1075
  for (const [code, resp] of Object.entries(responses)) {
1076
+ if (!/^\d{3}$/.test(code)) {
1077
+ ctx.warnings.warn(`${pathPrefix}/responses/${code}`, `response key '${code}' is not a numeric status code; dropped`);
1078
+ continue;
1079
+ }
949
1080
  const statusCode = parseInt(code, 10);
950
- if (isNaN(statusCode)) continue;
951
1081
  const respNode = responseToNode(statusCode, resp, op.operationId ?? `${method}${toPascalCase(path)}`, schemaCtx, ctx);
952
1082
  node.responses.push(respNode);
953
1083
  }
954
- if (op.security !== void 0) {
955
- node.security = convertSecurity(op.security);
1084
+ const security = op.security ?? ctx.globalSecurity;
1085
+ if (security !== void 0) {
1086
+ node.security = convertSecurity(security);
956
1087
  }
957
1088
  return node;
958
1089
  }
@@ -1011,14 +1142,13 @@ __name(parameterToNode, "parameterToNode");
1011
1142
  function requestBodyToNode(reqBody, operationName, schemaCtx, ctx) {
1012
1143
  const content = reqBody.content;
1013
1144
  if (!content) return void 0;
1014
- const supported = /* @__PURE__ */ new Set([
1015
- "application/json",
1016
- "application/x-www-form-urlencoded",
1017
- "multipart/form-data"
1018
- ]);
1019
1145
  const bodies = [];
1020
1146
  for (const [contentType, mediaType] of Object.entries(content)) {
1021
- if (!supported.has(contentType) || !mediaType?.schema) continue;
1147
+ if (!MIME_RE.test(contentType)) {
1148
+ ctx.warnings.warn(`${schemaCtx.path}/requestBody/content`, `content type '${contentType}' is not a plain type/subtype; skipped`);
1149
+ continue;
1150
+ }
1151
+ if (!mediaType?.schema) continue;
1022
1152
  const { typeNode, model } = extractInlineModel(mediaType.schema, `${toPascalCase(operationName)}Request`, schemaCtx);
1023
1153
  if (model) {
1024
1154
  ctx.extractedModels.push(model);
@@ -1034,14 +1164,24 @@ function requestBodyToNode(reqBody, operationName, schemaCtx, ctx) {
1034
1164
  };
1035
1165
  }
1036
1166
  __name(requestBodyToNode, "requestBodyToNode");
1167
+ function shouldDocument(statusCode, braced, resp, ctx) {
1168
+ if (!braced) return false;
1169
+ if (resp["x-contractkit-emit"] === "documented") return true;
1170
+ return statusCode >= 400 && ctx.errorResponses === "documented";
1171
+ }
1172
+ __name(shouldDocument, "shouldDocument");
1037
1173
  function responseToNode(statusCode, resp, operationName, schemaCtx, ctx) {
1038
1174
  const headers = convertResponseHeaders(resp.headers, schemaCtx);
1175
+ const documented = /* @__PURE__ */ __name((braced) => shouldDocument(statusCode, braced, resp, ctx) ? {
1176
+ emit: "documented"
1177
+ } : {}, "documented");
1039
1178
  const empty = /* @__PURE__ */ __name(() => ({
1040
1179
  statusCode,
1041
1180
  bodies: [],
1042
1181
  ...headers ? {
1043
1182
  headers,
1044
- hasBlock: true
1183
+ hasBlock: true,
1184
+ ...documented(true)
1045
1185
  } : {}
1046
1186
  }), "empty");
1047
1187
  if (!resp.content) return empty();
@@ -1063,7 +1203,8 @@ function responseToNode(statusCode, resp, operationName, schemaCtx, ctx) {
1063
1203
  hasBlock: true,
1064
1204
  ...headers ? {
1065
1205
  headers
1066
- } : {}
1206
+ } : {},
1207
+ ...documented(true)
1067
1208
  };
1068
1209
  }
1069
1210
  __name(responseToNode, "responseToNode");
@@ -1103,7 +1244,9 @@ function makeSchemaCtx(ctx, path) {
1103
1244
  includeComments: ctx.includeComments,
1104
1245
  namedSchemas: ctx.namedSchemas,
1105
1246
  extractedModels: ctx.extractedModels,
1106
- inlineCounter: 0
1247
+ inlineCounter: 0,
1248
+ // A response body, request body, param or header names an already-imported model.
1249
+ insideModel: false
1107
1250
  };
1108
1251
  }
1109
1252
  __name(makeSchemaCtx, "makeSchemaCtx");
@@ -1224,20 +1367,16 @@ function collectRouteRefs(route, refs) {
1224
1367
  }
1225
1368
  __name(collectRouteRefs, "collectRouteRefs");
1226
1369
  function collectParamSourceRefs(source, refs) {
1227
- if (typeof source === "string") {
1228
- refs.add(source);
1229
- return;
1230
- }
1231
- if (Array.isArray(source)) {
1232
- for (const param of source) {
1233
- if (param && typeof param === "object" && "type" in param) {
1234
- collectTypeRefs(param.type, refs);
1235
- }
1236
- }
1237
- return;
1238
- }
1239
- if (source && typeof source === "object" && "kind" in source) {
1240
- collectTypeRefs(source, refs);
1370
+ switch (source.kind) {
1371
+ case "ref":
1372
+ refs.add(source.name);
1373
+ return;
1374
+ case "params":
1375
+ for (const param of source.nodes) collectTypeRefs(param.type, refs);
1376
+ return;
1377
+ case "type":
1378
+ collectTypeRefs(source.node, refs);
1379
+ return;
1241
1380
  }
1242
1381
  }
1243
1382
  __name(collectParamSourceRefs, "collectParamSourceRefs");
@@ -1284,347 +1423,12 @@ function sanitizeFilename(tag) {
1284
1423
  __name(sanitizeFilename, "sanitizeFilename");
1285
1424
 
1286
1425
  // src/ast-to-ck.ts
1287
- var INDENT = " ";
1288
- var IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/;
1289
- function singleLineComment(text) {
1290
- return text.replace(/\s+/g, " ").trim();
1291
- }
1292
- __name(singleLineComment, "singleLineComment");
1293
- function quoteEnumValue(value) {
1294
- if (IDENT_RE.test(value)) return value;
1295
- if (!value.includes('"')) return `"${value}"`;
1296
- if (!value.includes("'")) return `'${value}'`;
1297
- return `"${value}"`;
1298
- }
1299
- __name(quoteEnumValue, "quoteEnumValue");
1300
- function astToCk(root, options = {}) {
1301
- const { includeComments = true } = options;
1302
- const ctx = {
1303
- includeComments
1304
- };
1305
- const parts = [];
1306
- const optionsBlock = serializeOptions(root);
1307
- if (optionsBlock) parts.push(optionsBlock);
1308
- for (const model of root.models) {
1309
- parts.push(serializeModel(model, ctx));
1310
- }
1311
- for (const route of root.routes) {
1312
- parts.push(serializeRoute(route, ctx));
1313
- }
1314
- return parts.join("\n\n") + "\n";
1426
+ import { printCk, printType } from "@contractkit/core";
1427
+ function astToCk(root, _options = {}) {
1428
+ return printCk(root);
1315
1429
  }
1316
1430
  __name(astToCk, "astToCk");
1317
- function serializeOptions(root) {
1318
- const hasKeys = Object.keys(root.meta).length > 0;
1319
- const hasServices = root.services && Object.keys(root.services).length > 0;
1320
- const hasSecurity = root.security !== void 0;
1321
- if (!hasKeys && !hasServices && !hasSecurity) return null;
1322
- const lines = [
1323
- "options {"
1324
- ];
1325
- if (hasKeys) {
1326
- lines.push(`${INDENT}keys: {`);
1327
- for (const [key, value] of Object.entries(root.meta)) {
1328
- lines.push(`${INDENT}${INDENT}${key}: ${value}`);
1329
- }
1330
- lines.push(`${INDENT}}`);
1331
- }
1332
- if (hasServices) {
1333
- lines.push(`${INDENT}services: {`);
1334
- for (const [name, path] of Object.entries(root.services)) {
1335
- lines.push(`${INDENT}${INDENT}${name}: "${path}"`);
1336
- }
1337
- lines.push(`${INDENT}}`);
1338
- }
1339
- if (hasSecurity) {
1340
- lines.push(`${INDENT}security: {`);
1341
- if (root.security === "none") {
1342
- lines.push(`${INDENT}${INDENT}none`);
1343
- } else {
1344
- const sec = root.security;
1345
- if (sec.policy !== void 0) {
1346
- const value = sec.policy === false ? "none" : sec.policy;
1347
- lines.push(`${INDENT}${INDENT}policy: ${value}`);
1348
- }
1349
- }
1350
- lines.push(`${INDENT}}`);
1351
- }
1352
- lines.push("}");
1353
- return lines.join("\n");
1354
- }
1355
- __name(serializeOptions, "serializeOptions");
1356
- function serializeModel(model, ctx) {
1357
- const parts = [];
1358
- const prefixes = [];
1359
- if (model.inputCase && model.inputCase !== "camel") {
1360
- prefixes.push(`format(input=${model.inputCase})`);
1361
- }
1362
- if (model.mode && model.mode !== "strict") {
1363
- prefixes.push(`mode(${model.mode})`);
1364
- }
1365
- if (model.deprecated) {
1366
- prefixes.push("deprecated");
1367
- }
1368
- const prefix = prefixes.length > 0 ? prefixes.join(" ") + " " : "";
1369
- const comment = ctx.includeComments && model.description ? ` # ${singleLineComment(model.description)}` : "";
1370
- if (model.type) {
1371
- parts.push(`contract ${prefix}${model.name}: ${serializeType(model.type)}${comment}`);
1372
- return parts.join("");
1373
- }
1374
- if (model.bases && model.bases.length > 0) {
1375
- parts.push(`contract ${prefix}${model.name}: ${model.bases.join(" & ")} & {${comment}`);
1376
- } else {
1377
- parts.push(`contract ${prefix}${model.name}: {${comment}`);
1378
- }
1379
- for (const field of model.fields) {
1380
- parts.push(serializeField(field, 1, ctx));
1381
- }
1382
- parts.push("}");
1383
- return parts.join("\n");
1384
- }
1385
- __name(serializeModel, "serializeModel");
1386
- function serializeField(field, depth, ctx) {
1387
- const indent = INDENT.repeat(depth);
1388
- const optional = field.optional ? "?" : "";
1389
- const visibility = field.visibility !== "normal" ? `${field.visibility} ` : "";
1390
- const deprecated = field.deprecated ? "deprecated " : "";
1391
- let typeStr = serializeType(field.type);
1392
- if (field.nullable && !typeContainsNull(field.type)) {
1393
- typeStr = `${typeStr} | null`;
1394
- }
1395
- const defaultVal = field.default !== void 0 ? ` = ${serializeDefault(field.default)}` : "";
1396
- const comment = ctx.includeComments && field.description ? ` # ${singleLineComment(field.description)}` : "";
1397
- return `${indent}${field.name}${optional}: ${deprecated}${visibility}${typeStr}${defaultVal}${comment}`;
1398
- }
1399
- __name(serializeField, "serializeField");
1400
- function typeContainsNull(type) {
1401
- if (type.kind === "scalar" && type.name === "null") return true;
1402
- if (type.kind === "union") return type.members.some(typeContainsNull);
1403
- return false;
1404
- }
1405
- __name(typeContainsNull, "typeContainsNull");
1406
- function serializeDefault(value) {
1407
- if (typeof value === "string") {
1408
- if (/^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(value)) return value;
1409
- return `"${value}"`;
1410
- }
1411
- return String(value);
1412
- }
1413
- __name(serializeDefault, "serializeDefault");
1414
- function serializeType(type) {
1415
- switch (type.kind) {
1416
- case "scalar":
1417
- return serializeScalar(type);
1418
- case "array":
1419
- return serializeArray(type);
1420
- case "tuple":
1421
- return `tuple(${type.items.map(serializeType).join(", ")})`;
1422
- case "record":
1423
- return `record(${serializeType(type.key)}, ${serializeType(type.value)})`;
1424
- case "enum":
1425
- return `enum(${type.values.map(quoteEnumValue).join(", ")})`;
1426
- case "literal":
1427
- return serializeLiteral(type);
1428
- case "union":
1429
- return type.members.map(serializeType).join(" | ");
1430
- case "discriminatedUnion":
1431
- return `discriminated(by=${type.discriminator}, ${type.members.map(serializeType).join(" | ")})`;
1432
- case "intersection":
1433
- return type.members.map(serializeType).join(" & ");
1434
- case "ref":
1435
- return type.name;
1436
- case "inlineObject":
1437
- return serializeInlineObject(type);
1438
- case "lazy":
1439
- return `lazy(${serializeType(type.inner)})`;
1440
- }
1441
- }
1442
- __name(serializeType, "serializeType");
1443
- function serializeScalar(type) {
1444
- const args = [];
1445
- if (type.len !== void 0) args.push(`length=${type.len}`);
1446
- if (type.min !== void 0) args.push(typeof type.min === "string" ? `min="${type.min}"` : `min=${type.min}`);
1447
- if (type.max !== void 0) args.push(typeof type.max === "string" ? `max="${type.max}"` : `max=${type.max}`);
1448
- if (type.regex !== void 0) args.push(`regex=${type.regex}`);
1449
- if (type.format !== void 0) args.push(`format=${type.format}`);
1450
- if (args.length === 0) return type.name;
1451
- return `${type.name}(${args.join(", ")})`;
1452
- }
1453
- __name(serializeScalar, "serializeScalar");
1454
- function serializeArray(type) {
1455
- const args = [
1456
- serializeType(type.item)
1457
- ];
1458
- if (type.min !== void 0) args.push(`min=${type.min}`);
1459
- if (type.max !== void 0) args.push(`max=${type.max}`);
1460
- return `array(${args.join(", ")})`;
1461
- }
1462
- __name(serializeArray, "serializeArray");
1463
- function serializeLiteral(type) {
1464
- if (typeof type.value === "string") return `literal("${type.value}")`;
1465
- return `literal(${type.value})`;
1466
- }
1467
- __name(serializeLiteral, "serializeLiteral");
1468
- function serializeInlineObject(type) {
1469
- const modePrefix = type.mode ? `mode(${type.mode}) ` : "";
1470
- if (type.fields.length === 0) return `${modePrefix}{}`;
1471
- const lines = [
1472
- `${modePrefix}{`
1473
- ];
1474
- for (const field of type.fields) {
1475
- lines.push(serializeField(field, 2, {
1476
- includeComments: true
1477
- }));
1478
- }
1479
- lines.push(`${INDENT}}`);
1480
- return lines.join("\n");
1481
- }
1482
- __name(serializeInlineObject, "serializeInlineObject");
1483
- function serializeRoute(route, ctx) {
1484
- const lines = [];
1485
- const modStr = serializeModifiers(route.modifiers);
1486
- const comment = ctx.includeComments && route.description ? ` # ${singleLineComment(route.description)}` : "";
1487
- lines.push(`operation${modStr} ${route.path}: {${comment}`);
1488
- if (route.params) {
1489
- serializeParamSource(lines, "params", route.params, route.paramsMode, 1, ctx);
1490
- }
1491
- if (route.security !== void 0) {
1492
- serializeSecurityBlock(lines, route.security, 1, ctx);
1493
- }
1494
- for (const op of route.operations) {
1495
- serializeOperation(lines, op, 1, ctx);
1496
- }
1497
- lines.push("}");
1498
- return lines.join("\n");
1499
- }
1500
- __name(serializeRoute, "serializeRoute");
1501
- function serializeOperation(lines, op, depth, ctx) {
1502
- const indent = INDENT.repeat(depth);
1503
- const modStr = serializeModifiers(op.modifiers);
1504
- const comment = ctx.includeComments && op.description ? ` # ${singleLineComment(op.description)}` : "";
1505
- lines.push(`${indent}${op.method}${modStr}: {${comment}`);
1506
- const inner = INDENT.repeat(depth + 1);
1507
- if (op.service) {
1508
- lines.push(`${inner}service: ${op.service}`);
1509
- }
1510
- if (op.sdk) {
1511
- lines.push(`${inner}sdk: ${op.sdk}`);
1512
- }
1513
- if (op.signature) {
1514
- const sigComment = ctx.includeComments && op.signatureDescription ? ` # ${singleLineComment(op.signatureDescription)}` : "";
1515
- if (op.signaturePolicy) {
1516
- lines.push(`${inner}signature: {`);
1517
- lines.push(`${inner} options: ${op.signature}${sigComment}`);
1518
- lines.push(`${inner} policy: ${op.signaturePolicy}`);
1519
- lines.push(`${inner}}`);
1520
- } else {
1521
- lines.push(`${inner}signature: ${op.signature}${sigComment}`);
1522
- }
1523
- }
1524
- if (op.security !== void 0) {
1525
- serializeSecurityBlock(lines, op.security, depth + 1, ctx);
1526
- }
1527
- if (op.query) {
1528
- serializeParamSource(lines, "query", op.query, op.queryMode, depth + 1, ctx);
1529
- }
1530
- if (op.headers) {
1531
- serializeParamSource(lines, "headers", op.headers, op.headersMode, depth + 1, ctx);
1532
- }
1533
- if (op.request) {
1534
- serializeRequest(lines, op.request, depth + 1);
1535
- }
1536
- if (op.responses.length > 0) {
1537
- serializeResponses(lines, op.responses, depth + 1);
1538
- }
1539
- lines.push(`${indent}}`);
1540
- return lines;
1541
- }
1542
- __name(serializeOperation, "serializeOperation");
1543
- function serializeModifiers(modifiers) {
1544
- if (!modifiers || modifiers.length === 0) return "";
1545
- return `(${modifiers.join(", ")})`;
1546
- }
1547
- __name(serializeModifiers, "serializeModifiers");
1548
- function serializeParamSource(lines, keyword, source, mode, depth, ctx) {
1549
- const indent = INDENT.repeat(depth);
1550
- if (source.kind === "ref") {
1551
- lines.push(`${indent}${keyword}: ${source.name}`);
1552
- return;
1553
- }
1554
- if (source.kind === "type") {
1555
- lines.push(`${indent}${keyword}: ${serializeType(source.node)}`);
1556
- return;
1557
- }
1558
- const modeStr = mode ? `mode(${mode}) ` : "";
1559
- lines.push(`${indent}${keyword}: ${modeStr}{`);
1560
- for (const param of source.nodes) {
1561
- const optional = param.optional ? "?" : "";
1562
- let typeStr = serializeType(param.type);
1563
- if (param.nullable && !typeContainsNull(param.type)) {
1564
- typeStr = `${typeStr} | null`;
1565
- }
1566
- const defaultVal = param.default !== void 0 ? ` = ${serializeDefault(param.default)}` : "";
1567
- const comment = ctx.includeComments && param.description ? ` # ${singleLineComment(param.description)}` : "";
1568
- lines.push(`${INDENT.repeat(depth + 1)}${param.name}${optional}: ${typeStr}${defaultVal}${comment}`);
1569
- }
1570
- lines.push(`${indent}}`);
1571
- }
1572
- __name(serializeParamSource, "serializeParamSource");
1573
- function serializeRequest(lines, request, depth) {
1574
- const indent = INDENT.repeat(depth);
1575
- lines.push(`${indent}request: {`);
1576
- for (const body of request.bodies) {
1577
- lines.push(`${INDENT.repeat(depth + 1)}${body.contentType}: ${serializeType(body.bodyType)}`);
1578
- }
1579
- lines.push(`${indent}}`);
1580
- }
1581
- __name(serializeRequest, "serializeRequest");
1582
- function serializeResponses(lines, responses, depth) {
1583
- const indent = INDENT.repeat(depth);
1584
- lines.push(`${indent}response: {`);
1585
- for (const resp of responses) {
1586
- const bodies = resp.bodies;
1587
- const hasHeaders = resp.headers && resp.headers.length > 0;
1588
- if (bodies.length > 0 || hasHeaders) {
1589
- lines.push(`${INDENT.repeat(depth + 1)}${resp.statusCode}: {`);
1590
- for (const body of bodies) {
1591
- lines.push(`${INDENT.repeat(depth + 2)}${body.contentType}: ${serializeType(body.bodyType)}`);
1592
- }
1593
- if (hasHeaders) {
1594
- lines.push(`${INDENT.repeat(depth + 2)}headers: {`);
1595
- for (const h of resp.headers) {
1596
- const opt = h.optional ? "?" : "";
1597
- const trail = h.description ? ` # ${singleLineComment(h.description)}` : "";
1598
- lines.push(`${INDENT.repeat(depth + 3)}${h.name}${opt}: ${serializeType(h.type)}${trail}`);
1599
- }
1600
- lines.push(`${INDENT.repeat(depth + 2)}}`);
1601
- }
1602
- lines.push(`${INDENT.repeat(depth + 1)}}`);
1603
- } else {
1604
- lines.push(`${INDENT.repeat(depth + 1)}${resp.statusCode}:`);
1605
- }
1606
- }
1607
- lines.push(`${indent}}`);
1608
- }
1609
- __name(serializeResponses, "serializeResponses");
1610
- function serializeSecurityBlock(lines, security, depth, ctx) {
1611
- const indent = INDENT.repeat(depth);
1612
- if (security === "none") {
1613
- lines.push(`${indent}security: none`);
1614
- return;
1615
- }
1616
- const sec = security;
1617
- if (sec.policy !== void 0) {
1618
- const comment = ctx.includeComments && sec.policyDescription ? ` # ${singleLineComment(sec.policyDescription)}` : "";
1619
- const value = sec.policy === false ? "none" : sec.policy;
1620
- lines.push(`${indent}security: {`);
1621
- lines.push(`${INDENT.repeat(depth + 1)}policy: ${value}${comment}`);
1622
- lines.push(`${indent}}`);
1623
- } else {
1624
- lines.push(`${indent}security: {}`);
1625
- }
1626
- }
1627
- __name(serializeSecurityBlock, "serializeSecurityBlock");
1431
+ var serializeType = printType;
1628
1432
 
1629
1433
  // src/convert.ts
1630
1434
  import { readFileSync } from "fs";
@@ -1661,8 +1465,9 @@ var WarningCollector = class {
1661
1465
  };
1662
1466
 
1663
1467
  // src/convert.ts
1468
+ import { parseCk, decomposeCk, validateRefs, DiagnosticCollector } from "@contractkit/core";
1664
1469
  async function convertOpenApiToCk(options) {
1665
- const { split = "by-tag", includeComments = true } = options;
1470
+ const { split = "by-tag", includeComments = true, errorResponses = "documented" } = options;
1666
1471
  const warnings = new WarningCollector(options.onWarning);
1667
1472
  const rawDoc = await parseInput(options.input);
1668
1473
  const doc = normalize(rawDoc, warnings);
@@ -1676,7 +1481,8 @@ async function convertOpenApiToCk(options) {
1676
1481
  includeComments,
1677
1482
  namedSchemas: schemas,
1678
1483
  extractedModels,
1679
- inlineCounter: 0
1484
+ inlineCounter: 0,
1485
+ insideModel: true
1680
1486
  };
1681
1487
  const models = schemasToModels(schemas, schemaCtx);
1682
1488
  const { routes, routeTags } = pathsToRoutes(doc, {
@@ -1685,28 +1491,50 @@ async function convertOpenApiToCk(options) {
1685
1491
  includeComments,
1686
1492
  namedSchemas: schemas,
1687
1493
  extractedModels,
1688
- globalSecurity: doc.security
1494
+ globalSecurity: doc.security,
1495
+ errorResponses
1689
1496
  });
1497
+ const known = new Set(models.map((m) => m.name));
1498
+ for (const extracted of extractedModels) {
1499
+ if (known.has(extracted.name)) continue;
1500
+ known.add(extracted.name);
1501
+ models.push(extracted);
1502
+ }
1690
1503
  const files = /* @__PURE__ */ new Map();
1691
1504
  if (split === "by-tag") {
1692
1505
  const ckRoots = splitByTag(models, routes, routeTags);
1693
1506
  for (const [filename, root] of ckRoots) {
1694
- files.set(filename, astToCk(root, {
1695
- includeComments
1696
- }));
1507
+ files.set(filename, astToCk(root));
1697
1508
  }
1698
1509
  } else {
1699
1510
  const root = mergeIntoSingle(models, routes);
1700
- files.set("api.ck", astToCk(root, {
1701
- includeComments
1702
- }));
1511
+ files.set("api.ck", astToCk(root));
1703
1512
  }
1513
+ checkGeneratedFiles(files, warnings);
1704
1514
  return {
1705
1515
  files,
1706
1516
  warnings: warnings.warnings
1707
1517
  };
1708
1518
  }
1709
1519
  __name(convertOpenApiToCk, "convertOpenApiToCk");
1520
+ function checkGeneratedFiles(files, warnings) {
1521
+ const diag = new DiagnosticCollector();
1522
+ const roots = [];
1523
+ for (const [filename, text] of files) {
1524
+ const before = diag.getAll().length;
1525
+ const root = parseCk(text, filename, diag);
1526
+ if (diag.getAll().length === before) roots.push(root);
1527
+ }
1528
+ if (roots.length === files.size) {
1529
+ const decomposed = roots.map(decomposeCk);
1530
+ validateRefs(decomposed.map((d) => d.contract), decomposed.map((d) => d.op), diag);
1531
+ }
1532
+ for (const d of diag.getAll()) {
1533
+ if (d.severity !== "error") continue;
1534
+ warnings.warn(d.file, `generated .ck is not valid (line ${d.line}): ${d.message}`);
1535
+ }
1536
+ }
1537
+ __name(checkGeneratedFiles, "checkGeneratedFiles");
1710
1538
  async function parseInput(input) {
1711
1539
  if (typeof input === "object") {
1712
1540
  return input;
@@ -1784,4 +1612,4 @@ export {
1784
1612
  serializeType,
1785
1613
  convertOpenApiToCk
1786
1614
  };
1787
- //# sourceMappingURL=chunk-JPI3AQ7V.js.map
1615
+ //# sourceMappingURL=chunk-U7V7LFBV.js.map