@distilled.cloud/core 0.30.3 → 1.0.0-rc.10

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 (216) hide show
  1. package/LICENSE +201 -0
  2. package/lib/api.d.ts +165 -0
  3. package/lib/api.d.ts.map +1 -0
  4. package/lib/api.js +190 -0
  5. package/lib/api.js.map +1 -0
  6. package/lib/category.d.ts +4 -4
  7. package/lib/category.js +4 -4
  8. package/lib/codegen/boolean-string-enums.d.ts +36 -0
  9. package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
  10. package/lib/codegen/boolean-string-enums.js +94 -0
  11. package/lib/codegen/boolean-string-enums.js.map +1 -0
  12. package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
  13. package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
  14. package/lib/codegen/boolean-string-enums.test.js +147 -0
  15. package/lib/codegen/boolean-string-enums.test.js.map +1 -0
  16. package/lib/codegen/cli.d.ts +79 -0
  17. package/lib/codegen/cli.d.ts.map +1 -0
  18. package/lib/codegen/cli.js +149 -0
  19. package/lib/codegen/cli.js.map +1 -0
  20. package/lib/codegen/emit.d.ts +125 -0
  21. package/lib/codegen/emit.d.ts.map +1 -0
  22. package/lib/codegen/emit.js +101 -0
  23. package/lib/codegen/emit.js.map +1 -0
  24. package/lib/codegen/format.d.ts +23 -0
  25. package/lib/codegen/format.d.ts.map +1 -0
  26. package/lib/codegen/format.js +28 -0
  27. package/lib/codegen/format.js.map +1 -0
  28. package/lib/codegen/generator.d.ts +334 -0
  29. package/lib/codegen/generator.d.ts.map +1 -0
  30. package/lib/codegen/generator.js +813 -0
  31. package/lib/codegen/generator.js.map +1 -0
  32. package/lib/codegen/graph.d.ts +36 -0
  33. package/lib/codegen/graph.d.ts.map +1 -0
  34. package/lib/codegen/graph.js +136 -0
  35. package/lib/codegen/graph.js.map +1 -0
  36. package/lib/codegen/graphql-client.d.ts +62 -0
  37. package/lib/codegen/graphql-client.d.ts.map +1 -0
  38. package/lib/codegen/graphql-client.js +294 -0
  39. package/lib/codegen/graphql-client.js.map +1 -0
  40. package/lib/codegen/graphql-client.test.d.ts +2 -0
  41. package/lib/codegen/graphql-client.test.d.ts.map +1 -0
  42. package/lib/codegen/graphql-client.test.js +311 -0
  43. package/lib/codegen/graphql-client.test.js.map +1 -0
  44. package/lib/codegen/graphql.d.ts +207 -0
  45. package/lib/codegen/graphql.d.ts.map +1 -0
  46. package/lib/codegen/graphql.js +799 -0
  47. package/lib/codegen/graphql.js.map +1 -0
  48. package/lib/codegen/members.d.ts +25 -0
  49. package/lib/codegen/members.d.ts.map +1 -0
  50. package/lib/codegen/members.js +55 -0
  51. package/lib/codegen/members.js.map +1 -0
  52. package/lib/codegen/naming.d.ts +29 -0
  53. package/lib/codegen/naming.d.ts.map +1 -0
  54. package/lib/codegen/naming.js +74 -0
  55. package/lib/codegen/naming.js.map +1 -0
  56. package/lib/codegen/openapi-cli.d.ts +52 -0
  57. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  58. package/lib/codegen/openapi-cli.js +109 -0
  59. package/lib/codegen/openapi-cli.js.map +1 -0
  60. package/lib/codegen/openapi.d.ts +178 -0
  61. package/lib/codegen/openapi.d.ts.map +1 -0
  62. package/lib/codegen/openapi.js +1377 -0
  63. package/lib/codegen/openapi.js.map +1 -0
  64. package/lib/codegen/operations.d.ts +24 -0
  65. package/lib/codegen/operations.d.ts.map +1 -0
  66. package/lib/codegen/operations.js +56 -0
  67. package/lib/codegen/operations.js.map +1 -0
  68. package/lib/codegen/pagination.d.ts +39 -0
  69. package/lib/codegen/pagination.d.ts.map +1 -0
  70. package/lib/codegen/pagination.js +33 -0
  71. package/lib/codegen/pagination.js.map +1 -0
  72. package/lib/codegen/patches.d.ts +65 -0
  73. package/lib/codegen/patches.d.ts.map +1 -0
  74. package/lib/codegen/patches.js +236 -0
  75. package/lib/codegen/patches.js.map +1 -0
  76. package/lib/codegen/patches.test.d.ts +2 -0
  77. package/lib/codegen/patches.test.d.ts.map +1 -0
  78. package/lib/codegen/patches.test.js +105 -0
  79. package/lib/codegen/patches.test.js.map +1 -0
  80. package/lib/codegen/prelude.d.ts +15 -0
  81. package/lib/codegen/prelude.d.ts.map +1 -0
  82. package/lib/codegen/prelude.js +60 -0
  83. package/lib/codegen/prelude.js.map +1 -0
  84. package/lib/codegen/proto.d.ts +121 -0
  85. package/lib/codegen/proto.d.ts.map +1 -0
  86. package/lib/codegen/proto.js +962 -0
  87. package/lib/codegen/proto.js.map +1 -0
  88. package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
  89. package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
  90. package/lib/codegen/rewrite-operation-ids.js +1079 -0
  91. package/lib/codegen/rewrite-operation-ids.js.map +1 -0
  92. package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
  93. package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
  94. package/lib/codegen/rewrite-operation-ids.test.js +533 -0
  95. package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
  96. package/lib/codegen/spec-path.d.ts +16 -0
  97. package/lib/codegen/spec-path.d.ts.map +1 -0
  98. package/lib/codegen/spec-path.js +101 -0
  99. package/lib/codegen/spec-path.js.map +1 -0
  100. package/lib/error-category.d.ts +28 -0
  101. package/lib/error-category.d.ts.map +1 -0
  102. package/lib/error-category.js +46 -0
  103. package/lib/error-category.js.map +1 -0
  104. package/lib/errors.d.ts +1 -0
  105. package/lib/errors.d.ts.map +1 -1
  106. package/lib/errors.js +18 -13
  107. package/lib/errors.js.map +1 -1
  108. package/lib/graphql.d.ts +284 -0
  109. package/lib/graphql.d.ts.map +1 -0
  110. package/lib/graphql.fixture.d.ts +249 -0
  111. package/lib/graphql.fixture.d.ts.map +1 -0
  112. package/lib/graphql.fixture.js +240 -0
  113. package/lib/graphql.fixture.js.map +1 -0
  114. package/lib/graphql.js +718 -0
  115. package/lib/graphql.js.map +1 -0
  116. package/lib/graphql.test.d.ts +2 -0
  117. package/lib/graphql.test.d.ts.map +1 -0
  118. package/lib/graphql.test.js +780 -0
  119. package/lib/graphql.test.js.map +1 -0
  120. package/lib/graphql.types.d.ts +2 -0
  121. package/lib/graphql.types.d.ts.map +1 -0
  122. package/lib/graphql.types.js +45 -0
  123. package/lib/graphql.types.js.map +1 -0
  124. package/lib/json-patch.d.ts +30 -30
  125. package/lib/json-patch.d.ts.map +1 -1
  126. package/lib/json-patch.js +73 -107
  127. package/lib/json-patch.js.map +1 -1
  128. package/lib/pagination.d.ts +77 -51
  129. package/lib/pagination.d.ts.map +1 -1
  130. package/lib/pagination.js +162 -94
  131. package/lib/pagination.js.map +1 -1
  132. package/lib/protocol-http.d.ts +74 -0
  133. package/lib/protocol-http.d.ts.map +1 -0
  134. package/lib/protocol-http.js +590 -0
  135. package/lib/protocol-http.js.map +1 -0
  136. package/lib/protocol-http.test.d.ts +2 -0
  137. package/lib/protocol-http.test.d.ts.map +1 -0
  138. package/lib/protocol-http.test.js +88 -0
  139. package/lib/protocol-http.test.js.map +1 -0
  140. package/lib/protocol-rest.d.ts +134 -0
  141. package/lib/protocol-rest.d.ts.map +1 -0
  142. package/lib/protocol-rest.js +256 -0
  143. package/lib/protocol-rest.js.map +1 -0
  144. package/lib/retry.d.ts +8 -2
  145. package/lib/retry.d.ts.map +1 -1
  146. package/lib/retry.js +22 -16
  147. package/lib/retry.js.map +1 -1
  148. package/lib/schema.d.ts +8 -9
  149. package/lib/schema.d.ts.map +1 -1
  150. package/lib/schema.js +8 -9
  151. package/lib/schema.js.map +1 -1
  152. package/lib/trait.d.ts +174 -0
  153. package/lib/trait.d.ts.map +1 -0
  154. package/lib/trait.js +123 -0
  155. package/lib/trait.js.map +1 -0
  156. package/package.json +24 -78
  157. package/src/api.ts +460 -0
  158. package/src/category.ts +4 -4
  159. package/src/codegen/boolean-string-enums.test.ts +168 -0
  160. package/src/codegen/boolean-string-enums.ts +106 -0
  161. package/src/codegen/cli.ts +285 -0
  162. package/src/codegen/emit.ts +203 -0
  163. package/src/codegen/format.ts +47 -0
  164. package/src/codegen/generator.ts +1283 -0
  165. package/src/codegen/graph.ts +151 -0
  166. package/src/codegen/graphql-client.test.ts +386 -0
  167. package/src/codegen/graphql-client.ts +419 -0
  168. package/src/codegen/graphql.ts +1217 -0
  169. package/src/codegen/members.ts +71 -0
  170. package/src/codegen/naming.ts +86 -0
  171. package/src/codegen/openapi-cli.ts +182 -0
  172. package/src/codegen/openapi.ts +1689 -0
  173. package/src/codegen/operations.ts +76 -0
  174. package/src/codegen/pagination.ts +71 -0
  175. package/src/codegen/patches.test.ts +130 -0
  176. package/src/codegen/patches.ts +291 -0
  177. package/src/codegen/prelude.ts +70 -0
  178. package/src/codegen/proto.ts +1128 -0
  179. package/src/codegen/rewrite-operation-ids.test.ts +563 -0
  180. package/src/codegen/rewrite-operation-ids.ts +1206 -0
  181. package/src/codegen/spec-path.ts +115 -0
  182. package/src/error-category.ts +84 -0
  183. package/src/errors.ts +22 -25
  184. package/src/graphql.fixture.ts +371 -0
  185. package/src/graphql.test.ts +974 -0
  186. package/src/graphql.ts +1321 -0
  187. package/src/graphql.types.ts +185 -0
  188. package/src/json-patch.ts +95 -122
  189. package/src/pagination.ts +217 -146
  190. package/src/protocol-http.test.ts +107 -0
  191. package/src/protocol-http.ts +735 -0
  192. package/src/protocol-rest.ts +391 -0
  193. package/src/retry.ts +21 -22
  194. package/src/schema.ts +9 -10
  195. package/src/trait.ts +274 -0
  196. package/README.md +0 -30
  197. package/lib/client.d.ts +0 -167
  198. package/lib/client.d.ts.map +0 -1
  199. package/lib/client.js +0 -659
  200. package/lib/client.js.map +0 -1
  201. package/lib/schemas.d.ts +0 -60
  202. package/lib/schemas.d.ts.map +0 -1
  203. package/lib/schemas.js +0 -79
  204. package/lib/schemas.js.map +0 -1
  205. package/lib/sensitive.d.ts +0 -71
  206. package/lib/sensitive.d.ts.map +0 -1
  207. package/lib/sensitive.js +0 -96
  208. package/lib/sensitive.js.map +0 -1
  209. package/lib/traits.d.ts +0 -421
  210. package/lib/traits.d.ts.map +0 -1
  211. package/lib/traits.js +0 -737
  212. package/lib/traits.js.map +0 -1
  213. package/src/client.ts +0 -1177
  214. package/src/schemas.ts +0 -128
  215. package/src/sensitive.ts +0 -119
  216. package/src/traits.ts +0 -996
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Operation-level smithy helpers for the SDK generators (dev-time only).
3
+ */
4
+ import { local } from "./naming.ts";
5
+ import type { ShapeMap } from "./graph.ts";
6
+
7
+ export interface OpEntry {
8
+ id: string;
9
+ def: any;
10
+ }
11
+
12
+ /** All operation shapes, sorted by local name. */
13
+ export const collectOperations = (shapes: ShapeMap): OpEntry[] => {
14
+ const operations: OpEntry[] = [];
15
+ for (const [id, def] of Object.entries(shapes)) {
16
+ if (def.type === "operation") operations.push({ id, def });
17
+ }
18
+ operations.sort((a, b) => local(a.id).localeCompare(local(b.id)));
19
+ return operations;
20
+ };
21
+
22
+ /** The model's shape namespace (from the first operation, else any shape). */
23
+ export const modelNamespace = (
24
+ operations: readonly OpEntry[],
25
+ shapes: ShapeMap,
26
+ fallback: string,
27
+ ): string =>
28
+ operations.length
29
+ ? operations[0].id.split("#")[0]
30
+ : (Object.keys(shapes)[0]?.split("#")[0] ?? fallback);
31
+
32
+ /**
33
+ * Resolve an operation's input/output targets, synthesizing an empty named
34
+ * structure for `smithy.api#Unit` so every operation has a shape that can
35
+ * carry operation-level traits (mutates `shapes`).
36
+ */
37
+ export const ensureNamedIo = (
38
+ shapes: ShapeMap,
39
+ op: OpEntry,
40
+ ns: string,
41
+ ): { input: string; output: string } => {
42
+ const opName = local(op.id);
43
+ let input = op.def.input?.target ?? "smithy.api#Unit";
44
+ let output = op.def.output?.target ?? "smithy.api#Unit";
45
+ if (input === "smithy.api#Unit") {
46
+ input = `${ns}#${opName}Request`;
47
+ shapes[input] = { type: "structure", members: {} };
48
+ }
49
+ if (output === "smithy.api#Unit") {
50
+ output = `${ns}#${opName}Response`;
51
+ shapes[output] = { type: "structure", members: {} };
52
+ }
53
+ return { input, output };
54
+ };
55
+
56
+ /**
57
+ * Every error shape id referenced by the operations' `errors` lists —
58
+ * deduplicated, existing in the model, sorted by local name.
59
+ */
60
+ export const collectOpErrorIds = (
61
+ operations: readonly OpEntry[],
62
+ shapes: ShapeMap,
63
+ ): string[] => {
64
+ const errorIds: string[] = [];
65
+ const seen = new Set<string>();
66
+ for (const op of operations) {
67
+ for (const e of op.def.errors ?? []) {
68
+ if (!seen.has(e.target) && shapes[e.target]) {
69
+ seen.add(e.target);
70
+ errorIds.push(e.target);
71
+ }
72
+ }
73
+ }
74
+ errorIds.sort((a, b) => local(a).localeCompare(local(b)));
75
+ return errorIds;
76
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared pagination-trait handling for the SDK generators (dev-time only).
3
+ *
4
+ * Both SDKs source `smithy.api#paginated`; AWS merges operation-level
5
+ * config over service-level defaults, cloudflare validates the trait
6
+ * against the operation's actual members (degrading to a plain op when it
7
+ * can't work). Both behaviors live here.
8
+ */
9
+ import type { PaginatedTrait } from "../pagination.ts";
10
+
11
+ /**
12
+ * Merge an operation-level paginated trait over service-level defaults
13
+ * (Smithy semantics: operations may specify partial pagination and inherit
14
+ * the rest from the service).
15
+ */
16
+ export const mergePaginated = (
17
+ op: Partial<PaginatedTrait> | undefined,
18
+ service: Partial<PaginatedTrait> | undefined,
19
+ ): PaginatedTrait | undefined => {
20
+ if (!op) return undefined;
21
+ const merged = {
22
+ mode: op.mode ?? service?.mode,
23
+ inputToken: op.inputToken ?? service?.inputToken,
24
+ outputToken: op.outputToken ?? service?.outputToken,
25
+ items: op.items ?? service?.items,
26
+ pageSize: op.pageSize ?? service?.pageSize,
27
+ };
28
+ return Object.fromEntries(
29
+ Object.entries(merged).filter(([, v]) => v !== undefined),
30
+ ) as PaginatedTrait;
31
+ };
32
+
33
+ export interface ValidatePaginatedOptions {
34
+ readonly trait: Partial<PaginatedTrait>;
35
+ /** TS-facing member names of the operation input shape. */
36
+ readonly inputNames: ReadonlySet<string>;
37
+ /** TS-facing member names of the operation output shape. */
38
+ readonly outputNames: ReadonlySet<string>;
39
+ /** Items path fallback when the trait omits `items` (e.g. `"result"`). */
40
+ readonly itemsFallback?: string;
41
+ /** Output names accepted even when not modeled (e.g. `"resultInfo"`). */
42
+ readonly syntheticOutputs?: ReadonlySet<string>;
43
+ }
44
+
45
+ export interface ValidatedPagination {
46
+ readonly ok: boolean;
47
+ /** Root member of the items path (`"result.items"` → `"result"`). */
48
+ readonly itemsRoot: string;
49
+ }
50
+
51
+ /**
52
+ * A paginated op must actually carry its page/cursor token on the input
53
+ * and its items member on the output — otherwise `.pages()` would loop or
54
+ * yield nothing. Returns the items-path root for emitters that promote it
55
+ * to required.
56
+ */
57
+ export const validatePaginated = (
58
+ o: ValidatePaginatedOptions,
59
+ ): ValidatedPagination => {
60
+ const itemsRoot = String(o.trait.items ?? o.itemsFallback ?? "").split(
61
+ ".",
62
+ )[0];
63
+ const tokenOk =
64
+ o.trait.mode === "single" ||
65
+ (typeof o.trait.inputToken === "string" &&
66
+ o.inputNames.has(o.trait.inputToken));
67
+ const itemsOk =
68
+ o.outputNames.has(itemsRoot) ||
69
+ (o.syntheticOutputs?.has(itemsRoot) ?? false);
70
+ return { ok: tokenOk && itemsOk, itemsRoot };
71
+ };
@@ -0,0 +1,130 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ FINALIZED_KEY,
7
+ danglingTargets,
8
+ finalizeConvert,
9
+ syncServiceOperations,
10
+ } from "./patches.ts";
11
+
12
+ const model = () => ({
13
+ smithy: "2.0",
14
+ shapes: {
15
+ "ns#Svc": {
16
+ type: "service",
17
+ operations: [{ target: "ns#AppsList" }, { target: "ns#AppsGet" }],
18
+ },
19
+ "ns#AppsList": {
20
+ type: "operation",
21
+ input: { target: "ns#AppsListRequest" },
22
+ output: { target: "smithy.api#Unit" },
23
+ },
24
+ "ns#AppsListRequest": { type: "structure", members: {} },
25
+ "ns#AppsGet": {
26
+ type: "operation",
27
+ input: { target: "smithy.api#Unit" },
28
+ output: { target: "smithy.api#Unit" },
29
+ },
30
+ },
31
+ });
32
+
33
+ const scaffold = (patches?: Record<string, unknown>) => {
34
+ const root = mkdtempSync(join(tmpdir(), "finalize-"));
35
+ mkdirSync(join(root, ".generated-specs"));
36
+ writeFileSync(
37
+ join(root, ".generated-specs", "svc.json"),
38
+ JSON.stringify(model()),
39
+ );
40
+ if (patches) {
41
+ mkdirSync(join(root, "patches", "svc"), { recursive: true });
42
+ writeFileSync(
43
+ join(root, "patches", "svc", "a.json"),
44
+ JSON.stringify(patches),
45
+ );
46
+ }
47
+ return root;
48
+ };
49
+
50
+ const read = (root: string) =>
51
+ JSON.parse(readFileSync(join(root, ".generated-specs", "svc.json"), "utf8"));
52
+
53
+ describe("finalizeConvert", () => {
54
+ test("renames, syncs the service list, stamps the marker", async () => {
55
+ const root = scaffold();
56
+ await finalizeConvert({ root });
57
+ const m = read(root);
58
+ expect(Object.keys(m.shapes).sort()).toEqual(
59
+ ["ns#Svc", "ns#ListApps", "ns#ListAppsRequest", "ns#GetApp"].sort(),
60
+ );
61
+ expect(m.shapes["ns#Svc"].operations).toEqual([
62
+ { target: "ns#ListApps" },
63
+ { target: "ns#GetApp" },
64
+ ]);
65
+ expect(m.metadata[FINALIZED_KEY]).toBe(true);
66
+ });
67
+
68
+ test("refuses a second pass", async () => {
69
+ const root = scaffold();
70
+ await finalizeConvert({ root });
71
+ await expect(finalizeConvert({ root })).rejects.toThrow(
72
+ /already finalized/,
73
+ );
74
+ });
75
+
76
+ test("applies Smithy patches and repairs the service list after a move", async () => {
77
+ const root = scaffold({
78
+ patches: [
79
+ { op: "move", from: "/shapes/ns#AppsGet", path: "/shapes/ns#FetchApp" },
80
+ ],
81
+ });
82
+ await finalizeConvert({ root });
83
+ const m = read(root);
84
+ expect(m.shapes["ns#FetchApp"]).toBeDefined();
85
+ expect(m.shapes["ns#Svc"].operations.map((o: any) => o.target)).toEqual([
86
+ "ns#ListApps",
87
+ "ns#FetchApp",
88
+ ]);
89
+ });
90
+
91
+ test("fails on a stale patch pointer by default", async () => {
92
+ const root = scaffold({
93
+ patches: [{ op: "remove", path: "/shapes/ns#Missing" }],
94
+ });
95
+ await expect(finalizeConvert({ root })).rejects.toThrow(/failed/);
96
+ });
97
+
98
+ test("fails on dangling targets and leaves the model unstamped", async () => {
99
+ const root = scaffold({
100
+ patches: [
101
+ {
102
+ op: "replace",
103
+ path: "/shapes/ns#AppsList/output/target",
104
+ value: "ns#Nope",
105
+ },
106
+ ],
107
+ });
108
+ await expect(finalizeConvert({ root })).rejects.toThrow(/do not exist/);
109
+ expect(read(root).metadata?.[FINALIZED_KEY]).toBeUndefined();
110
+ });
111
+ });
112
+
113
+ describe("danglingTargets", () => {
114
+ test("ignores prelude ids", () => {
115
+ expect(danglingTargets(model())).toEqual([]);
116
+ });
117
+ });
118
+
119
+ describe("syncServiceOperations", () => {
120
+ test("drops missing and appends unlisted, keeping order", () => {
121
+ const m = model();
122
+ delete (m.shapes as any)["ns#AppsGet"];
123
+ (m.shapes as any)["ns#Zed"] = { type: "operation" };
124
+ expect(syncServiceOperations(m)).toBe(1);
125
+ expect((m.shapes as any)["ns#Svc"].operations).toEqual([
126
+ { target: "ns#AppsList" },
127
+ { target: "ns#Zed" },
128
+ ]);
129
+ });
130
+ });
@@ -0,0 +1,291 @@
1
+ /**
2
+ * RFC-6902 patch application for convert (dev-time only).
3
+ *
4
+ * OpenAPI ops (`/paths`, `/components`, …) apply to the spec before
5
+ * conversion; Smithy ops (`/shapes`, `/metadata`, `/smithy`) apply to the
6
+ * model after conversion. `.generated-specs` is the patched model.
7
+ * `scripts/generate.ts` does not apply patches.
8
+ */
9
+ import * as fs from "node:fs/promises";
10
+ import * as path from "node:path";
11
+ import {
12
+ applyOperation,
13
+ isStaleTargetError,
14
+ type JsonPatchOperation,
15
+ type PatchFile,
16
+ } from "../json-patch.ts";
17
+ import { verbNounSmithyModel } from "./rewrite-operation-ids.ts";
18
+
19
+ export type OnStalePatch = "fail" | "warn";
20
+
21
+ export interface ApplyPatchesResult {
22
+ files: number;
23
+ applied: number;
24
+ stale: number;
25
+ errors: string[];
26
+ }
27
+
28
+ /** Smithy-model JSON pointers — OpenAPI has no `/shapes` tree. */
29
+ export const isSmithyPatchPath = (pointer: string): boolean =>
30
+ pointer.startsWith("/shapes") ||
31
+ pointer.startsWith("/metadata") ||
32
+ pointer.startsWith("/smithy");
33
+
34
+ const exists = async (p: string): Promise<boolean> => {
35
+ try {
36
+ await fs.access(p);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ };
42
+
43
+ /**
44
+ * RFC-6902 files in `dir`: every `*.json`, `*.manual.json` last (those
45
+ * usually target post-rename shape names). Missing dir → `[]`.
46
+ */
47
+ export const listRfc6902PatchFiles = async (dir: string): Promise<string[]> => {
48
+ if (!(await exists(dir))) return [];
49
+ return (await fs.readdir(dir))
50
+ .filter((f) => f.endsWith(".json"))
51
+ .sort(
52
+ (a, b) =>
53
+ Number(a.endsWith(".manual.json")) -
54
+ Number(b.endsWith(".manual.json")) || a.localeCompare(b),
55
+ )
56
+ .map((f) => path.join(dir, f));
57
+ };
58
+
59
+ export const applyRfc6902Files = async (
60
+ target: unknown,
61
+ files: readonly string[],
62
+ opts: {
63
+ readonly onStalePatch?: OnStalePatch;
64
+ readonly include?: (op: JsonPatchOperation) => boolean;
65
+ readonly label?: (file: string) => string;
66
+ } = {},
67
+ ): Promise<ApplyPatchesResult> => {
68
+ const onStalePatch = opts.onStalePatch ?? "fail";
69
+ const include = opts.include ?? (() => true);
70
+ const result: ApplyPatchesResult = {
71
+ files: 0,
72
+ applied: 0,
73
+ stale: 0,
74
+ errors: [],
75
+ };
76
+ for (const file of files) {
77
+ const parsed = JSON.parse(await fs.readFile(file, "utf8")) as PatchFile;
78
+ const label = opts.label?.(file) ?? path.basename(file);
79
+ result.files++;
80
+ for (const patchOp of parsed.patches ?? []) {
81
+ if (!include(patchOp)) continue;
82
+ try {
83
+ applyOperation(target, patchOp);
84
+ result.applied++;
85
+ } catch (e) {
86
+ const msg = e instanceof Error ? e.message : String(e);
87
+ const line = `${label} [${patchOp.op} ${patchOp.path}]`;
88
+ if (isStaleTargetError(e)) {
89
+ result.stale++;
90
+ if (onStalePatch === "fail") {
91
+ result.errors.push(`${line}: stale target (${msg})`);
92
+ } else {
93
+ console.warn(` ⚠️ stale: ${line}`);
94
+ }
95
+ } else {
96
+ result.errors.push(`${line}: ${msg}`);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return result;
102
+ };
103
+
104
+ /**
105
+ * An RFC-6902 `move` that renames an operation shape leaves the service's
106
+ * `operations` list pointing at the old id. Drop entries whose target no
107
+ * longer exists and append operation shapes the list is missing, keeping
108
+ * the existing order. Returns the number of services whose list changed.
109
+ */
110
+ export const syncServiceOperations = (model: {
111
+ shapes?: Record<string, any>;
112
+ }): number => {
113
+ const shapes = model.shapes ?? {};
114
+ const opIds = Object.keys(shapes).filter(
115
+ (id) => shapes[id]?.type === "operation",
116
+ );
117
+ let changed = 0;
118
+ for (const def of Object.values(shapes)) {
119
+ if (def?.type !== "service") continue;
120
+ const current: Array<{ target: string }> = def.operations ?? [];
121
+ const kept = current.filter((o) => shapes[o.target]?.type === "operation");
122
+ const listed = new Set(kept.map((o) => o.target));
123
+ const after = [
124
+ ...kept,
125
+ ...opIds.filter((id) => !listed.has(id)).map((target) => ({ target })),
126
+ ];
127
+ if (JSON.stringify(current) !== JSON.stringify(after)) {
128
+ def.operations = after;
129
+ changed++;
130
+ }
131
+ }
132
+ return changed;
133
+ };
134
+
135
+ /**
136
+ * Every `target` in a Smithy model must resolve to a shape or a prelude
137
+ * (`smithy.api#…`) id. Returns the dangling ones as `"<owner> → <target>"`.
138
+ * A convert that produces any is broken — generate would emit references
139
+ * to types that do not exist.
140
+ */
141
+ export const danglingTargets = (model: {
142
+ shapes?: Record<string, any>;
143
+ }): string[] => {
144
+ const shapes = model.shapes ?? {};
145
+ const ids = new Set(Object.keys(shapes));
146
+ const out: string[] = [];
147
+ const walk = (owner: string, node: unknown): void => {
148
+ if (Array.isArray(node)) {
149
+ for (const item of node) walk(owner, item);
150
+ return;
151
+ }
152
+ if (node === null || typeof node !== "object") return;
153
+ for (const [key, value] of Object.entries(
154
+ node as Record<string, unknown>,
155
+ )) {
156
+ if (key === "target" && typeof value === "string") {
157
+ if (!value.startsWith("smithy.") && !ids.has(value)) {
158
+ out.push(`${owner} → ${value}`);
159
+ }
160
+ } else {
161
+ walk(owner, value);
162
+ }
163
+ }
164
+ };
165
+ for (const [id, def] of Object.entries(shapes)) walk(id, def);
166
+ return out;
167
+ };
168
+
169
+ /**
170
+ * Last step of every convert: Smithy RFC-6902 patches, then verbNoun
171
+ * operation names, then an optional model transform, then a reference
172
+ * check. Writes models back so `.generated-specs` is what generate
173
+ * compiles. `outDir` may be nested (GCP `stable/` / `unstable/`).
174
+ *
175
+ * Input must be FRESHLY converted models. Smithy patches are `move`/`add`
176
+ * ops that are not idempotent, and a `transform` may not be either, so
177
+ * running this over a model that already went through it is an error —
178
+ * re-run the package's `convert` instead.
179
+ */
180
+ export const finalizeConvert = async (o: {
181
+ readonly root: string;
182
+ readonly outDir?: string;
183
+ readonly patchesDir?: string | false;
184
+ readonly exclude?: (file: string) => boolean;
185
+ readonly include?: (resource: string) => boolean;
186
+ readonly transform?: (model: any, resource: string) => string | void;
187
+ readonly operationNaming?: "as-is" | "verbNoun";
188
+ readonly onStalePatch?: OnStalePatch;
189
+ }): Promise<void> => {
190
+ const specsDir = path.resolve(o.root, o.outDir ?? ".generated-specs");
191
+ if (!(await exists(specsDir))) return;
192
+ const patchesDir =
193
+ o.patchesDir === false
194
+ ? undefined
195
+ : path.resolve(o.root, o.patchesDir ?? "patches");
196
+ const naming = o.operationNaming ?? "verbNoun";
197
+
198
+ const walk = async (dir: string): Promise<string[]> => {
199
+ const out: string[] = [];
200
+ for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
201
+ const p = path.join(dir, ent.name);
202
+ if (ent.isDirectory()) {
203
+ out.push(...(await walk(p)));
204
+ } else if (
205
+ ent.name.endsWith(".json") &&
206
+ !(o.exclude?.(ent.name) ?? false)
207
+ ) {
208
+ out.push(p);
209
+ }
210
+ }
211
+ return out;
212
+ };
213
+
214
+ const files = (await walk(specsDir)).sort((a, b) => a.localeCompare(b));
215
+ const broken: string[] = [];
216
+ for (const modelPath of files) {
217
+ const resource = path.basename(modelPath, ".json");
218
+ if (o.include && !o.include(resource)) continue;
219
+ const model = JSON.parse(await fs.readFile(modelPath, "utf8"));
220
+ if (model.metadata?.[FINALIZED_KEY]) {
221
+ throw new Error(
222
+ `${path.relative(o.root, modelPath)} was already finalized — finalizeConvert is not idempotent; re-run this package's convert from the spec instead`,
223
+ );
224
+ }
225
+ if (patchesDir) {
226
+ const patchFiles = await listRfc6902PatchFiles(
227
+ path.join(patchesDir, resource),
228
+ );
229
+ const applied = await applyRfc6902Files(model, patchFiles, {
230
+ onStalePatch: o.onStalePatch,
231
+ include: (op) => isSmithyPatchPath(op.path),
232
+ label: (f) => `${resource}/${path.basename(f)}`,
233
+ });
234
+ if (applied.errors.length) {
235
+ for (const err of applied.errors) console.error(`❌ bad patch: ${err}`);
236
+ throw new Error(
237
+ `${applied.errors.length} patch operation(s) failed for ${resource} — fix the pointers or delete the patch`,
238
+ );
239
+ }
240
+ if (applied.applied > 0) {
241
+ console.log(
242
+ ` patched ${resource}: ${applied.files} file(s), ${applied.applied} op(s)` +
243
+ (applied.stale ? `, ${applied.stale} stale` : ""),
244
+ );
245
+ }
246
+ }
247
+
248
+ if (naming === "verbNoun") {
249
+ const { renamed, collisions } = verbNounSmithyModel(model);
250
+ if (renamed > 0) {
251
+ console.log(` verbNoun ${resource}: renamed ${renamed} operation(s)`);
252
+ }
253
+ for (const c of collisions) {
254
+ console.warn(
255
+ ` ⚠️ verbNoun collision ${resource}: ${c} (kept original)`,
256
+ );
257
+ }
258
+ }
259
+
260
+ const note = o.transform?.(model, resource);
261
+ if (note) console.log(` ${note}`);
262
+
263
+ syncServiceOperations(model);
264
+
265
+ const dangling = danglingTargets(model);
266
+ if (dangling.length) {
267
+ broken.push(`${resource}: ${dangling.length} dangling target(s)`);
268
+ for (const d of dangling.slice(0, 5)) console.error(` ❌ ${d}`);
269
+ if (dangling.length > 5) {
270
+ console.error(` … ${dangling.length - 5} more`);
271
+ }
272
+ // Leave the unfinalized model on disk for inspection; never stamp
273
+ // a model that generate cannot compile.
274
+ continue;
275
+ }
276
+
277
+ model.metadata = { ...model.metadata, [FINALIZED_KEY]: true };
278
+ await fs.writeFile(modelPath, `${JSON.stringify(model, null, 2)}\n`);
279
+ }
280
+ if (broken.length) {
281
+ throw new Error(
282
+ `finalizeConvert: ${broken.length} model(s) reference shapes that do not exist:\n ${broken.join("\n ")}`,
283
+ );
284
+ }
285
+ };
286
+
287
+ /**
288
+ * Metadata marker stamped by {@link finalizeConvert}. Guards against a
289
+ * second pass over the same file (see the note on finalizeConvert).
290
+ */
291
+ export const FINALIZED_KEY = "distilled.finalized";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Smithy prelude scalar baselines + reference resolvers for the SDK
3
+ * generators (dev-time only).
4
+ *
5
+ * The JSON flavor maps timestamps/blobs to plain strings — the baseline for
6
+ * SDKs whose wire format is JSON-over-docs (cloudflare). Generators with
7
+ * richer models (AWS) emit real timestamp/blob schemas themselves and only
8
+ * fall back here for untyped scalars.
9
+ */
10
+ import { isPrelude, local } from "./naming.ts";
11
+ import { isForwardRef } from "./graph.ts";
12
+
13
+ /** Prelude shape → schema expression (JSON-wire flavor). */
14
+ export const JSON_PRELUDE: Record<string, string> = {
15
+ String: "S.String",
16
+ Boolean: "S.Boolean",
17
+ Double: "S.Number",
18
+ Float: "S.Number",
19
+ Integer: "S.Number",
20
+ Long: "S.Number",
21
+ BigInteger: "S.Number",
22
+ BigDecimal: "S.Number",
23
+ Timestamp: "S.String",
24
+ Blob: "S.String",
25
+ Document: "S.Unknown",
26
+ Unit: "S.Struct({})",
27
+ };
28
+
29
+ /** TypeScript type for each prelude shape, mirroring {@link JSON_PRELUDE}. */
30
+ export const TS_JSON_PRELUDE: Record<string, string> = {
31
+ String: "string",
32
+ Boolean: "boolean",
33
+ Double: "number",
34
+ Float: "number",
35
+ Integer: "number",
36
+ Long: "number",
37
+ BigInteger: "number",
38
+ BigDecimal: "number",
39
+ Timestamp: "string",
40
+ Blob: "string",
41
+ Document: "unknown",
42
+ Unit: "{}",
43
+ };
44
+
45
+ /**
46
+ * Schema reference resolver: prelude shapes map through the prelude table,
47
+ * forward references (including into cycles) wrap in `S.suspend`.
48
+ */
49
+ export const makeSchemaRef =
50
+ (
51
+ prelude: Record<string, string>,
52
+ indexOf: ReadonlyMap<string, number>,
53
+ ): ((target: string, selfIdx: number) => string) =>
54
+ (target, selfIdx) => {
55
+ if (isPrelude(target)) return prelude[local(target)] ?? "S.Unknown";
56
+ const name = local(target);
57
+ if (isForwardRef(indexOf, target, selfIdx)) {
58
+ return `S.suspend(() => ${name})`;
59
+ }
60
+ return name;
61
+ };
62
+
63
+ /**
64
+ * TypeScript type reference resolver. Named shapes all get an emitted
65
+ * interface / type alias, so forward references are always fine.
66
+ */
67
+ export const makeTsRef =
68
+ (tsPrelude: Record<string, string>): ((target: string) => string) =>
69
+ (target) =>
70
+ isPrelude(target) ? (tsPrelude[local(target)] ?? "unknown") : local(target);