@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,1689 @@
1
+ /**
2
+ * OpenAPI → Smithy 2.0 JSON converter (dev-time only, provider-agnostic).
3
+ *
4
+ * Turns a Swagger 2.0 / OAS 3.0 / OAS 3.1 document into the same Smithy JSON
5
+ * model shape the other spec converters produce (`{ smithy: "2.0", metadata,
6
+ * shapes }`), so every OpenAPI-sourced provider flows through the one shared
7
+ * `generateService` compiler. Conversion fidelity follows distilled v0's
8
+ * `generate-openapi.ts` feature matrix:
9
+ *
10
+ * • one operation per (path × get/post/put/patch/delete — plus head/options
11
+ * when a provider opts in via `extraHttpMethods`), deprecated skipped by
12
+ * default; op shape name = PascalCase(operationId), or verbNoun
13
+ * (`Apps_list` → `ListApps`) when {@link OpenApiConvertOptions.operationNaming}
14
+ * is `"verbNoun"`
15
+ * • input `<Op>Request`: path params → `smithy.api#httpLabel` (+required),
16
+ * query params → `smithy.api#httpQuery`, header params →
17
+ * `smithy.api#httpHeader` when `headerParams` is on (dropped otherwise,
18
+ * as cookie params always are),
19
+ * body properties flattened alongside
20
+ * (labels win, then query, then headers, then body);
21
+ * non-object bodies become a sole `body` member with `smithy.api#httpPayload`
22
+ * • `$ref`s become NAMED shapes (`components/schemas/X` → `<ns>#X`), reused
23
+ * across operations; anonymous nested objects synthesize names from the
24
+ * parent + member path
25
+ * • responses: 200 → 201 → 204 precedence (`successStatuses` overrides),
26
+ * `application/json` only; object
27
+ * results become `<Op>Response` structures (a sole `$ref` reuses the named
28
+ * shape); bare array/scalar results wrap in a structure whose single
29
+ * member carries `com.distilled.openapi#rawResponse` (the SdkSpec maps it
30
+ * to a root pipe); no schema → `smithy.api#Unit`; `head` is always
31
+ * `smithy.api#Unit`, whatever content the spec declares
32
+ * • nullability (3.0 `nullable`, 3.1 `type: [..., "null"]`, 2.0
33
+ * `x-nullable`, `oneOf`/`anyOf` null branches) → member-level
34
+ * `com.distilled.openapi#nullable` trait
35
+ * • sensitive string members (x-sensitive or name-pattern match) →
36
+ * `smithy.api#sensitive` on the member (direction-agnostic; the SdkSpec
37
+ * decides the input/output treatment)
38
+ * • per-op non-2xx statuses (outside `defaultErrorStatuses`) → typed error
39
+ * shapes with `smithy.api#error`/`smithy.api#httpError` and
40
+ * `com.distilled.openapi#errorMatchers` status matchers
41
+ * • v0 pagination detection (`pagination.cursor` / `.next` / `.next_page`,
42
+ * `next_token`/`NextToken`/`nextToken`, `next_page` + input alias lists +
43
+ * first top-level array property) → `smithy.api#paginated` on the op
44
+ * (with the non-standard `mode` member the core runtime dispatches on)
45
+ */
46
+
47
+ import {
48
+ isMechanicalOperationId,
49
+ isVerbatimRouteId,
50
+ pathToVerbNoun,
51
+ resolveOperationName,
52
+ toVerbNoun,
53
+ type OperationIdRewrite,
54
+ } from "./rewrite-operation-ids.ts";
55
+
56
+ // ============================================================================
57
+ // Trait ids (the `com.distilled.openapi` vocabulary)
58
+ // ============================================================================
59
+
60
+ /** Member-level nullability (`S.NullOr` + `| null` via `SdkSpec.nullableTrait`). */
61
+ export const NULLABLE_TRAIT = "com.distilled.openapi#nullable";
62
+ /**
63
+ * Marks the sole member of a synthesized response wrapper for bare
64
+ * array/scalar response bodies. SdkSpecs bind it with a `rootPipe`
65
+ * (`T.RawResponseRoot()` from `core/protocol-rest`), so the emitted response
66
+ * type IS the payload type.
67
+ */
68
+ export const RAW_RESPONSE_TRAIT = "com.distilled.openapi#rawResponse";
69
+ /**
70
+ * Wire-matching rules for a generated error class (see
71
+ * `SdkSpec.errorMatchersTrait` + `applyErrorMatchers`). The converter stamps
72
+ * `[{ status }]` from the response status the class was mapped from.
73
+ */
74
+ export const ERROR_MATCHERS_TRAIT = "com.distilled.openapi#errorMatchers";
75
+ /**
76
+ * Non-JSON request body encoding chosen by the v0 content-type precedence
77
+ * (json > form-urlencoded > multipart). Value: `"form-urlencoded"` |
78
+ * `"multipart"`. `"multipart"` is also merged into the `smithy.api#http`
79
+ * trait's `contentType` (which core's `buildRequest` understands);
80
+ * form-urlencoded is left to provider SdkSpecs/protocols.
81
+ */
82
+ export const CONTENT_TYPE_TRAIT = "com.distilled.openapi#contentType";
83
+ /**
84
+ * The `apiVersion` recorded on every operation when
85
+ * {@link OpenApiConvertOptions.apiVersion} is set (Azure ARM style). The
86
+ * matching `api-version` query parameter is dropped from inputs; the
87
+ * provider's protocol injects it at request time.
88
+ */
89
+ export const API_VERSION_TRAIT = "com.distilled.openapi#apiVersion";
90
+
91
+ // ============================================================================
92
+ // Sensitive-field name patterns (distilled v0's SENSITIVE_FIELD_PATTERNS)
93
+ // ============================================================================
94
+
95
+ export const SENSITIVE_FIELD_PATTERNS: readonly RegExp[] = [
96
+ /password/i,
97
+ /^secret$/i,
98
+ /secret[-_]?key/i,
99
+ /[-_]secret$/i,
100
+ /^client[-_]?secret$/i,
101
+ /^access[-_]?token$/i,
102
+ /^refresh[-_]?token$/i,
103
+ /^api[-_]?key$/i,
104
+ /^api[-_]?key[-_]?secret$/i,
105
+ /^api[-_]?token$/i,
106
+ /^private[-_]?key$/i,
107
+ /^secret[-_]?access[-_]?key$/i,
108
+ /^session[-_]?token$/i,
109
+ /^access[-_]?key[-_]?id$/i,
110
+ /^one[-_]?time[-_]?password$/i,
111
+ /^connection[-_]?string$/i,
112
+ /^connection[-_]?uri$/i,
113
+ /^plain[-_]?text$/i,
114
+ /^plain[-_]?text[-_]?refresh[-_]?token$/i,
115
+ ];
116
+
117
+ // ============================================================================
118
+ // Options
119
+ // ============================================================================
120
+
121
+ export interface OpenApiConvertOptions {
122
+ /** Shape namespace, e.g. `"com.neon.api"`. */
123
+ readonly namespace: string;
124
+ /** Service shape name (PascalCased), e.g. `"Neon"`. */
125
+ readonly serviceName: string;
126
+ /** Service shape `version`. Default: the spec's `info.version`, else "1.0". */
127
+ readonly serviceVersion?: string;
128
+ /**
129
+ * Non-2xx response status → error class name. Statuses in
130
+ * {@link defaultErrorStatuses} never produce per-op errors. Default:
131
+ * `{400: BadRequest, 403: Forbidden, 404: NotFound, 409: Conflict,
132
+ * 422: UnprocessableEntity}` (the v0 default).
133
+ */
134
+ readonly statusToErrorClass?: Readonly<Record<string, string>>;
135
+ /**
136
+ * Statuses covered by the SDK's common errors, excluded from per-op error
137
+ * lists. Default: `{"401","429","500","503"}` (the v0 default).
138
+ */
139
+ readonly defaultErrorStatuses?: Iterable<string>;
140
+ /** Skip operations marked `deprecated: true`. Default true. */
141
+ readonly skipDeprecated?: boolean;
142
+ /**
143
+ * HTTP methods to convert IN ADDITION to get/post/put/patch/delete —
144
+ * currently `"head"` and `"options"`. Opt-in, so a provider that models
145
+ * them (Vercel probes cache artifacts and sandbox files with HEAD) gets
146
+ * them without every other provider silently gaining operations.
147
+ *
148
+ * `head` operations always output `smithy.api#Unit` — see the output-shape
149
+ * comment in {@link convertOpenApiToSmithy} for why a declared response
150
+ * body on a HEAD can never arrive.
151
+ */
152
+ readonly extraHttpMethods?: readonly ("head" | "options")[];
153
+ /**
154
+ * Emit `in: header` parameters as `smithy.api#httpHeader` members instead
155
+ * of dropping them. Opt-in for the same reason as
156
+ * {@link extraHttpMethods}: turning it on adds input members to every
157
+ * operation whose spec declares a header parameter, and most providers
158
+ * declare ones the protocol already sends itself (Accept, Authorization,
159
+ * an API-version pin). Providers whose headers are real per-call inputs
160
+ * — Vercel's `x-Artifact-*` remote-cache metadata, `x-Vercel-Digest` —
161
+ * ask for them.
162
+ */
163
+ readonly headerParams?: boolean;
164
+ /**
165
+ * Response statuses to read the operation's output shape from, most
166
+ * preferred first. Default `["200", "201", "204"]`. Extend it for an API
167
+ * that answers asynchronous work with a body under another status — Vercel
168
+ * returns `202 Accepted` with a payload from nine endpoints (artifact
169
+ * upload, account deletion, VCR blob/manifest writes), which would
170
+ * otherwise generate as a `void` output.
171
+ */
172
+ readonly successStatuses?: readonly string[];
173
+ /**
174
+ * Azure-style fixed `api-version`: drops `api-version` query params and
175
+ * stamps {@link API_VERSION_TRAIT} on every operation.
176
+ */
177
+ readonly apiVersion?: string;
178
+ /**
179
+ * Name patterns marking string members sensitive. Default
180
+ * {@link SENSITIVE_FIELD_PATTERNS}. Pass `[]` to disable name-based
181
+ * detection (explicit `x-sensitive: true` still applies).
182
+ */
183
+ readonly sensitivePatterns?: readonly RegExp[];
184
+ /**
185
+ * Optional full shape-definition overrides for emitted error classes
186
+ * (keyed by class name) — e.g. custom members. The converter's default is
187
+ * an empty structure with `smithy.api#error` + `smithy.api#httpError` +
188
+ * {@link ERROR_MATCHERS_TRAIT} traits; overrides are merged over it.
189
+ */
190
+ readonly errorShapes?: Readonly<Record<string, any>>;
191
+ /**
192
+ * How to turn an OpenAPI `operationId` into the Smithy/SDK operation name.
193
+ * This is a convert policy, not a spec patch — the OpenAPI document is
194
+ * left alone.
195
+ *
196
+ * - `"verbNoun"` (default): `Apps_list` / `ConfigsList` / `showContact` →
197
+ * `listApps` / `listConfigs` / `getApp`. Already verb-first ids stay.
198
+ * An operation with no `operationId`, or a mechanical one that only
199
+ * restates the method and path (`get-api-card`, `post_v1_users`), is
200
+ * named from the route instead: `GET /users` → `listUsers`,
201
+ * `GET /users/{id}` → `getUser`, `POST /users/{id}/reset` →
202
+ * `resetUser`.
203
+ * - `"as-is"`: `PascalCase(operationId)` (`Apps_list` → `AppsList`).
204
+ *
205
+ * {@link operationNames} overrides win (lookup by `"METHOD path"`, then
206
+ * operationId).
207
+ */
208
+ readonly operationNaming?: "as-is" | "verbNoun";
209
+ /**
210
+ * Per-operation name overrides for {@link operationNaming}. Use
211
+ * `"METHOD path"` keys when two methods share an `operationId`.
212
+ */
213
+ readonly operationNames?: OperationIdRewrite;
214
+ }
215
+
216
+ export interface SmithyModel {
217
+ smithy: "2.0";
218
+ metadata: any;
219
+ shapes: Record<string, any>;
220
+ }
221
+
222
+ // ============================================================================
223
+ // String helpers (same conventions as cloudflare's spec-to-smithy)
224
+ // ============================================================================
225
+
226
+ const PRELUDE = {
227
+ Unit: "smithy.api#Unit",
228
+ String: "smithy.api#String",
229
+ Boolean: "smithy.api#Boolean",
230
+ Double: "smithy.api#Double",
231
+ Integer: "smithy.api#Integer",
232
+ Document: "smithy.api#Document",
233
+ } as const;
234
+
235
+ /** snake/kebab/space/camel → PascalCase identifier (inner caps preserved). */
236
+ const pascal = (s: string): string => {
237
+ const parts = s.split(/[^A-Za-z0-9]+/).filter(Boolean);
238
+ let out = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
239
+ if (out === "") out = "Shape";
240
+ if (/^[0-9]/.test(out)) out = `_${out}`;
241
+ return out;
242
+ };
243
+
244
+ /** Keep a JSON field name if it's a valid Smithy member identifier, else sanitize. */
245
+ const memberIdent = (name: string): string => {
246
+ let out = name.replace(/[^A-Za-z0-9_]/g, "_");
247
+ if (/^[0-9]/.test(out)) out = `_${out}`;
248
+ return out || "_";
249
+ };
250
+
251
+ /**
252
+ * Header name → member identifier (`x-Artifact-Tag` → `xArtifactTag`). The
253
+ * wire name is carried by the `smithy.api#httpHeader` trait, so the member
254
+ * can read like the rest of the input rather than like a header.
255
+ */
256
+ const headerMemberName = (name: string): string => {
257
+ const parts = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
258
+ if (parts.length === 0) return "_";
259
+ const out = parts
260
+ .map((p, i) =>
261
+ i === 0
262
+ ? p.charAt(0).toLowerCase() + p.slice(1)
263
+ : p.charAt(0).toUpperCase() + p.slice(1),
264
+ )
265
+ .join("");
266
+ return /^[0-9]/.test(out) ? `_${out}` : out;
267
+ };
268
+
269
+ const enumMemberName = (value: string): string => {
270
+ let out = value
271
+ .toUpperCase()
272
+ .replace(/[^A-Z0-9]+/g, "_")
273
+ .replace(/^_+|_+$/g, "");
274
+ if (out === "") out = "VALUE";
275
+ if (/^[0-9]/.test(out)) out = `_${out}`;
276
+ return out;
277
+ };
278
+
279
+ // ============================================================================
280
+ // Conversion context
281
+ // ============================================================================
282
+
283
+ const MAX_SCHEMA_DEPTH = 40;
284
+
285
+ /**
286
+ * Conversion direction: `"in"` for request-position schemas (drop
287
+ * `readOnly: true` members), `"out"` for response-position schemas (drop
288
+ * `writeOnly: true` members).
289
+ */
290
+ type Dir = "in" | "out";
291
+
292
+ interface Ctx {
293
+ readonly spec: any;
294
+ readonly version: "2.0" | "3.0" | "3.1";
295
+ readonly ns: string;
296
+ readonly shapes: Record<string, any>;
297
+ readonly names: Set<string>;
298
+ /**
299
+ * Variant-keyed `$ref` cache (`"*:<ref>"` for direction-agnostic schemas,
300
+ * `"in:<ref>"`/`"out:<ref>"` for direction-sensitive ones) → converted
301
+ * result (cycle-safe; mutated in place).
302
+ */
303
+ readonly refs: Map<string, { target: string; nullable: boolean }>;
304
+ /**
305
+ * Per-direction set of `$ref` pointers whose reachable schema graph
306
+ * contains the direction's excluded flag (see {@link dirSensitive}).
307
+ * Built lazily, once per direction, by {@link buildDirSensitiveRefs}.
308
+ */
309
+ readonly dirSensitiveRefs: Map<Dir, ReadonlySet<string>>;
310
+ readonly sensitivePatterns: readonly RegExp[];
311
+ }
312
+
313
+ interface Converted {
314
+ target: string;
315
+ nullable: boolean;
316
+ }
317
+
318
+ const uniqueName = (ctx: Ctx, base: string): string => {
319
+ const want = pascal(base);
320
+ let name = want;
321
+ let n = 2;
322
+ while (ctx.names.has(name)) name = `${want}${n++}`;
323
+ ctx.names.add(name);
324
+ return name;
325
+ };
326
+
327
+ const addShape = (ctx: Ctx, base: string, def: any): string => {
328
+ const id = `${ctx.ns}#${uniqueName(ctx, base)}`;
329
+ ctx.shapes[id] = def;
330
+ return id;
331
+ };
332
+
333
+ const detectVersion = (spec: any): "2.0" | "3.0" | "3.1" => {
334
+ if (spec?.swagger === "2.0") return "2.0";
335
+ const v = spec?.openapi;
336
+ if (typeof v === "string" && v.startsWith("3.1")) return "3.1";
337
+ if (typeof v === "string" && v.startsWith("3.0")) return "3.0";
338
+ throw new Error(
339
+ `Unsupported OpenAPI version (swagger=${spec?.swagger}, openapi=${spec?.openapi})`,
340
+ );
341
+ };
342
+
343
+ /** Resolve a local `#/...` JSON pointer against the spec document. */
344
+ const resolvePointer = (spec: any, ref: string): any => {
345
+ if (typeof ref !== "string" || !ref.startsWith("#/")) return undefined;
346
+ let cur = spec;
347
+ for (const raw of ref.slice(2).split("/")) {
348
+ const seg = raw.replace(/~1/g, "/").replace(/~0/g, "~");
349
+ if (cur === null || typeof cur !== "object") return undefined;
350
+ cur = cur[seg];
351
+ }
352
+ return cur;
353
+ };
354
+
355
+ /** Follow `$ref` chains (bounded) to the underlying schema object. */
356
+ const deref = (ctx: Ctx, def: any): any => {
357
+ let cur = def;
358
+ for (let i = 0; i < 16 && cur && typeof cur === "object" && cur.$ref; i++) {
359
+ cur = resolvePointer(ctx.spec, cur.$ref);
360
+ }
361
+ return cur;
362
+ };
363
+
364
+ // ============================================================================
365
+ // allOf flattening + nullability
366
+ // ============================================================================
367
+
368
+ interface FlatObject {
369
+ properties: Record<string, any>;
370
+ required: Set<string>;
371
+ isObject: boolean;
372
+ /** First `additionalProperties` schema seen (allOf-merged map bodies). */
373
+ additionalProperties?: any;
374
+ }
375
+
376
+ /**
377
+ * Flatten a schema into a property bag: `$ref`s resolved, `allOf` merged
378
+ * (properties + required unioned across resolved subschemas). A
379
+ * `oneOf`/`anyOf` encountered INSIDE an `allOf` merges its branches'
380
+ * properties as OPTIONAL members — the loosest satisfiable reading of the
381
+ * intersection — rather than dropping them; a top-level union is left to
382
+ * the union conversion path.
383
+ */
384
+ const flattenObject = (ctx: Ctx, def: any, depth = 0): FlatObject => {
385
+ const out: FlatObject = {
386
+ properties: {},
387
+ required: new Set(),
388
+ isObject: false,
389
+ };
390
+ // Revisiting a `$ref` under the same flags merges nothing new (property
391
+ // merge is first-wins) — dedupe so shared/cyclic allOf bases are walked
392
+ // once per flag combination instead of exponentially (MongoDB Atlas's
393
+ // polymorphic allOf/oneOf graph never finished under the bare depth cap).
394
+ const seen = new Set<string>();
395
+ const visit = (
396
+ d: any,
397
+ dep: number,
398
+ inAllOf: boolean,
399
+ inUnion: boolean,
400
+ ): void => {
401
+ if (dep > MAX_SCHEMA_DEPTH) return;
402
+ if (d && typeof d === "object" && typeof d.$ref === "string") {
403
+ const key = `${d.$ref}|${inAllOf}|${inUnion}`;
404
+ if (seen.has(key)) return;
405
+ seen.add(key);
406
+ }
407
+ const r = deref(ctx, d);
408
+ if (!r || typeof r !== "object") return;
409
+ if (Array.isArray(r.allOf)) {
410
+ for (const sub of r.allOf) visit(sub, dep + 1, true, inUnion);
411
+ }
412
+ if (inAllOf) {
413
+ const branches = r.oneOf ?? r.anyOf;
414
+ if (Array.isArray(branches)) {
415
+ for (const b of branches) {
416
+ if (!isNullBranch(ctx, b)) visit(b, dep + 1, inAllOf, true);
417
+ }
418
+ }
419
+ }
420
+ if (r.properties && typeof r.properties === "object") {
421
+ out.isObject = true;
422
+ for (const [k, v] of Object.entries(r.properties)) {
423
+ if (!(k in out.properties)) out.properties[k] = v;
424
+ }
425
+ }
426
+ if (r.type === "object") out.isObject = true;
427
+ if (
428
+ out.additionalProperties === undefined &&
429
+ r.additionalProperties !== undefined &&
430
+ r.additionalProperties !== false
431
+ ) {
432
+ out.additionalProperties = r.additionalProperties;
433
+ }
434
+ if (!inUnion && Array.isArray(r.required)) {
435
+ for (const k of r.required) out.required.add(k);
436
+ }
437
+ };
438
+ visit(def, depth, false, false);
439
+ return out;
440
+ };
441
+
442
+ /** Whether a `oneOf`/`anyOf` branch represents JSON null. */
443
+ const isNullBranch = (ctx: Ctx, branch: any): boolean => {
444
+ const r = deref(ctx, branch);
445
+ if (!r || typeof r !== "object") return false;
446
+ if (r.type === "null") return true;
447
+ if (Array.isArray(r.type) && r.type.every((t: unknown) => t === "null")) {
448
+ return true;
449
+ }
450
+ if (
451
+ Array.isArray(r.enum) &&
452
+ r.enum.length > 0 &&
453
+ r.enum.every((v: unknown) => v === null)
454
+ ) {
455
+ return true;
456
+ }
457
+ return false;
458
+ };
459
+
460
+ /** Intrinsic nullability flags on a schema node (not union null branches). */
461
+ const ownNullable = (ctx: Ctx, def: any): boolean => {
462
+ if (!def || typeof def !== "object") return false;
463
+ // `nullable: true` is 3.0 vocabulary but appears in real 3.1 (and even
464
+ // 2.0) documents — honor it everywhere rather than silently dropping the
465
+ // null arm.
466
+ if (def.nullable === true) return true;
467
+ if (def["x-nullable"] === true) return true;
468
+ if (Array.isArray(def.type) && def.type.includes("null")) return true;
469
+ return false;
470
+ };
471
+
472
+ /**
473
+ * Whether a property schema is excluded from the given direction:
474
+ * `readOnly: true` properties never appear in requests, `writeOnly: true`
475
+ * properties never appear in responses. Checks the property node itself and
476
+ * its `$ref` resolution.
477
+ */
478
+ const dirExcluded = (ctx: Ctx, prop: any, dir: Dir): boolean => {
479
+ const flag = dir === "in" ? "readOnly" : "writeOnly";
480
+ if (prop && typeof prop === "object" && prop[flag] === true) return true;
481
+ const r = deref(ctx, prop);
482
+ return !!r && typeof r === "object" && r[flag] === true;
483
+ };
484
+
485
+ /** Child schema slots the direction-sensitivity walk descends through. */
486
+ const schemaChildren = (d: any): any[] => [
487
+ ...(Array.isArray(d.allOf) ? d.allOf : []),
488
+ ...(Array.isArray(d.oneOf) ? d.oneOf : []),
489
+ ...(Array.isArray(d.anyOf) ? d.anyOf : []),
490
+ ...(d.properties && typeof d.properties === "object"
491
+ ? Object.values(d.properties)
492
+ : []),
493
+ ...(d.items ? [d.items] : []),
494
+ ...(d.additionalProperties && typeof d.additionalProperties === "object"
495
+ ? [d.additionalProperties]
496
+ : []),
497
+ ];
498
+
499
+ /**
500
+ * Walk an INLINE schema subtree (stopping at `$ref` nodes, which are
501
+ * appended to `refs` instead of followed). Returns whether the flag
502
+ * appears inline. Inline subtrees are JSON trees — no cycles.
503
+ */
504
+ const scanInlineForFlag = (d: any, flag: string, refs: string[]): boolean => {
505
+ if (!d || typeof d !== "object") return false;
506
+ if (typeof d.$ref === "string") {
507
+ refs.push(d.$ref);
508
+ return false;
509
+ }
510
+ if (d[flag] === true) return true;
511
+ for (const sub of schemaChildren(d)) {
512
+ if (scanInlineForFlag(sub, flag, refs)) return true;
513
+ }
514
+ return false;
515
+ };
516
+
517
+ /**
518
+ * Compute, for the WHOLE document at once, the set of `$ref` pointers that
519
+ * are direction-sensitive: the flag appears somewhere in the schema graph
520
+ * reachable from the ref's target. One inline scan per distinct ref target
521
+ * (local flag + outgoing ref edges), then reverse propagation from the
522
+ * locally-flagged nodes — linear in spec size, and cycles / diamond-shared
523
+ * refs cost nothing. (This replaces a per-call DFS whose memoization was
524
+ * disabled by ANY re-encountered ref; on large specs where every schema
525
+ * shares refs — e.g. MongoDB Atlas's ubiquitous `links` — that re-walked
526
+ * the graph for each of thousands of `$ref` sites and never finished.)
527
+ */
528
+ const buildDirSensitiveRefs = (ctx: Ctx, dir: Dir): ReadonlySet<string> => {
529
+ const flag = dir === "in" ? "readOnly" : "writeOnly";
530
+
531
+ // Every distinct `$ref` pointer in the document.
532
+ const allRefs = new Set<string>();
533
+ const collect = (d: any): void => {
534
+ if (Array.isArray(d)) {
535
+ for (const v of d) collect(v);
536
+ return;
537
+ }
538
+ if (!d || typeof d !== "object") return;
539
+ if (typeof d.$ref === "string") allRefs.add(d.$ref);
540
+ for (const v of Object.values(d)) collect(v);
541
+ };
542
+ collect(ctx.spec);
543
+
544
+ // ref → refs reachable in one hop; refs whose target carries the flag inline.
545
+ const reverse = new Map<string, string[]>();
546
+ const flagged: string[] = [];
547
+ for (const ref of allRefs) {
548
+ const outgoing: string[] = [];
549
+ if (scanInlineForFlag(resolvePointer(ctx.spec, ref), flag, outgoing)) {
550
+ flagged.push(ref);
551
+ continue; // already sensitive; its edges can't add anything
552
+ }
553
+ for (const to of outgoing) {
554
+ let from = reverse.get(to);
555
+ if (!from) reverse.set(to, (from = []));
556
+ from.push(ref);
557
+ }
558
+ }
559
+
560
+ // Sensitivity propagates from flagged targets to every ref that reaches them.
561
+ const sensitive = new Set<string>(flagged);
562
+ const queue = [...flagged];
563
+ while (queue.length > 0) {
564
+ const next = queue.pop()!;
565
+ for (const from of reverse.get(next) ?? []) {
566
+ if (!sensitive.has(from)) {
567
+ sensitive.add(from);
568
+ queue.push(from);
569
+ }
570
+ }
571
+ }
572
+ return sensitive;
573
+ };
574
+
575
+ /**
576
+ * Whether a schema subtree is direction-sensitive — contains
577
+ * `readOnly: true` (request direction) or `writeOnly: true` (response
578
+ * direction) anywhere, following local `$ref`s. Direction-sensitive named
579
+ * components convert to separate request/response shape variants
580
+ * (`<Name>Input`/`<Name>Output`); everything else is shared.
581
+ */
582
+ const dirSensitive = (ctx: Ctx, def: any, dir: Dir): boolean => {
583
+ let sensitiveRefs = ctx.dirSensitiveRefs.get(dir);
584
+ if (sensitiveRefs === undefined) {
585
+ sensitiveRefs = buildDirSensitiveRefs(ctx, dir);
586
+ ctx.dirSensitiveRefs.set(dir, sensitiveRefs);
587
+ }
588
+ const refs: string[] = [];
589
+ if (scanInlineForFlag(def, dir === "in" ? "readOnly" : "writeOnly", refs)) {
590
+ return true;
591
+ }
592
+ return refs.some((ref) => sensitiveRefs.has(ref));
593
+ };
594
+
595
+ /** Single non-array type from a possibly 3.1-style `type` value. */
596
+ const typeOf = (def: any): string | undefined => {
597
+ const t = def?.type;
598
+ if (typeof t === "string") return t;
599
+ if (Array.isArray(t)) {
600
+ const real = t.filter((x: unknown) => x !== "null");
601
+ if (real.length === 1 && typeof real[0] === "string") return real[0];
602
+ }
603
+ return undefined;
604
+ };
605
+
606
+ // ============================================================================
607
+ // Schema conversion
608
+ // ============================================================================
609
+
610
+ /**
611
+ * Whether a schema definition warrants a NAMED shape (vs an inline prelude
612
+ * target). Named shapes can participate in reference cycles, so `$ref`s to
613
+ * nameable schemas reserve their name before converting.
614
+ */
615
+ const isNameable = (ctx: Ctx, def: any): boolean => {
616
+ if (!def || typeof def !== "object") return false;
617
+ if (def.$ref) return isNameable(ctx, deref(ctx, def));
618
+ if (Array.isArray(def.enum)) {
619
+ const values = def.enum.filter((v: unknown) => v !== null);
620
+ return (
621
+ values.length > 0 &&
622
+ (values.every((v: unknown) => typeof v === "string") ||
623
+ values.every((v: unknown) => typeof v === "number"))
624
+ );
625
+ }
626
+ if (Array.isArray(def.allOf)) return true;
627
+ const branches = def.oneOf ?? def.anyOf;
628
+ if (Array.isArray(branches)) {
629
+ return branches.filter((b: any) => !isNullBranch(ctx, b)).length >= 2;
630
+ }
631
+ const t = typeOf(def);
632
+ if (t === "object" || def.properties || def.additionalProperties) return true;
633
+ if (t === "array") return true;
634
+ return false;
635
+ };
636
+
637
+ /**
638
+ * Convert one schema to a shape target. `dir` is the conversion direction
639
+ * (request vs response position — readOnly/writeOnly members are dropped
640
+ * accordingly). `reservedId` names the top-level shape when the caller
641
+ * pre-registered it (named `$ref` targets); otherwise anonymous shapes
642
+ * synthesize names from `hint`.
643
+ */
644
+ const convertSchema = (
645
+ ctx: Ctx,
646
+ def: any,
647
+ hint: string,
648
+ depth: number,
649
+ dir: Dir,
650
+ reservedId?: string,
651
+ ): Converted => {
652
+ const emit = (shapeDef: any, base: string): string => {
653
+ if (reservedId !== undefined) {
654
+ ctx.shapes[reservedId] = shapeDef;
655
+ return reservedId;
656
+ }
657
+ return addShape(ctx, base, shapeDef);
658
+ };
659
+ const inline = (target: string, nullable: boolean): Converted => {
660
+ // A reserved placeholder that turned out to be inline (scalar/alias):
661
+ // drop the placeholder; the burned name is harmless.
662
+ if (reservedId !== undefined) delete ctx.shapes[reservedId];
663
+ return { target, nullable };
664
+ };
665
+
666
+ if (depth > MAX_SCHEMA_DEPTH) return inline(PRELUDE.Document, false);
667
+ if (def === true || def === undefined || def === null) {
668
+ return inline(PRELUDE.Document, false);
669
+ }
670
+ if (typeof def !== "object") return inline(PRELUDE.Document, false);
671
+
672
+ // --- $ref → named (or cached inline) shape --------------------------------
673
+ if (def.$ref) {
674
+ // Sibling nullability next to the $ref (common tool output even where
675
+ // the spec says siblings are ignored) survives onto the member.
676
+ const siteNullable = ownNullable(ctx, def);
677
+ // Direction-sensitive components (readOnly members in request position,
678
+ // writeOnly in response position) get per-direction variants; the rest
679
+ // share one shape across both directions.
680
+ const sensitive = dirSensitive(ctx, def, dir);
681
+ const cacheKey = sensitive ? `${dir}:${def.$ref}` : `*:${def.$ref}`;
682
+ const cached = ctx.refs.get(cacheKey);
683
+ if (cached) return inline(cached.target, cached.nullable || siteNullable);
684
+ const resolved = resolvePointer(ctx.spec, def.$ref);
685
+ if (resolved === undefined) return inline(PRELUDE.Document, siteNullable);
686
+ const refName =
687
+ (String(def.$ref).split("/").pop() ?? "Shape") +
688
+ (sensitive ? (dir === "in" ? "Input" : "Output") : "");
689
+ if (!isNameable(ctx, resolved)) {
690
+ // Scalars and aliases can't cycle — convert inline and cache.
691
+ const entry: Converted = { target: PRELUDE.Document, nullable: false };
692
+ ctx.refs.set(cacheKey, entry);
693
+ const r = convertSchema(ctx, resolved, pascal(refName), depth + 1, dir);
694
+ entry.target = r.target;
695
+ entry.nullable = r.nullable;
696
+ return inline(r.target, r.nullable || siteNullable);
697
+ }
698
+ // Reserve the component name before converting so cycles resolve.
699
+ const id = `${ctx.ns}#${uniqueName(ctx, refName)}`;
700
+ const entry: Converted = { target: id, nullable: false };
701
+ ctx.refs.set(cacheKey, entry);
702
+ ctx.shapes[id] = { type: "structure", members: {} }; // placeholder
703
+ const r = convertSchema(ctx, resolved, pascal(refName), depth + 1, dir, id);
704
+ entry.target = r.target;
705
+ entry.nullable = r.nullable;
706
+ return inline(r.target, r.nullable || siteNullable);
707
+ }
708
+
709
+ let nullable = ownNullable(ctx, def);
710
+ const doc = typeof def.description === "string" ? def.description : undefined;
711
+ const docTraits = doc ? { "smithy.api#documentation": doc } : {};
712
+
713
+ // --- oneOf / anyOf --------------------------------------------------------
714
+ const branches = def.oneOf ?? def.anyOf;
715
+ if (Array.isArray(branches)) {
716
+ const real: any[] = [];
717
+ for (const b of branches) {
718
+ if (isNullBranch(ctx, b)) nullable = true;
719
+ else real.push(b);
720
+ }
721
+ if (real.length === 0) return inline(PRELUDE.Document, nullable);
722
+ if (real.length === 1) {
723
+ const r = convertSchema(ctx, real[0], hint, depth + 1, dir, reservedId);
724
+ return { target: r.target, nullable: nullable || r.nullable };
725
+ }
726
+ // Distinct branches → a union shape. Members are synthesized case names;
727
+ // duplicate targets are deduped.
728
+ const members: Record<string, any> = {};
729
+ const seenTargets = new Set<string>();
730
+ real.forEach((b, i) => {
731
+ const branchName =
732
+ typeof b?.$ref === "string"
733
+ ? pascal(String(b.$ref).split("/").pop() ?? `Case${i}`)
734
+ : `Case${i}`;
735
+ const r = convertSchema(ctx, b, `${hint}${branchName}`, depth + 1, dir);
736
+ if (seenTargets.has(r.target)) return;
737
+ seenTargets.add(r.target);
738
+ let mn = memberIdent(branchName);
739
+ let k = 2;
740
+ while (mn in members) mn = `${memberIdent(branchName)}_${k++}`;
741
+ members[mn] = { target: r.target };
742
+ });
743
+ const targets = Object.values(members);
744
+ if (targets.length === 1) {
745
+ return inline((targets[0] as any).target, nullable);
746
+ }
747
+ return {
748
+ target: emit({ type: "union", members, traits: docTraits }, hint),
749
+ nullable,
750
+ };
751
+ }
752
+
753
+ // --- allOf ----------------------------------------------------------------
754
+ if (Array.isArray(def.allOf)) {
755
+ // Single-entry allOf over a $ref: passthrough (v0 semantics), carrying
756
+ // the parent's nullability/description.
757
+ if (def.allOf.length === 1 && !def.properties) {
758
+ const r = convertSchema(
759
+ ctx,
760
+ def.allOf[0],
761
+ hint,
762
+ depth + 1,
763
+ dir,
764
+ reservedId,
765
+ );
766
+ return { target: r.target, nullable: nullable || r.nullable };
767
+ }
768
+ const flat = flattenObject(ctx, def, depth);
769
+ if (Object.keys(flat.properties).length === 0) {
770
+ // A property-less intersection of map schemas is a map, not an empty
771
+ // struct.
772
+ if (flat.additionalProperties !== undefined) {
773
+ const r = convertSchema(
774
+ ctx,
775
+ { type: "object", additionalProperties: flat.additionalProperties },
776
+ hint,
777
+ depth + 1,
778
+ dir,
779
+ reservedId,
780
+ );
781
+ return { target: r.target, nullable: nullable || r.nullable };
782
+ }
783
+ if (!flat.isObject) return inline(PRELUDE.Document, nullable);
784
+ }
785
+ const members = buildMembers(
786
+ ctx,
787
+ flat.properties,
788
+ flat.required,
789
+ hint,
790
+ depth + 1,
791
+ dir,
792
+ );
793
+ return {
794
+ target: emit({ type: "structure", members, traits: docTraits }, hint),
795
+ nullable,
796
+ };
797
+ }
798
+
799
+ // --- enum -----------------------------------------------------------------
800
+ if (Array.isArray(def.enum) && def.enum.length > 0) {
801
+ const values = def.enum.filter((v: unknown) => v !== null);
802
+ if (values.some((v: unknown) => v === null)) nullable = true;
803
+ if (values.length === 0) return inline(PRELUDE.Document, true);
804
+ if (values.every((v: unknown) => typeof v === "string")) {
805
+ const members: Record<string, any> = {};
806
+ const used = new Set<string>();
807
+ for (const lit of values as string[]) {
808
+ let mn = enumMemberName(lit);
809
+ let k = 2;
810
+ while (used.has(mn)) mn = `${enumMemberName(lit)}_${k++}`;
811
+ used.add(mn);
812
+ members[mn] = {
813
+ target: PRELUDE.Unit,
814
+ traits: { "smithy.api#enumValue": lit },
815
+ };
816
+ }
817
+ return {
818
+ target: emit({ type: "enum", members, traits: docTraits }, hint),
819
+ nullable,
820
+ };
821
+ }
822
+ // Numeric enums → intEnum shapes (closed numeric literal unions in the
823
+ // generated TS; the schema stays a plain number).
824
+ if (values.every((v: unknown) => typeof v === "number")) {
825
+ const members: Record<string, any> = {};
826
+ const used = new Set<string>();
827
+ for (const lit of values as number[]) {
828
+ let mn = enumMemberName(String(lit));
829
+ let k = 2;
830
+ while (used.has(mn)) mn = `${enumMemberName(String(lit))}_${k++}`;
831
+ used.add(mn);
832
+ members[mn] = {
833
+ target: PRELUDE.Unit,
834
+ traits: { "smithy.api#enumValue": lit },
835
+ };
836
+ }
837
+ return {
838
+ target: emit({ type: "intEnum", members, traits: docTraits }, hint),
839
+ nullable,
840
+ };
841
+ }
842
+ if (values.every((v: unknown) => typeof v === "boolean")) {
843
+ return inline(PRELUDE.Boolean, nullable);
844
+ }
845
+ return inline(PRELUDE.Document, nullable);
846
+ }
847
+
848
+ const t = typeOf(def);
849
+
850
+ // --- array ----------------------------------------------------------------
851
+ if (t === "array") {
852
+ const item = convertSchema(ctx, def.items, `${hint}Item`, depth + 1, dir);
853
+ return {
854
+ target: emit(
855
+ {
856
+ type: "list",
857
+ member: {
858
+ target: item.target,
859
+ ...(item.nullable ? { traits: { [NULLABLE_TRAIT]: {} } } : {}),
860
+ },
861
+ traits: docTraits,
862
+ },
863
+ reservedId !== undefined ? hint : `${hint}List`,
864
+ ),
865
+ nullable,
866
+ };
867
+ }
868
+
869
+ // --- object ---------------------------------------------------------------
870
+ if (t === "object" || def.properties || def.additionalProperties) {
871
+ if (def.properties && Object.keys(def.properties).length > 0) {
872
+ const required = new Set<string>(
873
+ Array.isArray(def.required) ? def.required : [],
874
+ );
875
+ const members = buildMembers(
876
+ ctx,
877
+ def.properties,
878
+ required,
879
+ hint,
880
+ depth + 1,
881
+ dir,
882
+ );
883
+ return {
884
+ target: emit({ type: "structure", members, traits: docTraits }, hint),
885
+ nullable,
886
+ };
887
+ }
888
+ const ap = def.additionalProperties;
889
+ if (ap !== undefined && ap !== false) {
890
+ const value =
891
+ ap === true || (typeof ap === "object" && Object.keys(ap).length === 0)
892
+ ? { target: PRELUDE.Document, nullable: false }
893
+ : convertSchema(ctx, ap, `${hint}Value`, depth + 1, dir);
894
+ return {
895
+ target: emit(
896
+ {
897
+ type: "map",
898
+ key: { target: PRELUDE.String },
899
+ value: {
900
+ target: value.target,
901
+ ...(value.nullable ? { traits: { [NULLABLE_TRAIT]: {} } } : {}),
902
+ },
903
+ traits: docTraits,
904
+ },
905
+ reservedId !== undefined ? hint : `${hint}Map`,
906
+ ),
907
+ nullable,
908
+ };
909
+ }
910
+ return inline(PRELUDE.Document, nullable);
911
+ }
912
+
913
+ // --- scalars --------------------------------------------------------------
914
+ switch (t) {
915
+ case "string":
916
+ return inline(PRELUDE.String, nullable);
917
+ case "boolean":
918
+ return inline(PRELUDE.Boolean, nullable);
919
+ case "integer":
920
+ return inline(PRELUDE.Integer, nullable);
921
+ case "number":
922
+ return inline(PRELUDE.Double, nullable);
923
+ case "null":
924
+ return inline(PRELUDE.Document, true);
925
+ default:
926
+ return inline(PRELUDE.Document, nullable);
927
+ }
928
+ };
929
+
930
+ /** Whether a property is a sensitive string (x-sensitive or name pattern). */
931
+ const isSensitiveProperty = (ctx: Ctx, name: string, def: any): boolean => {
932
+ const r = deref(ctx, def);
933
+ if (!r || typeof r !== "object") return false;
934
+ const isString = typeOf(r) === "string" && !Array.isArray(r.enum);
935
+ if (!isString) return false;
936
+ if (r["x-sensitive"] === true || def?.["x-sensitive"] === true) return true;
937
+ return ctx.sensitivePatterns.some((p) => p.test(name));
938
+ };
939
+
940
+ /**
941
+ * Build a structure's members from an OpenAPI property map. Direction-
942
+ * excluded properties (`readOnly` in requests, `writeOnly` in responses)
943
+ * are dropped — along with any response-side `required` they carried.
944
+ */
945
+ const buildMembers = (
946
+ ctx: Ctx,
947
+ properties: Record<string, any>,
948
+ required: ReadonlySet<string>,
949
+ hint: string,
950
+ depth: number,
951
+ dir: Dir,
952
+ ): Record<string, any> => {
953
+ const members: Record<string, any> = {};
954
+ for (const [name, prop] of Object.entries(properties)) {
955
+ if (dirExcluded(ctx, prop, dir)) continue;
956
+ let mn = memberIdent(name);
957
+ let k = 2;
958
+ while (mn in members) mn = `${memberIdent(name)}_${k++}`;
959
+ const conv = convertSchema(ctx, prop, `${hint}${pascal(name)}`, depth, dir);
960
+ const traits: Record<string, any> = {};
961
+ const doc =
962
+ prop && typeof prop === "object" && typeof prop.description === "string"
963
+ ? prop.description
964
+ : undefined;
965
+ if (doc) traits["smithy.api#documentation"] = doc;
966
+ if (required.has(name)) traits["smithy.api#required"] = {};
967
+ if (mn !== name) traits["smithy.api#jsonName"] = name;
968
+ if (conv.nullable) traits[NULLABLE_TRAIT] = {};
969
+ if (isSensitiveProperty(ctx, name, prop)) {
970
+ traits["smithy.api#sensitive"] = {};
971
+ }
972
+ members[mn] = {
973
+ target: conv.target,
974
+ ...(Object.keys(traits).length ? { traits } : {}),
975
+ };
976
+ }
977
+ return members;
978
+ };
979
+
980
+ // ============================================================================
981
+ // Parameters
982
+ // ============================================================================
983
+
984
+ interface Param {
985
+ in: string;
986
+ name: string;
987
+ required: boolean;
988
+ description?: string;
989
+ schema: any;
990
+ }
991
+
992
+ /** Normalize a (possibly `$ref`'d) parameter; Swagger 2.0 carries the schema inline. */
993
+ const normalizeParam = (ctx: Ctx, raw: any): Param | undefined => {
994
+ const p = raw?.$ref ? resolvePointer(ctx.spec, raw.$ref) : raw;
995
+ if (!p || typeof p !== "object" || typeof p.name !== "string") {
996
+ return undefined;
997
+ }
998
+ // Swagger 2.0: `in: body` parameters carry their (usually `$ref`'d)
999
+ // schema under `schema`; all other locations describe the type inline on
1000
+ // the parameter itself.
1001
+ const schema =
1002
+ ctx.version === "2.0"
1003
+ ? p.in === "body"
1004
+ ? (p.schema ?? {})
1005
+ : {
1006
+ type: p.type,
1007
+ enum: p.enum,
1008
+ items: p.items,
1009
+ format: p.format,
1010
+ "x-nullable": p["x-nullable"],
1011
+ }
1012
+ : (p.schema ?? { type: "string" });
1013
+ return {
1014
+ in: p.in,
1015
+ name: p.name,
1016
+ required: p.required === true || p.in === "path",
1017
+ description: typeof p.description === "string" ? p.description : undefined,
1018
+ schema,
1019
+ };
1020
+ };
1021
+
1022
+ /**
1023
+ * Path-level parameters merged before operation-level ones; an op-level
1024
+ * parameter overrides a path-level one with the same (in, name). Header and
1025
+ * cookie parameters are dropped (v0 parity); `in: body` (2.0) is handled by
1026
+ * the request-body path.
1027
+ */
1028
+ const collectParams = (ctx: Ctx, pathItem: any, op: any): Param[] => {
1029
+ const byKey = new Map<string, Param>();
1030
+ for (const raw of [
1031
+ ...(Array.isArray(pathItem?.parameters) ? pathItem.parameters : []),
1032
+ ...(Array.isArray(op?.parameters) ? op.parameters : []),
1033
+ ]) {
1034
+ const p = normalizeParam(ctx, raw);
1035
+ if (!p) continue;
1036
+ byKey.set(`${p.in}${p.name}`, p);
1037
+ }
1038
+ return [...byKey.values()].filter(
1039
+ (p) =>
1040
+ p.in === "path" ||
1041
+ p.in === "query" ||
1042
+ p.in === "body" ||
1043
+ p.in === "header",
1044
+ );
1045
+ };
1046
+
1047
+ /**
1048
+ * Simple-type coercion for path labels (Smithy restricts label targets):
1049
+ * enum → enum shape, integer → Integer, number → Double, boolean → Boolean,
1050
+ * everything else → String.
1051
+ */
1052
+ const labelTarget = (ctx: Ctx, schema: any, hint: string): string => {
1053
+ const r = deref(ctx, schema);
1054
+ if (
1055
+ Array.isArray(r?.enum) &&
1056
+ r.enum.length > 0 &&
1057
+ r.enum.every((v: unknown) => typeof v === "string")
1058
+ ) {
1059
+ return convertSchema(ctx, r, hint, 0, "in").target;
1060
+ }
1061
+ switch (typeOf(r)) {
1062
+ case "integer":
1063
+ return PRELUDE.Integer;
1064
+ case "number":
1065
+ return PRELUDE.Double;
1066
+ case "boolean":
1067
+ return PRELUDE.Boolean;
1068
+ default:
1069
+ return PRELUDE.String;
1070
+ }
1071
+ };
1072
+
1073
+ // ============================================================================
1074
+ // Docs
1075
+ // ============================================================================
1076
+
1077
+ /**
1078
+ * v0 trimming rules: stop at a `### Authorization` heading, strip markdown
1079
+ * table rows (lines starting with `|`).
1080
+ */
1081
+ const trimDescription = (desc: unknown): string | undefined => {
1082
+ if (typeof desc !== "string" || !desc) return undefined;
1083
+ const lines: string[] = [];
1084
+ for (const line of desc.split(/\r?\n/)) {
1085
+ if (line.trim().startsWith("### Authorization")) break;
1086
+ if (line.trim().startsWith("|")) continue;
1087
+ lines.push(line);
1088
+ }
1089
+ const out = lines.join("\n").trim();
1090
+ return out || undefined;
1091
+ };
1092
+
1093
+ const opDoc = (op: any): string | undefined => {
1094
+ const summary =
1095
+ typeof op.summary === "string" ? op.summary.trim() : undefined;
1096
+ const desc = trimDescription(op.description);
1097
+ const parts = [summary, desc].filter(
1098
+ (s): s is string => s !== undefined && s !== "",
1099
+ );
1100
+ return parts.length ? parts.join("\n\n") : undefined;
1101
+ };
1102
+
1103
+ // ============================================================================
1104
+ // Responses
1105
+ // ============================================================================
1106
+
1107
+ /** First declared status in `order` wins; response-level `$ref` resolved; JSON only. */
1108
+ const successSchema = (
1109
+ ctx: Ctx,
1110
+ responses: any,
1111
+ order: readonly string[],
1112
+ ): { schema: any | undefined } => {
1113
+ for (const code of order) {
1114
+ const raw = responses?.[code];
1115
+ if (!raw) continue;
1116
+ const resp = raw.$ref ? resolvePointer(ctx.spec, raw.$ref) : raw;
1117
+ if (!resp || typeof resp !== "object") return { schema: undefined };
1118
+ if (ctx.version === "2.0") {
1119
+ return { schema: resp.schema };
1120
+ }
1121
+ return { schema: resp.content?.["application/json"]?.schema };
1122
+ }
1123
+ return { schema: undefined };
1124
+ };
1125
+
1126
+ /**
1127
+ * Whether the success body is a collection: an array, or an object whose
1128
+ * array members outnumber its scalar ones (envelopes like `{ data: [] }`,
1129
+ * `{ items: [], total }`). `undefined` when there is no body to judge.
1130
+ */
1131
+ const responseIsCollection = (
1132
+ ctx: Ctx,
1133
+ responses: any,
1134
+ order: readonly string[],
1135
+ ): boolean | undefined => {
1136
+ const { schema } = successSchema(ctx, responses, order);
1137
+ if (!schema) return undefined;
1138
+ const s = deref(ctx, schema);
1139
+ if (!s || typeof s !== "object") return undefined;
1140
+ if (s.type === "array" || s.items) return true;
1141
+ const props = s.properties;
1142
+ if (!props || typeof props !== "object") {
1143
+ return s.allOf || s.oneOf || s.anyOf ? undefined : false;
1144
+ }
1145
+ let arrays = 0;
1146
+ let others = 0;
1147
+ for (const prop of Object.values(props)) {
1148
+ const p = deref(ctx, prop);
1149
+ if (p?.type === "array" || p?.items) arrays++;
1150
+ else others++;
1151
+ }
1152
+ if (arrays === 0) return false;
1153
+ // `{ data: [...] }`, `{ items, next_cursor }`, `{ results, count, page }`
1154
+ return arrays >= 1 && others <= 3;
1155
+ };
1156
+
1157
+ // ============================================================================
1158
+ // Pagination detection (v0 detectPagination)
1159
+ // ============================================================================
1160
+
1161
+ interface DetectedPagination {
1162
+ mode: "cursor" | "page" | "token";
1163
+ inputToken: string;
1164
+ outputToken: string;
1165
+ items: string;
1166
+ }
1167
+
1168
+ const PAGINATION_INPUT_ALIASES: Record<string, readonly string[]> = {
1169
+ cursor: ["cursor", "page_token", "pageToken"],
1170
+ token: ["next_token", "NextToken", "nextToken"],
1171
+ page: ["page"],
1172
+ };
1173
+
1174
+ const detectPagination = (
1175
+ ctx: Ctx,
1176
+ params: readonly Param[],
1177
+ responseSchema: any,
1178
+ ): DetectedPagination | undefined => {
1179
+ if (!responseSchema) return undefined;
1180
+ const bag = flattenObject(ctx, responseSchema).properties;
1181
+ if (Object.keys(bag).length === 0) return undefined;
1182
+
1183
+ let mode: DetectedPagination["mode"] | undefined;
1184
+ let outputToken: string | undefined;
1185
+ const pag = bag["pagination"]
1186
+ ? flattenObject(ctx, bag["pagination"]).properties
1187
+ : undefined;
1188
+ if (pag?.["cursor"]) {
1189
+ mode = "cursor";
1190
+ outputToken = "pagination.cursor";
1191
+ } else if (pag?.["next"]) {
1192
+ mode = "cursor";
1193
+ outputToken = "pagination.next";
1194
+ } else if (pag?.["next_page"]) {
1195
+ mode = "page";
1196
+ outputToken = "pagination.next_page";
1197
+ } else if (bag["next_token"]) {
1198
+ mode = "token";
1199
+ outputToken = "next_token";
1200
+ } else if (bag["NextToken"]) {
1201
+ mode = "token";
1202
+ outputToken = "NextToken";
1203
+ } else if (bag["nextToken"]) {
1204
+ mode = "token";
1205
+ outputToken = "nextToken";
1206
+ } else if (bag["next_page"]) {
1207
+ mode = "page";
1208
+ outputToken = "next_page";
1209
+ }
1210
+ if (!mode || !outputToken) return undefined;
1211
+
1212
+ const aliases = PAGINATION_INPUT_ALIASES[mode]!;
1213
+ const inputToken = params.find(
1214
+ (p) => p.in === "query" && aliases.includes(p.name),
1215
+ )?.name;
1216
+ if (!inputToken) return undefined;
1217
+
1218
+ let items: string | undefined;
1219
+ for (const [k, v] of Object.entries(bag)) {
1220
+ if (k === "pagination" || k === "next_token" || k === "NextToken") continue;
1221
+ if (typeOf(deref(ctx, v)) === "array") {
1222
+ items = k;
1223
+ break;
1224
+ }
1225
+ }
1226
+ if (!items) return undefined;
1227
+
1228
+ return { mode, inputToken, outputToken, items };
1229
+ };
1230
+
1231
+ // ============================================================================
1232
+ // Main conversion
1233
+ // ============================================================================
1234
+
1235
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
1236
+
1237
+ /**
1238
+ * Methods a spec may declare beyond {@link HTTP_METHODS}, opt-in via
1239
+ * {@link OpenApiConvertOptions.extraHttpMethods} — a provider that models
1240
+ * `head` (Vercel's cache-artifact and file existence probes) asks for it
1241
+ * rather than every provider silently gaining operations on regeneration.
1242
+ */
1243
+ const OPTIONAL_HTTP_METHODS = ["head", "options"] as const;
1244
+
1245
+ const DEFAULT_STATUS_TO_ERROR_CLASS: Readonly<Record<string, string>> = {
1246
+ "400": "BadRequest",
1247
+ "403": "Forbidden",
1248
+ "404": "NotFound",
1249
+ "409": "Conflict",
1250
+ "422": "UnprocessableEntity",
1251
+ };
1252
+
1253
+ const DEFAULT_ERROR_STATUSES = ["401", "429", "500", "503"];
1254
+
1255
+ const DEFAULT_SUCCESS_STATUSES = ["200", "201", "204"];
1256
+
1257
+ export const convertOpenApiToSmithy = (
1258
+ spec: unknown,
1259
+ options: OpenApiConvertOptions,
1260
+ ): SmithyModel => {
1261
+ const doc = spec as any;
1262
+ const version = detectVersion(doc);
1263
+ const ctx: Ctx = {
1264
+ spec: doc,
1265
+ version,
1266
+ ns: options.namespace,
1267
+ shapes: {},
1268
+ names: new Set(),
1269
+ refs: new Map(),
1270
+ dirSensitiveRefs: new Map(),
1271
+ sensitivePatterns: options.sensitivePatterns ?? SENSITIVE_FIELD_PATTERNS,
1272
+ };
1273
+ const statusToErrorClass =
1274
+ options.statusToErrorClass ?? DEFAULT_STATUS_TO_ERROR_CLASS;
1275
+ const defaultErrorStatuses = new Set(
1276
+ options.defaultErrorStatuses ?? DEFAULT_ERROR_STATUSES,
1277
+ );
1278
+ const skipDeprecated = options.skipDeprecated ?? true;
1279
+ const successStatuses = options.successStatuses ?? DEFAULT_SUCCESS_STATUSES;
1280
+ const httpMethods = [
1281
+ ...HTTP_METHODS,
1282
+ ...OPTIONAL_HTTP_METHODS.filter((m) =>
1283
+ options.extraHttpMethods?.includes(m),
1284
+ ),
1285
+ ];
1286
+
1287
+ // Error class names and the service name are reserved up front so schema
1288
+ // components can never steal them.
1289
+ const errorIds = new Map<string, string>(); // class name → shape id
1290
+ for (const cls of new Set(Object.values(statusToErrorClass))) {
1291
+ errorIds.set(cls, `${ctx.ns}#${uniqueName(ctx, cls)}`);
1292
+ }
1293
+ const serviceName = uniqueName(ctx, options.serviceName);
1294
+
1295
+ const usedErrors = new Map<string, number>(); // class name → first status
1296
+ const serviceOps: Array<{ target: string }> = [];
1297
+
1298
+ for (const [rawPath, pathItem] of Object.entries(doc.paths ?? {})) {
1299
+ if (!pathItem || typeof pathItem !== "object") continue;
1300
+ for (const method of httpMethods) {
1301
+ const op = (pathItem as any)[method];
1302
+ if (!op || typeof op !== "object") continue;
1303
+ if (skipDeprecated && op.deprecated === true) continue;
1304
+
1305
+ const rawOperationId =
1306
+ typeof op.operationId === "string" && op.operationId
1307
+ ? op.operationId
1308
+ : `${method}_${rawPath}`;
1309
+ const idCtx = { path: rawPath, method };
1310
+ const naming = options.operationNaming ?? "verbNoun";
1311
+ let named =
1312
+ options.operationNames !== undefined
1313
+ ? resolveOperationName(options.operationNames, rawOperationId, idCtx)
1314
+ : undefined;
1315
+ if (named === undefined && naming === "verbNoun") {
1316
+ // A mechanical id (`get-api-card`, or none at all) is named from
1317
+ // the route. A hand-chosen one that merely starts with the method
1318
+ // and reads the route in order (`get-feeds` on /feeds) keeps its
1319
+ // own nouns; only the HTTP-method verb is normalised.
1320
+ if (typeof op.operationId !== "string" || !op.operationId) {
1321
+ // No id: everything comes from the route, including whether a
1322
+ // GET on a collection route is `list` or `get`.
1323
+ named = pathToVerbNoun(idCtx, {
1324
+ returnsCollection: responseIsCollection(
1325
+ ctx,
1326
+ op.responses,
1327
+ successStatuses,
1328
+ ),
1329
+ });
1330
+ } else if (isMechanicalOperationId(op.operationId, idCtx)) {
1331
+ // Method-prefixed and reading the route (`get-feeds`,
1332
+ // `postV1AppsByAppIdPromote`, `deleteProjectJWKS`): keep the
1333
+ // author's tokens and casing; normalise `post`/`patch`, drop
1334
+ // parameter clauses (`ByAppId`, `ById`) and api/version roots.
1335
+ named = pathToVerbNoun(idCtx, {
1336
+ nouns: op.operationId,
1337
+ verbatim: isVerbatimRouteId(op.operationId, idCtx),
1338
+ });
1339
+ } else {
1340
+ named = toVerbNoun(rawOperationId);
1341
+ }
1342
+ }
1343
+ let resolved: string = named ?? rawOperationId;
1344
+ // Two routes can derive the same name (`/collections/{slug}` and
1345
+ // `/collections/{slug}-{id}`); suffix the trailing parameter rather
1346
+ // than a counter.
1347
+ if (
1348
+ naming === "verbNoun" &&
1349
+ ctx.names.has(pascal(resolved)) &&
1350
+ isMechanicalOperationId(
1351
+ typeof op.operationId === "string" ? op.operationId : undefined,
1352
+ idCtx,
1353
+ )
1354
+ ) {
1355
+ const lastParam = /\{([^}]+)\}[^/]*$/.exec(rawPath)?.[1];
1356
+ if (lastParam) resolved = `${resolved}By${pascal(lastParam)}`;
1357
+ }
1358
+ const opName = pascal(resolved);
1359
+
1360
+ const params = collectParams(ctx, pathItem, op);
1361
+
1362
+ // ---- URI + labels (sanitize placeholder names to member idents) ----
1363
+ let uri = rawPath.split(/[?#]/)[0]!;
1364
+ const rawLabels = Array.from(uri.matchAll(/\{([^}]+)\}/g)).map(
1365
+ (m) => m[1]!,
1366
+ );
1367
+ const members: Record<string, any> = {};
1368
+ const addMember = (name: string, member: any): boolean => {
1369
+ if (name in members) return false;
1370
+ members[name] = member;
1371
+ return true;
1372
+ };
1373
+
1374
+ for (const raw of rawLabels) {
1375
+ const san = memberIdent(raw);
1376
+ if (san !== raw) uri = uri.split(`{${raw}}`).join(`{${san}}`);
1377
+ const p = params.find((x) => x.in === "path" && x.name === raw);
1378
+ addMember(san, {
1379
+ target: labelTarget(ctx, p?.schema, `${opName}Request${pascal(san)}`),
1380
+ traits: {
1381
+ "smithy.api#httpLabel": {},
1382
+ "smithy.api#required": {},
1383
+ ...(p?.description
1384
+ ? { "smithy.api#documentation": p.description }
1385
+ : {}),
1386
+ },
1387
+ });
1388
+ }
1389
+
1390
+ // ---- Query params ----
1391
+ for (const p of params) {
1392
+ if (p.in !== "query") continue;
1393
+ if (options.apiVersion !== undefined && p.name === "api-version") {
1394
+ continue;
1395
+ }
1396
+ const san = memberIdent(p.name);
1397
+ const conv = convertSchema(
1398
+ ctx,
1399
+ p.schema,
1400
+ `${opName}Request${pascal(p.name)}`,
1401
+ 0,
1402
+ "in",
1403
+ );
1404
+ addMember(san, {
1405
+ target: conv.target,
1406
+ traits: {
1407
+ "smithy.api#httpQuery": p.name,
1408
+ ...(p.required ? { "smithy.api#required": {} } : {}),
1409
+ ...(p.description
1410
+ ? { "smithy.api#documentation": p.description }
1411
+ : {}),
1412
+ },
1413
+ });
1414
+ }
1415
+
1416
+ // ---- Header params (opt-in) ----
1417
+ if (options.headerParams) {
1418
+ for (const p of params) {
1419
+ if (p.in !== "header") continue;
1420
+ const conv = convertSchema(
1421
+ ctx,
1422
+ p.schema,
1423
+ `${opName}Request${pascal(p.name)}`,
1424
+ 0,
1425
+ "in",
1426
+ );
1427
+ addMember(headerMemberName(p.name), {
1428
+ target: conv.target,
1429
+ traits: {
1430
+ // The wire name rides on the trait, so the member is free to be
1431
+ // a normal identifier.
1432
+ "smithy.api#httpHeader": p.name,
1433
+ ...(p.required ? { "smithy.api#required": {} } : {}),
1434
+ ...(p.description
1435
+ ? { "smithy.api#documentation": p.description }
1436
+ : {}),
1437
+ },
1438
+ });
1439
+ }
1440
+ }
1441
+
1442
+ // ---- Request body (json > form-urlencoded > multipart) ----
1443
+ let contentType: "form-urlencoded" | "multipart" | undefined;
1444
+ let bodySchema: any;
1445
+ let bodyRequired = false;
1446
+ if (version === "2.0") {
1447
+ const bodyParam = params.find((p) => p.in === "body");
1448
+ bodySchema = bodyParam?.schema;
1449
+ bodyRequired = bodyParam?.required === true;
1450
+ } else if (op.requestBody) {
1451
+ const rb = op.requestBody.$ref
1452
+ ? resolvePointer(doc, op.requestBody.$ref)
1453
+ : op.requestBody;
1454
+ bodyRequired = rb?.required === true;
1455
+ const content = rb?.content ?? {};
1456
+ if (content["application/json"]) {
1457
+ bodySchema = content["application/json"].schema;
1458
+ } else if (content["application/x-www-form-urlencoded"]) {
1459
+ bodySchema = content["application/x-www-form-urlencoded"].schema;
1460
+ contentType = "form-urlencoded";
1461
+ } else if (content["multipart/form-data"]) {
1462
+ bodySchema = content["multipart/form-data"].schema;
1463
+ contentType = "multipart";
1464
+ }
1465
+ }
1466
+ if (bodySchema !== undefined) {
1467
+ const flat = flattenObject(ctx, bodySchema);
1468
+ if (Object.keys(flat.properties).length > 0) {
1469
+ const bodyMembers = buildMembers(
1470
+ ctx,
1471
+ flat.properties,
1472
+ flat.required,
1473
+ `${opName}Request`,
1474
+ 0,
1475
+ "in",
1476
+ );
1477
+ for (const [mn, m] of Object.entries(bodyMembers)) {
1478
+ addMember(mn, m); // labels win, then query, then body
1479
+ }
1480
+ } else {
1481
+ // Non-flattenable body (bare array/scalar/map/union) → sole TYPED
1482
+ // `body` member sent as the entire request body
1483
+ // (`smithy.api#httpPayload`). A schema-less JSON body (empty or
1484
+ // bare-object schema → Document) gets NO body member at all — the
1485
+ // runtime's unknown-key passthrough is the escape hatch, and an
1486
+ // opaque `body: unknown` payload member would swallow the typed
1487
+ // surface.
1488
+ const conv = convertSchema(
1489
+ ctx,
1490
+ bodySchema,
1491
+ `${opName}RequestBody`,
1492
+ 0,
1493
+ "in",
1494
+ );
1495
+ if (conv.target !== PRELUDE.Document) {
1496
+ addMember("body", {
1497
+ target: conv.target,
1498
+ traits: {
1499
+ "smithy.api#httpPayload": {},
1500
+ ...(bodyRequired ? { "smithy.api#required": {} } : {}),
1501
+ ...(conv.nullable ? { [NULLABLE_TRAIT]: {} } : {}),
1502
+ },
1503
+ });
1504
+ }
1505
+ }
1506
+ }
1507
+
1508
+ // ---- Input shape ----
1509
+ const inputTarget =
1510
+ Object.keys(members).length > 0
1511
+ ? addShape(ctx, `${opName}Request`, {
1512
+ type: "structure",
1513
+ members,
1514
+ traits: { "smithy.api#input": {} },
1515
+ })
1516
+ : PRELUDE.Unit;
1517
+
1518
+ // ---- Output shape ----
1519
+ // A HEAD response carries no content, and not merely by convention:
1520
+ // HTTP/1.1 framing terminates it at the end of the header section
1521
+ // "regardless of the header fields present in the message" (RFC 9112
1522
+ // §6.3), so a declared `Content-Length` describes what a GET *would*
1523
+ // return and no body can reach the client. Specs declare one anyway —
1524
+ // Vercel mirrors the GET's schema onto two of its HEAD probes and
1525
+ // gives the other two a bare `{"nullable": true}` — and honouring it
1526
+ // generates an output struct with required members that then fails to
1527
+ // decode against the empty body on every successful call.
1528
+ const { schema: respSchema } =
1529
+ method === "head"
1530
+ ? { schema: undefined }
1531
+ : successSchema(ctx, op.responses, successStatuses);
1532
+ let outputTarget: string = PRELUDE.Unit;
1533
+ if (respSchema !== undefined) {
1534
+ const flat = flattenObject(ctx, respSchema);
1535
+ if (Object.keys(flat.properties).length > 0) {
1536
+ const resolved = deref(ctx, respSchema);
1537
+ const isPlainRef =
1538
+ typeof respSchema.$ref === "string" &&
1539
+ !Array.isArray(resolved?.allOf) &&
1540
+ isNameable(ctx, resolved);
1541
+ if (isPlainRef) {
1542
+ // Reuse the named component shape as the output directly.
1543
+ outputTarget = convertSchema(
1544
+ ctx,
1545
+ respSchema,
1546
+ opName,
1547
+ 0,
1548
+ "out",
1549
+ ).target;
1550
+ } else {
1551
+ outputTarget = addShape(ctx, `${opName}Response`, {
1552
+ type: "structure",
1553
+ members: buildMembers(
1554
+ ctx,
1555
+ flat.properties,
1556
+ flat.required,
1557
+ `${opName}Response`,
1558
+ 0,
1559
+ "out",
1560
+ ),
1561
+ traits: { "smithy.api#output": {} },
1562
+ });
1563
+ }
1564
+ } else {
1565
+ // Non-flattenable response (bare array/scalar/map/union, or an
1566
+ // opaque object) → wrapper whose sole TYPED member IS the payload;
1567
+ // the SdkSpec's rootPipe collapses the wrapper.
1568
+ const conv = convertSchema(
1569
+ ctx,
1570
+ respSchema,
1571
+ `${opName}ResponseBody`,
1572
+ 0,
1573
+ "out",
1574
+ );
1575
+ if (conv.target !== PRELUDE.Document || deref(ctx, respSchema)) {
1576
+ outputTarget = addShape(ctx, `${opName}Response`, {
1577
+ type: "structure",
1578
+ members: {
1579
+ body: {
1580
+ target: conv.target,
1581
+ traits: {
1582
+ [RAW_RESPONSE_TRAIT]: {},
1583
+ "smithy.api#required": {},
1584
+ ...(conv.nullable ? { [NULLABLE_TRAIT]: {} } : {}),
1585
+ },
1586
+ },
1587
+ },
1588
+ traits: { "smithy.api#output": {} },
1589
+ });
1590
+ }
1591
+ }
1592
+ }
1593
+
1594
+ // ---- Errors ----
1595
+ const errors: Array<{ target: string }> = [];
1596
+ for (const status of Object.keys(op.responses ?? {})) {
1597
+ if (!/^[45]\d\d$/.test(status)) continue;
1598
+ if (defaultErrorStatuses.has(status)) continue;
1599
+ const cls = statusToErrorClass[status];
1600
+ if (!cls) continue;
1601
+ const id = errorIds.get(cls)!;
1602
+ if (!usedErrors.has(cls)) usedErrors.set(cls, Number(status));
1603
+ if (!errors.some((e) => e.target === id)) errors.push({ target: id });
1604
+ }
1605
+
1606
+ // ---- Pagination ----
1607
+ const pagination = detectPagination(ctx, params, respSchema);
1608
+
1609
+ // ---- Operation shape ----
1610
+ const httpTrait: Record<string, any> = {
1611
+ method: method.toUpperCase(),
1612
+ uri,
1613
+ code: 200,
1614
+ };
1615
+ if (contentType === "multipart") httpTrait.contentType = "multipart";
1616
+ const traits: Record<string, any> = { "smithy.api#http": httpTrait };
1617
+ const documentation = opDoc(op);
1618
+ if (documentation) traits["smithy.api#documentation"] = documentation;
1619
+ if (pagination) traits["smithy.api#paginated"] = pagination;
1620
+ if (contentType) traits[CONTENT_TYPE_TRAIT] = contentType;
1621
+ if (options.apiVersion !== undefined) {
1622
+ traits[API_VERSION_TRAIT] = options.apiVersion;
1623
+ }
1624
+
1625
+ const opId = addShape(ctx, opName, {
1626
+ type: "operation",
1627
+ input: { target: inputTarget },
1628
+ output: { target: outputTarget },
1629
+ ...(errors.length ? { errors } : {}),
1630
+ traits,
1631
+ });
1632
+ serviceOps.push({ target: opId });
1633
+ }
1634
+ }
1635
+
1636
+ // ---- Error shapes (one per used class) ----
1637
+ for (const [cls, status] of usedErrors) {
1638
+ const id = errorIds.get(cls)!;
1639
+ const base = {
1640
+ type: "structure",
1641
+ members: {},
1642
+ traits: {
1643
+ "smithy.api#error": status < 500 ? "client" : "server",
1644
+ "smithy.api#httpError": status,
1645
+ [ERROR_MATCHERS_TRAIT]: [{ status }],
1646
+ },
1647
+ };
1648
+ const override = options.errorShapes?.[cls];
1649
+ ctx.shapes[id] = override
1650
+ ? {
1651
+ ...base,
1652
+ ...override,
1653
+ traits: { ...base.traits, ...(override.traits ?? {}) },
1654
+ }
1655
+ : base;
1656
+ }
1657
+
1658
+ // ---- Service shape ----
1659
+ ctx.shapes[`${ctx.ns}#${serviceName}`] = {
1660
+ type: "service",
1661
+ version:
1662
+ options.serviceVersion ??
1663
+ (typeof doc.info?.version === "string" ? doc.info.version : "1.0"),
1664
+ operations: serviceOps,
1665
+ traits: {
1666
+ "smithy.api#title":
1667
+ typeof doc.info?.title === "string"
1668
+ ? doc.info.title
1669
+ : options.serviceName,
1670
+ ...(typeof doc.info?.description === "string"
1671
+ ? {
1672
+ "smithy.api#documentation": trimDescription(doc.info.description),
1673
+ }
1674
+ : {}),
1675
+ },
1676
+ };
1677
+
1678
+ return {
1679
+ smithy: "2.0",
1680
+ metadata: {
1681
+ suppressions: [
1682
+ { id: "HttpUriConflict", namespace: "*" },
1683
+ { id: "HttpMethodSemantics", namespace: "*" },
1684
+ { id: "UnreferencedShape", namespace: "*" },
1685
+ ],
1686
+ },
1687
+ shapes: ctx.shapes,
1688
+ };
1689
+ };