@contractkit/openapi-to-ck 0.10.1 → 0.11.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 (42) hide show
  1. package/.turbo/turbo-build$colon$ci.log +7 -7
  2. package/.turbo/turbo-test$colon$ci.log +30 -26
  3. package/CHANGELOG.md +117 -0
  4. package/LICENSE +21 -0
  5. package/README.md +30 -8
  6. package/dist/ast-to-ck.d.ts +32 -16
  7. package/dist/ast-to-ck.d.ts.map +1 -1
  8. package/dist/{chunk-JPI3AQ7V.js → chunk-Z53MK4FM.js} +196 -390
  9. package/dist/chunk-Z53MK4FM.js.map +1 -0
  10. package/dist/convert.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/normalize.d.ts +6 -2
  13. package/dist/normalize.d.ts.map +1 -1
  14. package/dist/paths-to-ast.d.ts +2 -0
  15. package/dist/paths-to-ast.d.ts.map +1 -1
  16. package/dist/plugin.d.ts.map +1 -1
  17. package/dist/plugin.js +18 -3
  18. package/dist/plugin.js.map +1 -1
  19. package/dist/schema-to-ast.d.ts +13 -1
  20. package/dist/schema-to-ast.d.ts.map +1 -1
  21. package/dist/tag-splitter.d.ts.map +1 -1
  22. package/dist/types.d.ts +28 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/package.json +5 -5
  25. package/src/ast-to-ck.ts +29 -453
  26. package/src/convert.ts +57 -3
  27. package/src/normalize.ts +87 -11
  28. package/src/paths-to-ast.ts +92 -11
  29. package/src/plugin.ts +17 -2
  30. package/src/schema-to-ast.ts +51 -7
  31. package/src/tag-splitter.ts +21 -16
  32. package/src/types.ts +28 -0
  33. package/tests/__snapshots__/kitchen-sink.ck +102 -0
  34. package/tests/ast-to-ck.test.ts +34 -17
  35. package/tests/component-refs.test.ts +114 -0
  36. package/tests/coverage.test.ts +246 -0
  37. package/tests/error-responses.test.ts +94 -0
  38. package/tests/fixtures/kitchen-sink-3.1.json +100 -0
  39. package/tests/helpers.ts +40 -0
  40. package/tests/kitchen-sink.test.ts +116 -0
  41. package/tests/schema-to-ast.test.ts +11 -2
  42. 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,12 +471,17 @@ 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
486
  int64: "bigint"
430
487
  };
@@ -472,6 +529,11 @@ function schemaToModel(name, schema, ctx) {
472
529
  kind: "model",
473
530
  name,
474
531
  fields,
532
+ // `additionalProperties: true` says unknown keys are allowed, which is `mode(loose)`.
533
+ // `false` and absent both match `.ck`'s `strict` default, so neither needs a mode.
534
+ ...schema.additionalProperties === true ? {
535
+ mode: "loose"
536
+ } : {},
475
537
  description,
476
538
  loc: LOC
477
539
  };
@@ -488,10 +550,11 @@ function schemaToModel(name, schema, ctx) {
488
550
  }
489
551
  __name(schemaToModel, "schemaToModel");
490
552
  function schemaToTypeNode(schema, ctx) {
553
+ warnUnrepresentableConstraints(schema, ctx);
491
554
  if (schema.$ref) {
492
555
  const refName = extractRefName(schema.$ref);
493
556
  if (refName) {
494
- if (ctx.circularRefs.has(refName)) {
557
+ if (ctx.insideModel && ctx.circularRefs.has(refName)) {
495
558
  return {
496
559
  kind: "lazy",
497
560
  inner: {
@@ -623,7 +686,7 @@ function stringSchemaToType(schema) {
623
686
  if (schema.minLength !== void 0) mods.min = schema.minLength;
624
687
  if (schema.maxLength !== void 0) mods.max = schema.maxLength;
625
688
  }
626
- if (schema.pattern) mods.regex = `/${schema.pattern}/`;
689
+ if (schema.pattern) mods.regex = schema.pattern;
627
690
  if (schema.format && !FORMAT_TO_SCALAR[schema.format]) mods.format = schema.format;
628
691
  return {
629
692
  kind: "scalar",
@@ -793,12 +856,24 @@ function toDiscriminatedUnion(schemas, discriminator, ctx) {
793
856
  };
794
857
  }
795
858
  __name(toDiscriminatedUnion, "toDiscriminatedUnion");
859
+ var UNREPRESENTABLE_KEYWORDS = [
860
+ "exclusiveMinimum",
861
+ "exclusiveMaximum",
862
+ "multipleOf",
863
+ "uniqueItems"
864
+ ];
796
865
  function warnUnsupported(schema, ctx) {
797
866
  if (schema.xml) ctx.warnings.warn(ctx.path, "xml metadata is not supported, skipping");
798
867
  if (schema.externalDocs) ctx.warnings.info(ctx.path, "externalDocs is not supported, skipping");
799
868
  if (schema.not) ctx.warnings.warn(ctx.path, "not keyword is not supported, skipping");
800
869
  }
801
870
  __name(warnUnsupported, "warnUnsupported");
871
+ function warnUnrepresentableConstraints(schema, ctx) {
872
+ for (const keyword of UNREPRESENTABLE_KEYWORDS) {
873
+ if (schema[keyword] !== void 0) ctx.warnings.warn(ctx.path, `${keyword} has no .ck equivalent, dropping the constraint`);
874
+ }
875
+ }
876
+ __name(warnUnrepresentableConstraints, "warnUnrepresentableConstraints");
802
877
  function extractInlineModel(schema, suggestedName, ctx) {
803
878
  if (schema.$ref) {
804
879
  return {
@@ -806,7 +881,10 @@ function extractInlineModel(schema, suggestedName, ctx) {
806
881
  };
807
882
  }
808
883
  if (schema.properties || schema.type === "object" && schema.additionalProperties === void 0) {
809
- const fields = schemaPropertiesToFields(schema, ctx);
884
+ const fields = schemaPropertiesToFields(schema, {
885
+ ...ctx,
886
+ insideModel: true
887
+ });
810
888
  const model = {
811
889
  kind: "model",
812
890
  name: suggestedName,
@@ -829,10 +907,11 @@ function extractInlineModel(schema, suggestedName, ctx) {
829
907
  __name(extractInlineModel, "extractInlineModel");
830
908
  function sanitizeName(name, warnings) {
831
909
  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}"`);
910
+ const safe = /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
911
+ if (safe !== name) {
912
+ warnings.info(`#/components/schemas/${name}`, `Schema name sanitized: "${name}" \u2192 "${safe}"`);
834
913
  }
835
- return cleaned || "UnnamedSchema";
914
+ return safe || "UnnamedSchema";
836
915
  }
837
916
  __name(sanitizeName, "sanitizeName");
838
917
 
@@ -841,6 +920,11 @@ var LOC2 = {
841
920
  file: "",
842
921
  line: 0
843
922
  };
923
+ var MIME_RE = /^[a-z0-9][a-z0-9.+_-]*\/[a-z0-9][a-z0-9.+_-]*$/i;
924
+ function toNameText(summary) {
925
+ return summary.replace(/[}#]/g, " ").replace(/\s+/g, " ").trim();
926
+ }
927
+ __name(toNameText, "toNameText");
844
928
  var HTTP_METHODS = [
845
929
  "get",
846
930
  "post",
@@ -848,6 +932,11 @@ var HTTP_METHODS = [
848
932
  "patch",
849
933
  "delete"
850
934
  ];
935
+ var UNSUPPORTED_METHODS = [
936
+ "head",
937
+ "options",
938
+ "trace"
939
+ ];
851
940
  function pathsToRoutes(doc, ctx) {
852
941
  const routes = [];
853
942
  const routeTags = /* @__PURE__ */ new Map();
@@ -870,6 +959,11 @@ function pathItemToRoute(path, pathItem, ctx) {
870
959
  const operations = [];
871
960
  let primaryTag = "default";
872
961
  const pathParams = (pathItem.parameters ?? []).filter((p) => p.in === "path");
962
+ for (const method of UNSUPPORTED_METHODS) {
963
+ if (pathItem[method]) {
964
+ ctx.warnings.warn(`#/paths/${encodePathSegment2(path)}/${method}`, `\`${method}\` operations have no .ck equivalent; dropped`);
965
+ }
966
+ }
873
967
  for (const method of HTTP_METHODS) {
874
968
  const op = pathItem[method];
875
969
  if (!op) continue;
@@ -912,6 +1006,11 @@ function operationToNode(method, op, path, ctx) {
912
1006
  if (op.operationId) {
913
1007
  node.sdk = op.operationId;
914
1008
  }
1009
+ if (op.summary) {
1010
+ const name = toNameText(op.summary);
1011
+ if (name) node.name = name;
1012
+ else ctx.warnings.warn(`${pathPrefix}/summary`, "summary has no content `.ck` can carry as a name; dropped");
1013
+ }
915
1014
  if (op.description && ctx.includeComments) {
916
1015
  node.description = op.description;
917
1016
  }
@@ -923,10 +1022,16 @@ function operationToNode(method, op, path, ctx) {
923
1022
  const queryParams = [];
924
1023
  const headerParams = [];
925
1024
  for (const param of op.parameters ?? []) {
1025
+ if (!param?.name) {
1026
+ ctx.warnings.warn(`${pathPrefix}/parameters`, "skipped a parameter with no name (an unresolved $ref?)");
1027
+ continue;
1028
+ }
926
1029
  if (param.in === "query") {
927
1030
  queryParams.push(parameterToNode(param, schemaCtx));
928
1031
  } else if (param.in === "header") {
929
1032
  headerParams.push(parameterToNode(param, schemaCtx));
1033
+ } else if (param.in === "cookie") {
1034
+ ctx.warnings.warn(`${pathPrefix}/parameters/${param.name}`, "cookie parameters have no `.ck` equivalent; dropped");
930
1035
  }
931
1036
  }
932
1037
  if (queryParams.length > 0) {
@@ -946,13 +1051,17 @@ function operationToNode(method, op, path, ctx) {
946
1051
  }
947
1052
  const responses = op.responses ?? {};
948
1053
  for (const [code, resp] of Object.entries(responses)) {
1054
+ if (!/^\d{3}$/.test(code)) {
1055
+ ctx.warnings.warn(`${pathPrefix}/responses/${code}`, `response key '${code}' is not a numeric status code; dropped`);
1056
+ continue;
1057
+ }
949
1058
  const statusCode = parseInt(code, 10);
950
- if (isNaN(statusCode)) continue;
951
1059
  const respNode = responseToNode(statusCode, resp, op.operationId ?? `${method}${toPascalCase(path)}`, schemaCtx, ctx);
952
1060
  node.responses.push(respNode);
953
1061
  }
954
- if (op.security !== void 0) {
955
- node.security = convertSecurity(op.security);
1062
+ const security = op.security ?? ctx.globalSecurity;
1063
+ if (security !== void 0) {
1064
+ node.security = convertSecurity(security);
956
1065
  }
957
1066
  return node;
958
1067
  }
@@ -1011,14 +1120,13 @@ __name(parameterToNode, "parameterToNode");
1011
1120
  function requestBodyToNode(reqBody, operationName, schemaCtx, ctx) {
1012
1121
  const content = reqBody.content;
1013
1122
  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
1123
  const bodies = [];
1020
1124
  for (const [contentType, mediaType] of Object.entries(content)) {
1021
- if (!supported.has(contentType) || !mediaType?.schema) continue;
1125
+ if (!MIME_RE.test(contentType)) {
1126
+ ctx.warnings.warn(`${schemaCtx.path}/requestBody/content`, `content type '${contentType}' is not a plain type/subtype; skipped`);
1127
+ continue;
1128
+ }
1129
+ if (!mediaType?.schema) continue;
1022
1130
  const { typeNode, model } = extractInlineModel(mediaType.schema, `${toPascalCase(operationName)}Request`, schemaCtx);
1023
1131
  if (model) {
1024
1132
  ctx.extractedModels.push(model);
@@ -1034,14 +1142,24 @@ function requestBodyToNode(reqBody, operationName, schemaCtx, ctx) {
1034
1142
  };
1035
1143
  }
1036
1144
  __name(requestBodyToNode, "requestBodyToNode");
1145
+ function shouldDocument(statusCode, braced, resp, ctx) {
1146
+ if (!braced) return false;
1147
+ if (resp["x-contractkit-emit"] === "documented") return true;
1148
+ return statusCode >= 400 && ctx.errorResponses === "documented";
1149
+ }
1150
+ __name(shouldDocument, "shouldDocument");
1037
1151
  function responseToNode(statusCode, resp, operationName, schemaCtx, ctx) {
1038
1152
  const headers = convertResponseHeaders(resp.headers, schemaCtx);
1153
+ const documented = /* @__PURE__ */ __name((braced) => shouldDocument(statusCode, braced, resp, ctx) ? {
1154
+ emit: "documented"
1155
+ } : {}, "documented");
1039
1156
  const empty = /* @__PURE__ */ __name(() => ({
1040
1157
  statusCode,
1041
1158
  bodies: [],
1042
1159
  ...headers ? {
1043
1160
  headers,
1044
- hasBlock: true
1161
+ hasBlock: true,
1162
+ ...documented(true)
1045
1163
  } : {}
1046
1164
  }), "empty");
1047
1165
  if (!resp.content) return empty();
@@ -1063,7 +1181,8 @@ function responseToNode(statusCode, resp, operationName, schemaCtx, ctx) {
1063
1181
  hasBlock: true,
1064
1182
  ...headers ? {
1065
1183
  headers
1066
- } : {}
1184
+ } : {},
1185
+ ...documented(true)
1067
1186
  };
1068
1187
  }
1069
1188
  __name(responseToNode, "responseToNode");
@@ -1103,7 +1222,9 @@ function makeSchemaCtx(ctx, path) {
1103
1222
  includeComments: ctx.includeComments,
1104
1223
  namedSchemas: ctx.namedSchemas,
1105
1224
  extractedModels: ctx.extractedModels,
1106
- inlineCounter: 0
1225
+ inlineCounter: 0,
1226
+ // A response body, request body, param or header names an already-imported model.
1227
+ insideModel: false
1107
1228
  };
1108
1229
  }
1109
1230
  __name(makeSchemaCtx, "makeSchemaCtx");
@@ -1224,20 +1345,16 @@ function collectRouteRefs(route, refs) {
1224
1345
  }
1225
1346
  __name(collectRouteRefs, "collectRouteRefs");
1226
1347
  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);
1348
+ switch (source.kind) {
1349
+ case "ref":
1350
+ refs.add(source.name);
1351
+ return;
1352
+ case "params":
1353
+ for (const param of source.nodes) collectTypeRefs(param.type, refs);
1354
+ return;
1355
+ case "type":
1356
+ collectTypeRefs(source.node, refs);
1357
+ return;
1241
1358
  }
1242
1359
  }
1243
1360
  __name(collectParamSourceRefs, "collectParamSourceRefs");
@@ -1284,347 +1401,12 @@ function sanitizeFilename(tag) {
1284
1401
  __name(sanitizeFilename, "sanitizeFilename");
1285
1402
 
1286
1403
  // 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";
1404
+ import { printCk, printType } from "@contractkit/core";
1405
+ function astToCk(root, _options = {}) {
1406
+ return printCk(root);
1315
1407
  }
1316
1408
  __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");
1409
+ var serializeType = printType;
1628
1410
 
1629
1411
  // src/convert.ts
1630
1412
  import { readFileSync } from "fs";
@@ -1661,8 +1443,9 @@ var WarningCollector = class {
1661
1443
  };
1662
1444
 
1663
1445
  // src/convert.ts
1446
+ import { parseCk, decomposeCk, validateRefs, DiagnosticCollector } from "@contractkit/core";
1664
1447
  async function convertOpenApiToCk(options) {
1665
- const { split = "by-tag", includeComments = true } = options;
1448
+ const { split = "by-tag", includeComments = true, errorResponses = "documented" } = options;
1666
1449
  const warnings = new WarningCollector(options.onWarning);
1667
1450
  const rawDoc = await parseInput(options.input);
1668
1451
  const doc = normalize(rawDoc, warnings);
@@ -1676,7 +1459,8 @@ async function convertOpenApiToCk(options) {
1676
1459
  includeComments,
1677
1460
  namedSchemas: schemas,
1678
1461
  extractedModels,
1679
- inlineCounter: 0
1462
+ inlineCounter: 0,
1463
+ insideModel: true
1680
1464
  };
1681
1465
  const models = schemasToModels(schemas, schemaCtx);
1682
1466
  const { routes, routeTags } = pathsToRoutes(doc, {
@@ -1685,28 +1469,50 @@ async function convertOpenApiToCk(options) {
1685
1469
  includeComments,
1686
1470
  namedSchemas: schemas,
1687
1471
  extractedModels,
1688
- globalSecurity: doc.security
1472
+ globalSecurity: doc.security,
1473
+ errorResponses
1689
1474
  });
1475
+ const known = new Set(models.map((m) => m.name));
1476
+ for (const extracted of extractedModels) {
1477
+ if (known.has(extracted.name)) continue;
1478
+ known.add(extracted.name);
1479
+ models.push(extracted);
1480
+ }
1690
1481
  const files = /* @__PURE__ */ new Map();
1691
1482
  if (split === "by-tag") {
1692
1483
  const ckRoots = splitByTag(models, routes, routeTags);
1693
1484
  for (const [filename, root] of ckRoots) {
1694
- files.set(filename, astToCk(root, {
1695
- includeComments
1696
- }));
1485
+ files.set(filename, astToCk(root));
1697
1486
  }
1698
1487
  } else {
1699
1488
  const root = mergeIntoSingle(models, routes);
1700
- files.set("api.ck", astToCk(root, {
1701
- includeComments
1702
- }));
1489
+ files.set("api.ck", astToCk(root));
1703
1490
  }
1491
+ checkGeneratedFiles(files, warnings);
1704
1492
  return {
1705
1493
  files,
1706
1494
  warnings: warnings.warnings
1707
1495
  };
1708
1496
  }
1709
1497
  __name(convertOpenApiToCk, "convertOpenApiToCk");
1498
+ function checkGeneratedFiles(files, warnings) {
1499
+ const diag = new DiagnosticCollector();
1500
+ const roots = [];
1501
+ for (const [filename, text] of files) {
1502
+ const before = diag.getAll().length;
1503
+ const root = parseCk(text, filename, diag);
1504
+ if (diag.getAll().length === before) roots.push(root);
1505
+ }
1506
+ if (roots.length === files.size) {
1507
+ const decomposed = roots.map(decomposeCk);
1508
+ validateRefs(decomposed.map((d) => d.contract), decomposed.map((d) => d.op), diag);
1509
+ }
1510
+ for (const d of diag.getAll()) {
1511
+ if (d.severity !== "error") continue;
1512
+ warnings.warn(d.file, `generated .ck is not valid (line ${d.line}): ${d.message}`);
1513
+ }
1514
+ }
1515
+ __name(checkGeneratedFiles, "checkGeneratedFiles");
1710
1516
  async function parseInput(input) {
1711
1517
  if (typeof input === "object") {
1712
1518
  return input;
@@ -1784,4 +1590,4 @@ export {
1784
1590
  serializeType,
1785
1591
  convertOpenApiToCk
1786
1592
  };
1787
- //# sourceMappingURL=chunk-JPI3AQ7V.js.map
1593
+ //# sourceMappingURL=chunk-Z53MK4FM.js.map