@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,1283 @@
1
+ /**
2
+ * The generic smithy→SDK service generator (dev-time only).
3
+ *
4
+ * One driver compiles a Smithy JSON model into an Effect SDK service module.
5
+ * Everything provider-specific arrives through {@link SdkSpec}: import
6
+ * header, trait vocabulary (which trait ids mean payload/file/nullable/…),
7
+ * the `T.*` pipe expressions to emit for each binding, how operations are
8
+ * declared (protocol/retry/error names), and naming policies. The driver
9
+ * owns the pipeline: operation discovery, reachability, topological order,
10
+ * schema/interface emission, error classes, pagination validation, and
11
+ * operation consts.
12
+ *
13
+ * A provider's `scripts/generate.ts` reduces to: load models (its own
14
+ * pipeline — docs-derived specs, patches, …), define its {@link SdkSpec},
15
+ * call {@link generateService} per model, write files.
16
+ */
17
+ import {
18
+ camel as _camel,
19
+ local,
20
+ lowerFirst,
21
+ oneLine,
22
+ q,
23
+ tsKey,
24
+ upperFirst,
25
+ } from "./naming.ts";
26
+ import {
27
+ orderIndex,
28
+ reachableFrom,
29
+ shapeDeps,
30
+ topoOrder,
31
+ type ShapeMap,
32
+ } from "./graph.ts";
33
+ import {
34
+ enumDecl,
35
+ errorClass,
36
+ errorUnionAlias,
37
+ interfaceDecl,
38
+ interfaceField,
39
+ operationConst,
40
+ PURE,
41
+ suspendConst,
42
+ } from "./emit.ts";
43
+ import {
44
+ JSON_PRELUDE,
45
+ makeSchemaRef,
46
+ makeTsRef,
47
+ TS_JSON_PRELUDE,
48
+ } from "./prelude.ts";
49
+ import {
50
+ collectOperations,
51
+ collectOpErrorIds,
52
+ ensureNamedIo,
53
+ modelNamespace,
54
+ type OpEntry,
55
+ } from "./operations.ts";
56
+ import { memberBases, smithyWireName } from "./members.ts";
57
+ import { validatePaginated } from "./pagination.ts";
58
+ import {
59
+ booleanStringEnums,
60
+ STRING_ENCODED_TRAIT,
61
+ } from "./boolean-string-enums.ts";
62
+
63
+ const PAGINATED_TRAIT = "smithy.api#paginated";
64
+
65
+ /**
66
+ * Error categories, derived from the STANDARD Smithy error traits.
67
+ *
68
+ * `smithy.api#httpError` and `smithy.api#retryable` are how every Smithy
69
+ * model — AWS's included — says what kind of failure an error is. The
70
+ * categories the runtime acts on (`core/category.ts`, and `isTransientError`
71
+ * in `core/retry.ts`) are a reading of those two traits, so the reading
72
+ * belongs here, once, for every provider.
73
+ *
74
+ * A model that wants its errors classified states the status the API
75
+ * actually returns:
76
+ *
77
+ * "traits": { "smithy.api#error": "client", "smithy.api#httpError": 404 }
78
+ *
79
+ * and the error class is emitted with `.pipe(C.withNotFoundError)`. A model
80
+ * with no `httpError` gets no categories — the same as today.
81
+ */
82
+ export const errorCategories = (
83
+ traits: Record<string, any> | undefined,
84
+ /** Categories a provider knows that the traits don't say (AWS's spec file). */
85
+ extra: readonly string[] = [],
86
+ ): string[] => {
87
+ const categories: string[] = [];
88
+ const add = (name: string) => {
89
+ if (!categories.includes(name)) categories.push(name);
90
+ };
91
+ const status = traits?.["smithy.api#httpError"] as number | undefined;
92
+ if (typeof status === "number") {
93
+ if (status === 401 || status === 403) add("AuthError");
94
+ else if (status === 402) add("QuotaError");
95
+ else if (
96
+ status === 400 ||
97
+ status === 404 ||
98
+ status === 405 ||
99
+ status === 406 ||
100
+ status === 410 ||
101
+ status === 413 ||
102
+ status === 415 ||
103
+ status === 422
104
+ ) {
105
+ add("BadRequestError");
106
+ } else if (status === 408 || status === 504) add("TimeoutError");
107
+ else if (status === 409) add("ConflictError");
108
+ else if (status === 429) add("ThrottlingError");
109
+ else if (status >= 500 && status < 600) add("ServerError");
110
+ }
111
+ const retryable = traits?.["smithy.api#retryable"];
112
+ if (retryable !== undefined) {
113
+ add("RetryableError");
114
+ if (retryable?.throttling) add("ThrottlingError");
115
+ }
116
+ for (const name of extra) add(name);
117
+ return categories;
118
+ };
119
+
120
+ /** `["NotFoundError"]` → `C.withNotFoundError`. */
121
+ const categoryPipes = (traits: Record<string, any> | undefined): string[] =>
122
+ errorCategories(traits).map((name) => `C.with${name}`);
123
+
124
+ /** A member's resolved binding. The four generic kinds plus provider extras. */
125
+ export type MemberBinding =
126
+ | "label"
127
+ | "query"
128
+ | "header"
129
+ | "body"
130
+ | (string & {});
131
+
132
+ export interface EmittedMember {
133
+ readonly name: string;
134
+ readonly tsName: string;
135
+ readonly wire: string;
136
+ readonly target: string;
137
+ readonly binding: MemberBinding;
138
+ readonly required: boolean;
139
+ readonly nullable: boolean;
140
+ readonly doc: string | undefined;
141
+ readonly traits: Record<string, any>;
142
+ }
143
+
144
+ export interface PaginationProfile {
145
+ /** Protocol const for paginated ops. Defaults to operationDecl.protocol. */
146
+ readonly protocol?: string;
147
+ /** Strategy expression passed as makePaginated's second argument. */
148
+ readonly strategy?: string;
149
+ /** Items path fallback when the trait omits `items`. */
150
+ readonly itemsFallback: string;
151
+ /** Output names accepted even when not modeled (delivered by the protocol). */
152
+ readonly syntheticOutputs?: readonly string[];
153
+ /**
154
+ * Extra interface field + struct member appended to paginated outputs
155
+ * that don't already model them (what the protocol delivers beyond the
156
+ * modeled shape — e.g. cloudflare's `resultInfo` from the envelope's
157
+ * `result_info`). `imports` are pulled from the pagination module in the
158
+ * header.
159
+ */
160
+ readonly injectOutputMember?: {
161
+ readonly tsName: string;
162
+ readonly interfaceLines: readonly string[];
163
+ readonly structLine: string;
164
+ readonly imports?: readonly string[];
165
+ };
166
+ }
167
+
168
+ export interface OperationEmit {
169
+ readonly op: OpEntry;
170
+ readonly opName: string;
171
+ readonly exportName: string;
172
+ readonly inputName: string;
173
+ readonly outputName: string;
174
+ /**
175
+ * The output as a TS type and as a schema expression. These differ from
176
+ * {@link outputName} when the operation's output IS a prelude shape
177
+ * (e.g. stripe's freeform `smithy.api#Document` responses): the bare
178
+ * local name isn't declared in the module, so it must resolve through
179
+ * the prelude maps (`unknown` / `S.Unknown`) instead.
180
+ */
181
+ readonly outputTsType: string;
182
+ readonly outputSchema: string;
183
+ /** Declared error class names present in the model. */
184
+ readonly errorNames: readonly string[];
185
+ readonly doc: string | undefined;
186
+ /** The validated pagination trait, when the op paginates. */
187
+ readonly pagination: unknown | undefined;
188
+ }
189
+
190
+ export interface SdkSpec {
191
+ /** Namespace fallback for models with no operations. Default `"smithy.unknown"`. */
192
+ readonly namespaceFallback?: string;
193
+ /** PURE marker before schema consts. Default: the shared single marker. */
194
+ readonly pure?: string;
195
+ /** Prelude scalar → schema expression map. Default {@link JSON_PRELUDE}. */
196
+ readonly prelude?: Record<string, string>;
197
+ /** Prelude scalar → TS type map. Default {@link TS_JSON_PRELUDE}. */
198
+ readonly tsPrelude?: Record<string, string>;
199
+ /** Wire member name → TS-facing name. Default: identity. */
200
+ readonly memberName?: (name: string) => string;
201
+ /** Operation shape name → exported const name. Default: lowerFirst. */
202
+ readonly opExportName?: (name: string) => string;
203
+ /**
204
+ * Emit the one-line smithy doc comment above shapes and error classes.
205
+ * Default true. AWS turns this off — its models carry multi-kilobyte HTML
206
+ * docs per shape and the SDK only surfaces operation-level docs.
207
+ */
208
+ readonly shapeDocs?: boolean;
209
+ /**
210
+ * Extra reachability roots beyond the operations' I/O shapes. AWS seeds
211
+ * the error shapes here so their member targets are emitted (error class
212
+ * fields reference schema consts); the error shapes themselves are still
213
+ * emitted as error classes, not schemas.
214
+ */
215
+ readonly extraRoots?: (
216
+ selected: readonly OpEntry[],
217
+ shapes: ShapeMap,
218
+ ) => Iterable<string>;
219
+
220
+ /**
221
+ * Provider member bindings as data, checked in order between the generic
222
+ * header binding and `smithy.api#httpPayload`. The driver's cascade:
223
+ * label → query → header → extraBindings → rawBody (httpPayload) → body.
224
+ *
225
+ * `pipe` is emitted for the member; `tsType` overrides its interface
226
+ * type (e.g. file uploads → `(File | Blob)[]`).
227
+ */
228
+ readonly extraBindings?: ReadonlyArray<{
229
+ readonly trait: string;
230
+ readonly binding: MemberBinding;
231
+ readonly pipe: string;
232
+ readonly tsType?: string;
233
+ /**
234
+ * Bare-payload form: when this binding is the sole member of a
235
+ * (non-paginated) output structure, the whole response IS that
236
+ * member's value — the driver emits the member's type directly and
237
+ * pipes the schema through this root marker for the protocol.
238
+ */
239
+ readonly rootPipe?: string;
240
+ }>;
241
+ /**
242
+ * Which wire-name rule a binding follows. Defaults: the three generic
243
+ * kinds map to themselves, everything else to `"other"` (jsonName).
244
+ */
245
+ readonly wireKind?: (
246
+ binding: MemberBinding,
247
+ ) => "label" | "query" | "header" | "other";
248
+ /** Trait id marking a member nullable (`S.NullOr` + `| null`). */
249
+ readonly nullableTrait?: string;
250
+ /**
251
+ * Blanket-nullable optionals: every optional body member types and
252
+ * decodes as `X | null` in addition to being omittable (`?: X | null`,
253
+ * `S.optional(S.NullOr(X))`). The cloudflare docs pipeline sets this —
254
+ * the v4 API freely returns explicit nulls for absent optional fields,
255
+ * and the v0 SDK surface modeled every optional that way.
256
+ */
257
+ readonly optionalsNullable?: boolean;
258
+ /**
259
+ * Member traits emitted as pipes when present: trait id → pipe builder
260
+ * name in the SDK's traits module. The trait's value is JSON-inlined as
261
+ * the argument (e.g. `"…#keyDictionary": "T.KeyDictionary"` →
262
+ * `T.KeyDictionary({…})`).
263
+ */
264
+ readonly memberTraitPipes?: Readonly<Record<string, string>>;
265
+ /**
266
+ * Extra schema pipes appended after the generic ones, for anything the
267
+ * data tables can't express. The driver emits `T.Label/T.Query/T.Header`
268
+ * (wire-aware), `T.HttpBody()` for rawBody, and `T.Body(wire)` renames —
269
+ * the SDK's traits module must export those core builders under these
270
+ * names.
271
+ */
272
+ readonly memberExtraPipes?: (m: EmittedMember) => string[];
273
+ /** Full override of member pipe emission (rarely needed). */
274
+ readonly memberPipes?: (m: EmittedMember) => string[];
275
+ /** Function override for member TS types beyond the binding table. */
276
+ readonly memberTsType?: (
277
+ m: EmittedMember,
278
+ tsRef: (target: string) => string,
279
+ ) => string | undefined;
280
+
281
+ /**
282
+ * Struct-level pipes for a shape (after the member struct): the http
283
+ * trait on op inputs, service-wide key-dictionary stamping, etc.
284
+ */
285
+ readonly structPipes?: (ctx: {
286
+ readonly id: string;
287
+ readonly isOpIo: boolean;
288
+ readonly httpTrait: unknown | undefined;
289
+ }) => string[];
290
+
291
+ /**
292
+ * Pagination profiles. A profile is the codegen-side description of one
293
+ * paginated wire variant: the Protocol const that decodes it, the
294
+ * strategy passed to makePaginated, the trait-validation rules, and any
295
+ * output member the protocol delivers beyond the modeled shape. SDKs
296
+ * with several pagination styles declare several profiles and select
297
+ * per-op via {@link SdkSpec.paginationProfileFor}.
298
+ */
299
+ readonly paginationProfiles?: Readonly<Record<string, PaginationProfile>>;
300
+ /**
301
+ * Select the profile for a paginated op (from its `smithy.api#paginated`
302
+ * trait / shape). Default: the sole declared profile; with several
303
+ * profiles this becomes required for ops to paginate.
304
+ */
305
+ readonly paginationProfileFor?: (
306
+ trait: any,
307
+ op: OpEntry,
308
+ ) => string | undefined;
309
+
310
+ /**
311
+ * Service-wide fallback key dictionary stamped on op I/O roots (emitted
312
+ * as a `KEY_DICTIONARY` header const + `T.KeyDictionary(KEY_DICTIONARY)`
313
+ * root pipe). `doc` is the const's doc comment. An entry may list several
314
+ * wire spellings (first = canonical encode name; decode accepts all).
315
+ */
316
+ readonly rootKeyDictionary?: {
317
+ readonly dict: Record<string, string | ReadonlyArray<string>>;
318
+ readonly doc: string;
319
+ };
320
+
321
+ /** Banner suffix: `AUTO-GENERATED by scripts/generate.ts from <note>`. */
322
+ readonly sourceNote?: string;
323
+
324
+ /**
325
+ * Operation aliases: re-export the canonical op (and its
326
+ * Request/Response/Error types) under each alias name. Skipped when the
327
+ * target wasn't emitted or the alias name is taken.
328
+ */
329
+ readonly opAliases?: ReadonlyArray<{
330
+ readonly alias: string;
331
+ readonly target: string;
332
+ }>;
333
+
334
+ /**
335
+ * Full shape-emission override, checked before the driver's own shape
336
+ * handling. Return the emitted lines to own a shape (e.g. AWS's
337
+ * newtypes, structural unions, event streams), or undefined to let the
338
+ * driver emit it. `selfIdx` is the shape's position in emission order
339
+ * for forward-ref decisions.
340
+ */
341
+ readonly shapeOverride?: (ctx: {
342
+ readonly id: string;
343
+ readonly def: any;
344
+ readonly name: string;
345
+ readonly selfIdx: number;
346
+ readonly ref: (target: string, selfIdx: number) => string;
347
+ readonly tsRef: (target: string) => string;
348
+ readonly members: (d: any) => EmittedMember[];
349
+ }) => string[] | undefined;
350
+
351
+ /**
352
+ * Union emission style. `"opaque-cases"`: the TS type is the case union
353
+ * and the schema is `S.Unknown.pipe(T.UnionCases([...case key sets]))` —
354
+ * the protocol discriminates by key-set at decode time (for APIs that
355
+ * return every case's keys with nulls, like Cloudflare's).
356
+ */
357
+ readonly unionStyle?: "opaque-cases";
358
+ /** Full override of union emission. */
359
+ readonly union?: (ctx: {
360
+ readonly name: string;
361
+ readonly caseTargets: readonly string[];
362
+ readonly caseKeys: readonly (readonly string[])[];
363
+ readonly tsRef: (target: string) => string;
364
+ }) => string[];
365
+
366
+ /**
367
+ * Trait id carrying error matchers: when present on an error shape, the
368
+ * class is wrapped in `T.applyErrorMatchers(<cls>, <trait value>)`.
369
+ */
370
+ readonly errorMatchersTrait?: string;
371
+ /** Error-class emission details. All optional. */
372
+ readonly errors?: {
373
+ /**
374
+ * Full override of one error class's emission (mirrors shapeOverride):
375
+ * return the emitted lines to own the error, or undefined to fall back
376
+ * to the driver's field/wrap-based emission.
377
+ */
378
+ readonly override?: (ctx: {
379
+ readonly id: string;
380
+ readonly def: any;
381
+ readonly name: string;
382
+ }) => string[] | undefined;
383
+ /**
384
+ * Field lines used when the error shape declares no members.
385
+ * Default: `code` (integer) + `message` (string) — the common REST
386
+ * error envelope.
387
+ */
388
+ readonly defaultFields?: (prelude: Record<string, string>) => string[];
389
+ /** Field line for a declared member. Default: prelude-mapped schema. */
390
+ readonly field?: (name: string, target: string) => string;
391
+ /** Optional wrapper (e.g. matcher application) from the shape's traits. */
392
+ readonly wrap?: (
393
+ traits: Record<string, any>,
394
+ ) => ((cls: string) => string) | undefined;
395
+ };
396
+
397
+ /**
398
+ * Declarative operation emission — the names the op consts are built
399
+ * from. `operation` overrides this entirely when a provider needs full
400
+ * control of the emitted shape.
401
+ */
402
+ readonly operationDecl?: {
403
+ /** Requirements type in the OperationMethod annotation. */
404
+ readonly contextType: string;
405
+ /** Base of the per-op error union alias (e.g. `CloudflareOpError`). */
406
+ readonly commonErrorType: string;
407
+ /** Error classes appended to every op's `errors: [...]` list. */
408
+ readonly commonErrorClasses: readonly string[];
409
+ readonly protocol: string;
410
+ /** The retry tag expression (e.g. `Retry.Retry`). */
411
+ readonly retry: string;
412
+ /**
413
+ * Extra config lines inserted before the pagination entry (e.g. AWS's
414
+ * `operationName` and `endpointHostPrefix`).
415
+ */
416
+ readonly extraConfig?: (ctx: OperationEmit) => string[];
417
+ };
418
+ /** Full override of operation const emission. */
419
+ readonly operation?: (ctx: OperationEmit) => string;
420
+
421
+ /**
422
+ * Module header override. The default builds the banner + imports from
423
+ * {@link SdkSpec.operationDecl} names and the conventional module layout
424
+ * (`../traits.ts`, `../protocol.ts`, `../pagination.ts`, `../errors.ts`,
425
+ * `../retry.ts`), re-exports the op error/context types, and emits the
426
+ * `KEY_DICTIONARY` const when {@link SdkSpec.rootKeyDictionary} is set.
427
+ */
428
+ readonly header?: (ctx: {
429
+ readonly hasPaginated: boolean;
430
+ readonly model: any;
431
+ }) => string;
432
+
433
+ /** Final pass over the assembled module (e.g. pruning unused imports). */
434
+ readonly postProcess?: (code: string) => string;
435
+
436
+ /**
437
+ * Trailing sections after operations (e.g. route-alias re-exports).
438
+ * Receives the set of emitted op export names (mutable — additions are
439
+ * visible to subsequent alias checks).
440
+ */
441
+ readonly footer?: (ctx: { readonly emittedOps: Set<string> }) => string[];
442
+ }
443
+
444
+ export interface GeneratedService {
445
+ code: string;
446
+ operations: number;
447
+ }
448
+
449
+ /** Compile one Smithy model into a service module. */
450
+ export const generateService = (
451
+ model: any,
452
+ spec: SdkSpec,
453
+ ): GeneratedService => {
454
+ // `"true" | "false"` request members become real booleans that travel as
455
+ // their string spelling (see boolean-string-enums.ts).
456
+ booleanStringEnums(model);
457
+ const shapes: ShapeMap = model.shapes;
458
+ const pure = spec.pure ?? PURE;
459
+ const prelude = spec.prelude ?? JSON_PRELUDE;
460
+ const tsPrelude = spec.tsPrelude ?? TS_JSON_PRELUDE;
461
+ const memberName = spec.memberName ?? ((n: string) => n);
462
+ const opExportName = spec.opExportName ?? lowerFirst;
463
+
464
+ // 1. Operations — synthesize named Request/Response for Unit I/O so every
465
+ // operation has an input shape that can carry operation-level traits.
466
+ const operations = collectOperations(shapes);
467
+ const httpFor: Record<string, any> = {}; // input shape id → http trait
468
+ const ns = modelNamespace(
469
+ operations,
470
+ shapes,
471
+ spec.namespaceFallback ?? "smithy.unknown",
472
+ );
473
+
474
+ const selected: OpEntry[] = [];
475
+ for (const op of operations) {
476
+ selected.push(op);
477
+
478
+ const { input, output } = ensureNamedIo(shapes, op, ns);
479
+ op.def.__input = input;
480
+ op.def.__output = output;
481
+
482
+ const http = op.def.traits?.["smithy.api#http"];
483
+ if (http) httpFor[input] = http;
484
+ }
485
+
486
+ if (selected.length === 0) return { code: "", operations: 0 };
487
+
488
+ // 2. Reachability + dependencies-first order (cycles suspend at refs).
489
+ const roots = [
490
+ ...selected.flatMap((op) => [op.def.__input, op.def.__output]),
491
+ ...(spec.extraRoots?.(selected, shapes) ?? []),
492
+ ];
493
+ const reachable = reachableFrom(shapes, roots, shapeDeps);
494
+ const order = topoOrder(shapes, reachable, shapeDeps);
495
+ const indexOf = orderIndex(order);
496
+
497
+ /**
498
+ * Shape id → the id whose emitted body it shares.
499
+ *
500
+ * Structural aliasing (see the struct emission below) has to cascade: a
501
+ * parent only matches another parent if their MEMBERS render identically,
502
+ * and a member renders as its target's name. So every reference resolves
503
+ * through this map first — once `…ItemGroupRuleGroup` is aliased onto a
504
+ * canonical shape, the parents referencing it start rendering the same
505
+ * text and collapse in turn.
506
+ *
507
+ * Filled in topological order, so a canonical target is always a shape
508
+ * that was already emitted — a back-reference, never a forward one.
509
+ */
510
+ const canonicalId = new Map<string, string>();
511
+ const canon = (target: string): string => canonicalId.get(target) ?? target;
512
+
513
+ const rawRef = makeSchemaRef(prelude, indexOf);
514
+ const rawTsRef = makeTsRef(tsPrelude);
515
+ const ref = (target: string, selfIdx: number) =>
516
+ rawRef(canon(target), selfIdx);
517
+ const tsRef = (target: string) => rawTsRef(canon(target));
518
+
519
+ // Direction classification for enum openness. Enum ALIASES are emitted
520
+ // CLOSED (exhaustively matchable on reads); request-reachable shapes
521
+ // re-open enum references inline (`X | (string & {})`) so consumers can
522
+ // send tomorrow's values without an SDK update.
523
+ const requestReachable = reachableFrom(
524
+ shapes,
525
+ selected.map((op) => op.def.__input),
526
+ shapeDeps,
527
+ );
528
+ // Discriminant enums: single-value enums required by a union-arm
529
+ // structure (e.g. the worker binding `type: "ai"` literals). These stay
530
+ // CLOSED even on the request side — consumers and the union's TS
531
+ // narrowing rely on the exact literal (v0 parity).
532
+ const discriminantEnums = new Set<string>();
533
+ for (const d of Object.values(shapes) as any[]) {
534
+ if (d?.type !== "union") continue;
535
+ for (const um of Object.values(d.members ?? {}) as any[]) {
536
+ const arm = shapes[um.target];
537
+ if (arm?.type !== "structure") continue;
538
+ for (const am of Object.values(arm.members ?? {}) as any[]) {
539
+ const t = shapes[am.target];
540
+ if (
541
+ (t?.type === "enum" || t?.type === "intEnum") &&
542
+ Object.keys(t.members ?? {}).length === 1 &&
543
+ am.traits?.["smithy.api#required"] !== undefined
544
+ ) {
545
+ discriminantEnums.add(am.target);
546
+ }
547
+ }
548
+ }
549
+ }
550
+ /**
551
+ * TS reference with direction-aware enum openness. Any reference inside a
552
+ * request-reachable shape re-opens the enum inline — including shapes
553
+ * shared with responses (a value you can SEND must accept undocumented
554
+ * members, and response runtime is open regardless). References inside
555
+ * pure response/error shapes stay the plain closed alias, and so do
556
+ * union-arm discriminant literals.
557
+ */
558
+ const tsRefAt = (rawTarget: string, ownerId: string): string => {
559
+ const target = canon(rawTarget);
560
+ const base = tsRef(target);
561
+ if (!requestReachable.has(ownerId)) return base;
562
+ if (discriminantEnums.has(target)) return base;
563
+ const d = shapes[target];
564
+ if (d?.type === "enum") return `${base} | (string & {})`;
565
+ if (d?.type === "intEnum") return `${base} | (number & {})`;
566
+ return base;
567
+ };
568
+
569
+ const wireKind =
570
+ spec.wireKind ??
571
+ ((b: MemberBinding): "label" | "query" | "header" | "other" =>
572
+ b === "label"
573
+ ? "label"
574
+ : b === "query"
575
+ ? "query"
576
+ : b === "header"
577
+ ? "header"
578
+ : "other");
579
+
580
+ // The generic binding cascade; provider bindings slot in after headers.
581
+ const extraBindingOf = (
582
+ traits: Record<string, any>,
583
+ ): MemberBinding | undefined => {
584
+ for (const b of spec.extraBindings ?? []) {
585
+ if (b.trait in traits) return b.binding;
586
+ }
587
+ return undefined;
588
+ };
589
+ const bindingOf = (traits: Record<string, any>): MemberBinding =>
590
+ "smithy.api#httpLabel" in traits
591
+ ? "label"
592
+ : "smithy.api#httpQuery" in traits
593
+ ? "query"
594
+ : "smithy.api#httpHeader" in traits
595
+ ? "header"
596
+ : (extraBindingOf(traits) ??
597
+ ("smithy.api#httpPayload" in traits ? "rawBody" : "body"));
598
+
599
+ // Shapes reachable from any response/error root — used to scope the
600
+ // blanket-nullable-optionals rule to reads (the wire returns explicit
601
+ // nulls; requests only accept null where a nullable trait says so).
602
+ const responseReachable =
603
+ spec.optionalsNullable === true
604
+ ? reachableFrom(
605
+ shapes,
606
+ [
607
+ ...selected.map((op) => op.def.__output),
608
+ ...collectOpErrorIds(selected, shapes),
609
+ ],
610
+ shapeDeps,
611
+ )
612
+ : undefined;
613
+
614
+ const memberInfos = (d: any, ownerId?: string): EmittedMember[] =>
615
+ memberBases(d, memberName).map((base) => {
616
+ const traits = base.traits;
617
+ const binding = bindingOf(traits);
618
+ return {
619
+ ...base,
620
+ binding,
621
+ wire: smithyWireName(traits, base.name, wireKind(binding)),
622
+ nullable:
623
+ (spec.nullableTrait ? spec.nullableTrait in traits : false) ||
624
+ // Blanket-nullable optionals (v0 surface): every optional BODY
625
+ // member of a response-reachable shape accepts/announces null
626
+ // in addition to being omittable.
627
+ (responseReachable !== undefined &&
628
+ ownerId !== undefined &&
629
+ responseReachable.has(ownerId) &&
630
+ !base.required &&
631
+ binding === "body"),
632
+ };
633
+ });
634
+
635
+ /** The single value a one-value enum shape fixes, if `target` is one. */
636
+ const soleEnumValue = (target: string): string | undefined => {
637
+ const d = shapes[target];
638
+ if (d?.type !== "enum") return undefined;
639
+ const values = Object.values(d.members ?? {}).map(
640
+ (m: any) => m.traits?.["smithy.api#enumValue"],
641
+ );
642
+ return values.length === 1 && typeof values[0] === "string"
643
+ ? values[0]
644
+ : undefined;
645
+ };
646
+
647
+ /**
648
+ * The member every case of a union pins to a different literal — the tag
649
+ * the API itself discriminates by (`type: "zone"`, `id: "browser_check"`).
650
+ * Undefined unless every case is a structure carrying that member and no
651
+ * two cases claim the same value.
652
+ */
653
+ const unionDiscriminator = (
654
+ caseTargets: readonly string[],
655
+ ): { key: string; values: string[] } | undefined => {
656
+ if (caseTargets.length < 2) return undefined;
657
+ const perCase = caseTargets.map((t) => {
658
+ const cd = shapes[t];
659
+ if (cd?.type !== "structure") return undefined;
660
+ const tags = new Map<string, string>();
661
+ for (const mi of memberInfos(cd)) {
662
+ const value = soleEnumValue(mi.target);
663
+ if (value !== undefined) tags.set(mi.tsName, value);
664
+ }
665
+ return tags;
666
+ });
667
+ if (perCase.some((tags) => tags === undefined || tags.size === 0)) {
668
+ return undefined;
669
+ }
670
+ for (const key of perCase[0]!.keys()) {
671
+ const values = perCase.map((tags) => tags!.get(key));
672
+ if (values.some((v) => v === undefined)) continue;
673
+ if (new Set(values).size !== values.length) continue;
674
+ return { key, values: values as string[] };
675
+ }
676
+ return undefined;
677
+ };
678
+
679
+ // Generic pipes for the smithy bindings (the SDK's traits module exports
680
+ // core's Label/Query/Header/HttpBody/Body builders under these names);
681
+ // provider bindings and member traits append theirs via memberExtraPipes.
682
+ const genericPipes = (info: EmittedMember): string[] => {
683
+ switch (info.binding) {
684
+ case "label":
685
+ return [
686
+ info.wire === info.tsName ? "T.Label()" : `T.Label(${q(info.wire)})`,
687
+ ];
688
+ case "query":
689
+ return [
690
+ info.wire === info.tsName ? "T.Query()" : `T.Query(${q(info.wire)})`,
691
+ ];
692
+ case "header":
693
+ return [
694
+ info.wire === info.tsName
695
+ ? "T.Header()"
696
+ : `T.Header(${q(info.wire)})`,
697
+ ];
698
+ case "rawBody":
699
+ return ["T.HttpBody()"];
700
+ case "body":
701
+ return info.wire !== info.tsName ? [`T.Body(${q(info.wire)})`] : [];
702
+ default:
703
+ return [];
704
+ }
705
+ };
706
+
707
+ const memberPipes =
708
+ spec.memberPipes ??
709
+ ((info: EmittedMember) => [
710
+ ...genericPipes(info),
711
+ // The binding table's pipe (envelope payloads, file uploads, …).
712
+ ...(spec.extraBindings ?? [])
713
+ .filter((b) => b.binding === info.binding && b.trait in info.traits)
714
+ .map((b) => b.pipe),
715
+ // Trait-table pipes: trait value JSON-inlined as the argument.
716
+ ...Object.entries(spec.memberTraitPipes ?? {})
717
+ .filter(([trait]) => info.traits[trait] !== undefined)
718
+ .map(
719
+ ([trait, builder]) =>
720
+ `${builder}(${JSON.stringify(info.traits[trait])})`,
721
+ ),
722
+ ...(STRING_ENCODED_TRAIT in info.traits ? ["T.StringEncoded()"] : []),
723
+ ...(spec.memberExtraPipes?.(info) ?? []),
724
+ ]);
725
+
726
+ const memberTsTypeOf = (
727
+ info: EmittedMember,
728
+ tsRefFn: (target: string) => string,
729
+ ): string | undefined =>
730
+ spec.memberTsType?.(info, tsRefFn) ??
731
+ (spec.extraBindings ?? []).find(
732
+ (b) => b.binding === info.binding && b.tsType !== undefined,
733
+ )?.tsType;
734
+
735
+ const emitMember = (info: EmittedMember, selfIdx: number): string => {
736
+ let expr = ref(info.target, selfIdx);
737
+ if (info.nullable) expr = `S.NullOr(${expr})`;
738
+ const pipes = memberPipes(info);
739
+ if (pipes.length) expr = `${expr}.pipe(${pipes.join(", ")})`;
740
+ if (!info.required) expr = `S.optional(${expr})`;
741
+ return ` ${q(info.tsName)}: ${expr},`;
742
+ };
743
+
744
+ const opIoShapes = new Set<string>();
745
+ for (const op of selected) {
746
+ opIoShapes.add(op.def.__input);
747
+ opIoShapes.add(op.def.__output);
748
+ }
749
+
750
+ // 3. Validate pagination traits: a paginated op must actually carry its
751
+ // token on the input and its items member on the output, else it
752
+ // degrades to a plain operation.
753
+ const paginatedOutputs = new Set<string>();
754
+ const paginatedItemsRoot = new Map<string, string>();
755
+ /** Op id → its full (possibly dotted) items path, `""` when it has none. */
756
+ const paginatedItemsPath = new Map<string, string>();
757
+ /** Output shape id → the pagination profile that decodes it. */
758
+ const outputProfile = new Map<string, PaginationProfile>();
759
+ /** Op id → its pagination profile (drives protocol/strategy emission). */
760
+ const opProfile = new Map<string, PaginationProfile>();
761
+ const usedProfiles = new Set<PaginationProfile>();
762
+ const profileNames = Object.keys(spec.paginationProfiles ?? {});
763
+ if (profileNames.length > 0) {
764
+ for (const op of selected) {
765
+ const pg = op.def.traits?.[PAGINATED_TRAIT];
766
+ if (!pg) continue;
767
+ // Which profile decodes this op's pages: the selector's answer, or
768
+ // the sole declared profile.
769
+ const profileName =
770
+ spec.paginationProfileFor?.(pg, op) ??
771
+ (profileNames.length === 1 ? profileNames[0] : undefined);
772
+ const profile = profileName
773
+ ? spec.paginationProfiles![profileName]
774
+ : undefined;
775
+ if (!profile) continue;
776
+ const inNames = new Set(
777
+ memberInfos(shapes[op.def.__input] ?? {}).map((m) => m.tsName),
778
+ );
779
+ const outNames = new Set(
780
+ memberInfos(shapes[op.def.__output] ?? {}).map((m) => m.tsName),
781
+ );
782
+ const { ok, itemsRoot } = validatePaginated({
783
+ trait: pg,
784
+ inputNames: inNames,
785
+ outputNames: outNames,
786
+ itemsFallback: profile.itemsFallback,
787
+ syntheticOutputs: new Set(profile.syntheticOutputs ?? []),
788
+ });
789
+ if (ok) {
790
+ op.def.__pagination = pg;
791
+ paginatedOutputs.add(op.def.__output);
792
+ paginatedItemsRoot.set(op.def.__output, itemsRoot);
793
+ paginatedItemsPath.set(
794
+ op.id,
795
+ String(pg.items ?? profile.itemsFallback ?? ""),
796
+ );
797
+ outputProfile.set(op.def.__output, profile);
798
+ opProfile.set(op.id, profile);
799
+ usedProfiles.add(profile);
800
+ }
801
+ }
802
+ }
803
+
804
+ // 4. Error classes from the operations' errors lists.
805
+ const out: string[] = [];
806
+ // Emitted struct body → the name that owns it, for structural aliasing.
807
+ // Keyed on the emitted TEXT, so two shapes collapse only when what they
808
+ // would emit is byte-identical — including member names, targets, docs and
809
+ // pipes. References are already resolved to names at this point, so a
810
+ // difference anywhere in the tree shows up as different text.
811
+ const structBodies = new Map<string, { id: string; name: string }>();
812
+ let aliased = 0;
813
+ // Set when any error carries CATEGORY_TRAIT, so the header only imports
814
+ // the category module when something actually uses it.
815
+ let usesCategories = false;
816
+ const errorIds = collectOpErrorIds(selected, shapes);
817
+ const errorIdSet = new Set(errorIds);
818
+ const errorNames = new Set(errorIds.map(local));
819
+
820
+ const shapeDocs = spec.shapeDocs ?? true;
821
+
822
+ for (const id of errorIds) {
823
+ const d = shapes[id];
824
+ const name = local(id);
825
+ const doc = oneLine(d.traits?.["smithy.api#documentation"]);
826
+ if (doc && shapeDocs) out.push(`/** ${doc} */`);
827
+ const overridden = spec.errors?.override?.({ id, def: d, name });
828
+ if (overridden) {
829
+ out.push(...overridden);
830
+ continue;
831
+ }
832
+ const errorField =
833
+ spec.errors?.field ??
834
+ ((mn: string, target: string) =>
835
+ ` ${tsKey(mn)}: ${prelude[local(target)] ?? "S.Unknown"},`);
836
+ const fields =
837
+ d.members && Object.keys(d.members).length
838
+ ? Object.entries(d.members).map(([mn, m]: [string, any]) =>
839
+ errorField(mn, m.target),
840
+ )
841
+ : (spec.errors?.defaultFields?.(prelude) ?? [
842
+ errorField("code", "smithy.api#Integer"),
843
+ errorField("message", "smithy.api#String"),
844
+ ]);
845
+ const matchers = spec.errorMatchersTrait
846
+ ? d.traits?.[spec.errorMatchersTrait]
847
+ : undefined;
848
+ const categories = categoryPipes(d.traits);
849
+ if (categories.length) usesCategories = true;
850
+ out.push(
851
+ errorClass({
852
+ name,
853
+ fields,
854
+ pipes: categories.length
855
+ ? `.pipe(${categories.join(", ")})`
856
+ : undefined,
857
+ wrap:
858
+ spec.errors?.wrap?.(d.traits ?? {}) ??
859
+ (matchers
860
+ ? (cls) =>
861
+ `T.applyErrorMatchers(\n${cls},\n${JSON.stringify(matchers)},\n)`
862
+ : undefined),
863
+ }),
864
+ );
865
+ }
866
+
867
+ // 5. Every reachable shape in dependency order: explicit TS type next to
868
+ // a schema const cast to `S.Schema<T>` (the compile-perf pattern).
869
+ order.forEach((id, i) => {
870
+ if (errorIdSet.has(id)) return; // emitted as an error class above
871
+ const d = shapes[id];
872
+ const name = local(id);
873
+ const doc = oneLine(d.traits?.["smithy.api#documentation"]);
874
+ if (doc && shapeDocs) out.push(`/** ${doc} */`);
875
+
876
+ const override = spec.shapeOverride?.({
877
+ id,
878
+ def: d,
879
+ name,
880
+ selfIdx: i,
881
+ ref,
882
+ tsRef,
883
+ members: memberInfos,
884
+ });
885
+ if (override) {
886
+ out.push(...override);
887
+ return;
888
+ }
889
+
890
+ if (d.type === "structure") {
891
+ // Bare-payload response: single trait-marked member — the response IS
892
+ // that member's value; emit its type + a root marker for the protocol.
893
+ const memberEntriesAll = Object.entries(d.members ?? {});
894
+ const soleMemberRoot =
895
+ !paginatedOutputs.has(id) && memberEntriesAll.length === 1
896
+ ? (spec.extraBindings ?? []).find(
897
+ (b) =>
898
+ b.rootPipe !== undefined &&
899
+ b.trait in ((memberEntriesAll[0]![1] as any).traits ?? {}),
900
+ )
901
+ : undefined;
902
+ if (soleMemberRoot) {
903
+ const [, m] = memberEntriesAll[0]! as [string, any];
904
+ // Alias responses are op roots too — they must carry the service
905
+ // key dictionary so opaque content (union cases, freeform maps)
906
+ // nested in the payload decodes with the same wire mapping as
907
+ // struct-form roots.
908
+ const rootPipes = [
909
+ soleMemberRoot.rootPipe,
910
+ ...(spec.rootKeyDictionary && opIoShapes.has(id)
911
+ ? [`T.KeyDictionary(KEY_DICTIONARY)`]
912
+ : []),
913
+ ];
914
+ out.push(`export type ${name} = ${tsRef(m.target)};`);
915
+ out.push(
916
+ suspendConst({
917
+ name,
918
+ pure,
919
+ multiline: true,
920
+ annotateIdentifier: true,
921
+ expr: `${ref(m.target, i)}.pipe(${rootPipes.join(", ")})`,
922
+ }),
923
+ );
924
+ return;
925
+ }
926
+
927
+ // Paginated outputs always deliver their items member — required.
928
+ const itemsRoot = paginatedItemsRoot.get(id);
929
+ const infos = memberInfos(d, id).map((info) =>
930
+ info.tsName === itemsRoot ? { ...info, required: true } : info,
931
+ );
932
+ const fields = infos.flatMap((info) =>
933
+ interfaceField({
934
+ name: info.tsName,
935
+ optional: !info.required,
936
+ doc: info.doc,
937
+ type:
938
+ memberTsTypeOf(info, tsRef) ??
939
+ `${tsRefAt(info.target, id)}${info.nullable ? " | null" : ""}`,
940
+ }),
941
+ );
942
+ const members = infos.map((info) => emitMember(info, i));
943
+ const inject = outputProfile.get(id)?.injectOutputMember;
944
+ if (inject && !infos.some((m) => m.tsName === inject.tsName)) {
945
+ fields.push(...inject.interfaceLines);
946
+ members.push(inject.structLine);
947
+ }
948
+ const struct = members.length
949
+ ? `S.Struct({\n${members.join("\n")}\n})`
950
+ : `S.Struct({})`;
951
+ const structCtx = {
952
+ id,
953
+ isOpIo: opIoShapes.has(id),
954
+ httpTrait: httpFor[id],
955
+ };
956
+ const pipes = spec.structPipes?.(structCtx) ?? [
957
+ ...(structCtx.httpTrait
958
+ ? [`T.Http(${JSON.stringify(structCtx.httpTrait)})`]
959
+ : []),
960
+ ...(spec.rootKeyDictionary && structCtx.isOpIo
961
+ ? [`T.KeyDictionary(KEY_DICTIONARY)`]
962
+ : []),
963
+ ];
964
+ const tail = pipes.map((p) => `.pipe(${p})`).join("");
965
+
966
+ // Shapes whose emitted body is byte-identical are one shape wearing
967
+ // many names. The docs pipelines generate a fresh copy of every nested
968
+ // shape per operation, so cloudflare's zero_trust carries 374 copies of
969
+ // `{ group?: unknown }` — one per operation x application type x
970
+ // include/exclude/require — and 72% of its 16,800 structures are
971
+ // redundant that way.
972
+ //
973
+ // Emit the body once and alias the rest to it. The public name is kept,
974
+ // so nothing about the surface changes; only the duplicate bodies go.
975
+ // Operation I/O is excluded: those carry the operation's Http trait and
976
+ // must never be merged onto one another.
977
+ const bodyKey = `${JSON.stringify(fields)}|${struct}${tail}`;
978
+ const canonical = structCtx.isOpIo
979
+ ? undefined
980
+ : structBodies.get(bodyKey);
981
+ if (canonical !== undefined) {
982
+ out.push(`export type ${name} = ${canonical.name};`);
983
+ out.push(`export const ${name} = ${canonical.name};\n`);
984
+ // Later shapes referencing this one now render the canonical name,
985
+ // which is what lets their bodies collapse too.
986
+ canonicalId.set(id, canonical.id);
987
+ aliased++;
988
+ } else {
989
+ if (!structCtx.isOpIo) {
990
+ structBodies.set(bodyKey, { id, name });
991
+ }
992
+ out.push(interfaceDecl(name, fields));
993
+ out.push(
994
+ suspendConst({
995
+ name,
996
+ pure,
997
+ multiline: true,
998
+ annotateIdentifier: true,
999
+ expr: `${struct}${tail}`,
1000
+ }),
1001
+ );
1002
+ }
1003
+ } else if (d.type === "list") {
1004
+ const nullable =
1005
+ spec.nullableTrait !== undefined &&
1006
+ spec.nullableTrait in (d.member.traits ?? {});
1007
+ const item = ref(d.member.target, i);
1008
+ out.push(
1009
+ `export type ${name} = Array<${tsRefAt(d.member.target, id)}${nullable ? " | null" : ""}>;`,
1010
+ );
1011
+ out.push(
1012
+ `export const ${name} = ${pure}S.Array(${nullable ? `S.NullOr(${item})` : item}) as any as S.Schema<${name}>;\n`,
1013
+ );
1014
+ } else if (d.type === "map") {
1015
+ const nullable =
1016
+ spec.nullableTrait !== undefined &&
1017
+ spec.nullableTrait in (d.value.traits ?? {});
1018
+ const value = ref(d.value.target, i);
1019
+ out.push(
1020
+ `export type ${name} = { [key: string]: ${tsRefAt(d.value.target, id)}${nullable ? " | null" : ""} | undefined };`,
1021
+ );
1022
+ out.push(
1023
+ `export const ${name} = ${pure}S.Record(S.String, ${nullable ? `S.NullOr(${value})` : value}) as any as S.Schema<${name}>;\n`,
1024
+ );
1025
+ } else if (d.type === "union") {
1026
+ // A union arm targeting the union itself carries no information
1027
+ // (`type X = X | A | B` ≡ `A | B`) and is an illegal circular type
1028
+ // alias. Specs produce them: atlas's OnlineArchiveSchedule is a
1029
+ // discriminated oneOf whose DEFAULT arm is `allOf: [$ref <the union
1030
+ // itself>]`, so the arm resolves back to its own parent.
1031
+ const caseTargets = Object.values(d.members ?? {})
1032
+ .map((m: any) => m.target)
1033
+ .filter((t: string) => t !== id);
1034
+ const caseKeys = caseTargets.map((t: string) => {
1035
+ const cd = shapes[t];
1036
+ return cd?.type === "structure"
1037
+ ? memberInfos(cd).map((mi) => mi.tsName)
1038
+ : [];
1039
+ });
1040
+ if (spec.union) {
1041
+ out.push(...spec.union({ name, caseTargets, caseKeys, tsRef }));
1042
+ } else if (spec.unionStyle === "opaque-cases") {
1043
+ const disc = unionDiscriminator(caseTargets);
1044
+ out.push(
1045
+ `export type ${name} = ${caseTargets.map((t) => tsRefAt(t, id)).join(" | ") || "unknown"};`,
1046
+ `export const ${name} = ${pure}S.Unknown.pipe(T.UnionCases(${JSON.stringify(caseKeys)}${disc ? `, ${JSON.stringify(disc)}` : ""}));\n`,
1047
+ );
1048
+ } else {
1049
+ throw new Error(
1050
+ `no union emission configured for shape ${id} — set unionStyle or union`,
1051
+ );
1052
+ }
1053
+ } else if (d.type === "enum") {
1054
+ const values = Object.values(d.members ?? {})
1055
+ .map((m: any) => m.traits?.["smithy.api#enumValue"])
1056
+ .filter((v: unknown): v is string => typeof v === "string");
1057
+ out.push(...enumDecl({ name, values, pure }));
1058
+ } else if (d.type === "intEnum") {
1059
+ // CLOSED numeric literal union alias (`type Y = 1 | 30`); INPUT
1060
+ // references re-open it inline (`Y | (number & {})`). Schema stays
1061
+ // S.Number (protocols never validate enum membership).
1062
+ const values = Object.values(d.members ?? {})
1063
+ .map((m: any) => m.traits?.["smithy.api#enumValue"])
1064
+ .filter((v: unknown): v is number => typeof v === "number");
1065
+ const union = values.length ? values.join(" | ") : "number";
1066
+ out.push(
1067
+ `export type ${name} = ${union};`,
1068
+ `export const ${name} = S.Number;\n`,
1069
+ );
1070
+ }
1071
+ });
1072
+
1073
+ // 6. Operations — declarative emission from the names in operationDecl,
1074
+ // unless the provider overrides the whole shape.
1075
+ /**
1076
+ * The element type `.items()` yields, resolved from the pagination
1077
+ * trait's items PATH against the response shape.
1078
+ *
1079
+ * `makePaginated` can't infer this — the path is a runtime string — and
1080
+ * the structural `API.PaginatedItem` fallback only recognizes bare arrays
1081
+ * and `{ result: […] }` envelopes, so every other envelope degrades to
1082
+ * `unknown` (distilled #302). Resolving it here and passing it as an
1083
+ * explicit type argument is what makes `.items()` typed.
1084
+ *
1085
+ * Returns undefined when the path doesn't lead to a list, which leaves
1086
+ * the annotation on the structural fallback rather than asserting a type
1087
+ * the shape doesn't support.
1088
+ */
1089
+ const paginatedItemTsType = (
1090
+ outputId: string,
1091
+ itemsPath: string,
1092
+ ): string | undefined => {
1093
+ // No items path: `.items()` is a page passthrough at runtime, so an
1094
+ // item IS a whole response.
1095
+ if (!itemsPath) return tsRef(outputId);
1096
+ let id: string = outputId;
1097
+ // Whether the path crossed a list on the way down (`"edges.node"` on a
1098
+ // Relay connection). When it did, the path itself is what fans out, so
1099
+ // the value it lands on IS one item — it needn't be a list again.
1100
+ let fannedOut = false;
1101
+ for (const segment of itemsPath.split(".")) {
1102
+ let def: any = shapes[id];
1103
+ if (def?.type === "list") {
1104
+ id = def.member.target as string;
1105
+ def = shapes[id];
1106
+ fannedOut = true;
1107
+ }
1108
+ if (def?.type !== "structure") return undefined;
1109
+ const info = memberInfos(def).find((m) => m.tsName === segment);
1110
+ if (!info) return undefined;
1111
+ id = info.target;
1112
+ }
1113
+ const last: any = shapes[id];
1114
+ if (last?.type === "list") return tsRef(last.member.target);
1115
+ return fannedOut ? tsRef(id) : undefined;
1116
+ };
1117
+
1118
+ const emitOperation =
1119
+ spec.operation ??
1120
+ ((ctx: OperationEmit): string => {
1121
+ const decl = spec.operationDecl;
1122
+ if (!decl) {
1123
+ throw new Error("SdkSpec needs either operationDecl or operation");
1124
+ }
1125
+ const errList = [...ctx.errorNames, ...decl.commonErrorClasses];
1126
+ const paginated = ctx.pagination !== undefined;
1127
+ const itemTsType = paginated
1128
+ ? paginatedItemTsType(
1129
+ ctx.op.def.__output,
1130
+ paginatedItemsPath.get(ctx.op.id) ?? "",
1131
+ )
1132
+ : undefined;
1133
+ const typeAnnotation =
1134
+ `API.${paginated ? "PaginatedOperationMethod" : "OperationMethod"}<\n` +
1135
+ ` ${ctx.inputName},\n` +
1136
+ ` ${ctx.outputTsType},\n` +
1137
+ ` ${ctx.opName}Error,\n` +
1138
+ ` ${decl.contextType}` +
1139
+ (itemTsType ? `,\n ${itemTsType}\n` : `\n`) +
1140
+ `>`;
1141
+ const config =
1142
+ `{\n` +
1143
+ ` input: ${ctx.inputName},\n` +
1144
+ ` output: ${ctx.outputSchema},\n` +
1145
+ ` errors: [${errList.join(", ")}],\n` +
1146
+ ` protocol: ${(paginated && opProfile.get(ctx.op.id)?.protocol) || decl.protocol},\n` +
1147
+ ` retry: ${decl.retry},\n` +
1148
+ (decl.extraConfig?.(ctx) ?? []).map((l) => ` ${l},\n`).join("") +
1149
+ (paginated
1150
+ ? ` pagination: ${JSON.stringify(ctx.pagination)} as const,\n`
1151
+ : "") +
1152
+ `}`;
1153
+ return [
1154
+ errorUnionAlias(ctx.opName, ctx.errorNames, decl.commonErrorType),
1155
+ ...(ctx.doc ? [`/** ${ctx.doc} */`] : []),
1156
+ operationConst({
1157
+ exportName: ctx.exportName,
1158
+ typeAnnotation,
1159
+ factory: paginated ? "API.makePaginated" : "API.make",
1160
+ pure,
1161
+ extraArg: paginated ? opProfile.get(ctx.op.id)?.strategy : undefined,
1162
+ config,
1163
+ // `makePaginated` infers the items element from the structural
1164
+ // fallback; an explicit item type needs the same `as any as`
1165
+ // idiom the schema consts use.
1166
+ castToAnnotation: itemTsType !== undefined,
1167
+ }),
1168
+ ].join("\n");
1169
+ });
1170
+
1171
+ for (const op of selected) {
1172
+ const opName = local(op.id);
1173
+ const errNames = ((op.def.errors ?? []) as Array<{ target: string }>)
1174
+ .map((e) => local(e.target))
1175
+ .filter((n) => errorNames.has(n));
1176
+ out.push(
1177
+ emitOperation({
1178
+ op,
1179
+ opName,
1180
+ exportName: opExportName(opName),
1181
+ inputName: local(op.def.__input),
1182
+ outputName: local(op.def.__output),
1183
+ outputTsType: tsRef(op.def.__output),
1184
+ outputSchema: ref(op.def.__output, indexOf.size),
1185
+ errorNames: errNames,
1186
+ doc: oneLine(op.def.traits?.["smithy.api#documentation"]),
1187
+ pagination: op.def.__pagination,
1188
+ }),
1189
+ );
1190
+ }
1191
+
1192
+ // 7. Alias re-exports, then provider trailing sections.
1193
+ const emittedOps = new Set(selected.map((op) => opExportName(local(op.id))));
1194
+ for (const { alias, target } of spec.opAliases ?? []) {
1195
+ if (!emittedOps.has(target) || emittedOps.has(alias)) continue;
1196
+ emittedOps.add(alias);
1197
+ const A = upperFirst(alias);
1198
+ const T2 = upperFirst(target);
1199
+ out.push(
1200
+ `// Alias of ${target} (same route, alternate export name upstream).\n` +
1201
+ `export const ${alias} = ${target};\n` +
1202
+ `export type ${A}Request = ${T2}Request;\n` +
1203
+ `export type ${A}Response = ${T2}Response;\n` +
1204
+ `export type ${A}Error = ${T2}Error;\n`,
1205
+ );
1206
+ }
1207
+ if (spec.footer) {
1208
+ out.push(...spec.footer({ emittedOps }));
1209
+ }
1210
+
1211
+ // Default header: banner + imports derived from operationDecl names and
1212
+ // the conventional SDK module layout, op error/context type re-exports,
1213
+ // and the KEY_DICTIONARY const when configured.
1214
+ const defaultHeader = (ctx: { hasPaginated: boolean }): string => {
1215
+ const decl = spec.operationDecl;
1216
+ if (!decl) {
1217
+ throw new Error(
1218
+ "the default header needs operationDecl — or pass header",
1219
+ );
1220
+ }
1221
+ const retryNs = decl.retry.split(".")[0];
1222
+ // Imports the used pagination profiles pull in: their protocol consts
1223
+ // (from the protocol module) and strategy/injected-member names (from
1224
+ // the pagination module).
1225
+ const profileProtocols = [
1226
+ ...new Set(
1227
+ [...usedProfiles]
1228
+ .map((p) => p.protocol)
1229
+ .filter((p): p is string => p !== undefined && p !== decl.protocol),
1230
+ ),
1231
+ ];
1232
+ const pagImports = [
1233
+ ...new Set(
1234
+ [...usedProfiles].flatMap((p) => [
1235
+ ...(p.strategy ? [p.strategy] : []),
1236
+ ...(p.injectOutputMember?.imports ?? []),
1237
+ ]),
1238
+ ),
1239
+ ];
1240
+ return (
1241
+ `// AUTO-GENERATED by scripts/generate.ts${
1242
+ spec.sourceNote ? ` from ${spec.sourceNote}` : ""
1243
+ }. Do not edit.\n` +
1244
+ `import * as S from "@distilled.cloud/core/schema";\n` +
1245
+ `import * as API from "@distilled.cloud/core/api";\n` +
1246
+ (usesCategories
1247
+ ? `import * as C from "@distilled.cloud/core/category";\n`
1248
+ : "") +
1249
+ `import * as T from "../traits.ts";\n` +
1250
+ `import {\n` +
1251
+ ` ${decl.protocol},\n` +
1252
+ profileProtocols.map((p) => ` ${p},\n`).join("") +
1253
+ ` type ${decl.commonErrorType},\n` +
1254
+ ` type ${decl.contextType},\n` +
1255
+ `} from "../protocol.ts";\n` +
1256
+ (ctx.hasPaginated && pagImports.length
1257
+ ? `import { ${pagImports.join(", ")} } from "../pagination.ts";\n`
1258
+ : "") +
1259
+ `import { ${[...decl.commonErrorClasses].sort().join(", ")} } from "../errors.ts";\n` +
1260
+ `import * as ${retryNs} from "../retry.ts";\n\n` +
1261
+ // Re-exported so inferred provider types downstream can always name them.
1262
+ `export type { ${decl.commonErrorType}, ${decl.contextType} };\n\n` +
1263
+ (spec.rootKeyDictionary
1264
+ ? `/** ${spec.rootKeyDictionary.doc} */\n` +
1265
+ `const KEY_DICTIONARY: Record<string, string | ReadonlyArray<string>> = ${JSON.stringify(spec.rootKeyDictionary.dict)};\n\n`
1266
+ : "")
1267
+ );
1268
+ };
1269
+
1270
+ const header = (spec.header ?? defaultHeader)({
1271
+ hasPaginated: paginatedOutputs.size > 0,
1272
+ model,
1273
+ });
1274
+ const code = header + out.join("\n") + "\n";
1275
+ return {
1276
+ code: spec.postProcess?.(code) ?? code,
1277
+ operations: selected.length,
1278
+ };
1279
+ };
1280
+
1281
+ // Re-exported so provider specs can be written against the same helpers the
1282
+ // driver uses, without importing every codegen module individually.
1283
+ export { errorUnionAlias, operationConst, upperFirst, tsKey, q, local };