@distilled.cloud/core 0.30.2 → 1.0.0-rc.1
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.
- package/lib/api.d.ts +165 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +178 -0
- package/lib/api.js.map +1 -0
- package/lib/codegen/cli.d.ts +29 -0
- package/lib/codegen/cli.d.ts.map +1 -0
- package/lib/codegen/cli.js +165 -0
- package/lib/codegen/cli.js.map +1 -0
- package/lib/codegen/emit.d.ts +129 -0
- package/lib/codegen/emit.d.ts.map +1 -0
- package/lib/codegen/emit.js +105 -0
- package/lib/codegen/emit.js.map +1 -0
- package/lib/codegen/format.d.ts +23 -0
- package/lib/codegen/format.d.ts.map +1 -0
- package/lib/codegen/format.js +28 -0
- package/lib/codegen/format.js.map +1 -0
- package/lib/codegen/generator.d.ts +334 -0
- package/lib/codegen/generator.d.ts.map +1 -0
- package/lib/codegen/generator.js +691 -0
- package/lib/codegen/generator.js.map +1 -0
- package/lib/codegen/graph.d.ts +36 -0
- package/lib/codegen/graph.d.ts.map +1 -0
- package/lib/codegen/graph.js +136 -0
- package/lib/codegen/graph.js.map +1 -0
- package/lib/codegen/members.d.ts +25 -0
- package/lib/codegen/members.d.ts.map +1 -0
- package/lib/codegen/members.js +55 -0
- package/lib/codegen/members.js.map +1 -0
- package/lib/codegen/naming.d.ts +29 -0
- package/lib/codegen/naming.d.ts.map +1 -0
- package/lib/codegen/naming.js +74 -0
- package/lib/codegen/naming.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +38 -0
- package/lib/codegen/openapi-cli.d.ts.map +1 -0
- package/lib/codegen/openapi-cli.js +107 -0
- package/lib/codegen/openapi-cli.js.map +1 -0
- package/lib/codegen/openapi.d.ts +115 -0
- package/lib/codegen/openapi.d.ts.map +1 -0
- package/lib/codegen/openapi.js +1220 -0
- package/lib/codegen/openapi.js.map +1 -0
- package/lib/codegen/operations.d.ts +24 -0
- package/lib/codegen/operations.d.ts.map +1 -0
- package/lib/codegen/operations.js +56 -0
- package/lib/codegen/operations.js.map +1 -0
- package/lib/codegen/pagination.d.ts +39 -0
- package/lib/codegen/pagination.d.ts.map +1 -0
- package/lib/codegen/pagination.js +33 -0
- package/lib/codegen/pagination.js.map +1 -0
- package/lib/codegen/prelude.d.ts +15 -0
- package/lib/codegen/prelude.d.ts.map +1 -0
- package/lib/codegen/prelude.js +60 -0
- package/lib/codegen/prelude.js.map +1 -0
- package/lib/error-category.d.ts +28 -0
- package/lib/error-category.d.ts.map +1 -0
- package/lib/error-category.js +46 -0
- package/lib/error-category.js.map +1 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -0
- package/lib/errors.js.map +1 -1
- package/lib/json-patch.d.ts +25 -32
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +23 -95
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +37 -51
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +72 -90
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts +74 -0
- package/lib/protocol-http.d.ts.map +1 -0
- package/lib/protocol-http.js +554 -0
- package/lib/protocol-http.js.map +1 -0
- package/lib/protocol-rest.d.ts +124 -0
- package/lib/protocol-rest.d.ts.map +1 -0
- package/lib/protocol-rest.js +242 -0
- package/lib/protocol-rest.js.map +1 -0
- package/lib/retry.d.ts +8 -2
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +21 -15
- package/lib/retry.js.map +1 -1
- package/lib/schema.d.ts +7 -8
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +7 -8
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +150 -0
- package/lib/trait.d.ts.map +1 -0
- package/lib/trait.js +107 -0
- package/lib/trait.js.map +1 -0
- package/package.json +18 -75
- package/src/api.ts +446 -0
- package/src/codegen/cli.ts +268 -0
- package/src/codegen/emit.ts +207 -0
- package/src/codegen/format.ts +47 -0
- package/src/codegen/generator.ts +1153 -0
- package/src/codegen/graph.ts +151 -0
- package/src/codegen/members.ts +71 -0
- package/src/codegen/naming.ts +86 -0
- package/src/codegen/openapi-cli.ts +166 -0
- package/src/codegen/openapi.ts +1450 -0
- package/src/codegen/operations.ts +76 -0
- package/src/codegen/pagination.ts +71 -0
- package/src/codegen/prelude.ts +70 -0
- package/src/error-category.ts +84 -0
- package/src/errors.ts +2 -0
- package/src/json-patch.ts +26 -110
- package/src/pagination.ts +86 -142
- package/src/protocol-http.ts +699 -0
- package/src/protocol-rest.ts +367 -0
- package/src/retry.ts +20 -21
- package/src/schema.ts +7 -8
- package/src/trait.ts +238 -0
- package/README.md +0 -30
- package/lib/client.d.ts +0 -167
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js +0 -659
- package/lib/client.js.map +0 -1
- package/lib/schemas.d.ts +0 -60
- package/lib/schemas.d.ts.map +0 -1
- package/lib/schemas.js +0 -79
- package/lib/schemas.js.map +0 -1
- package/lib/sensitive.d.ts +0 -71
- package/lib/sensitive.d.ts.map +0 -1
- package/lib/sensitive.js +0 -96
- package/lib/sensitive.js.map +0 -1
- package/lib/traits.d.ts +0 -421
- package/lib/traits.d.ts.map +0 -1
- package/lib/traits.js +0 -737
- package/lib/traits.js.map +0 -1
- package/src/client.ts +0 -1177
- package/src/schemas.ts +0 -128
- package/src/sensitive.ts +0 -119
- package/src/traits.ts +0 -996
|
@@ -0,0 +1,1220 @@
|
|
|
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), deprecated
|
|
11
|
+
* skipped by default; op shape name = PascalCase(operationId)
|
|
12
|
+
* • input `<Op>Request`: path params → `smithy.api#httpLabel` (+required),
|
|
13
|
+
* query params → `smithy.api#httpQuery`, header/cookie params dropped,
|
|
14
|
+
* body properties flattened alongside (labels win, then query, then body);
|
|
15
|
+
* non-object bodies become a sole `body` member with `smithy.api#httpPayload`
|
|
16
|
+
* • `$ref`s become NAMED shapes (`components/schemas/X` → `<ns>#X`), reused
|
|
17
|
+
* across operations; anonymous nested objects synthesize names from the
|
|
18
|
+
* parent + member path
|
|
19
|
+
* • responses: 200 → 201 → 204 precedence, `application/json` only; object
|
|
20
|
+
* results become `<Op>Response` structures (a sole `$ref` reuses the named
|
|
21
|
+
* shape); bare array/scalar results wrap in a structure whose single
|
|
22
|
+
* member carries `com.distilled.openapi#rawResponse` (the SdkSpec maps it
|
|
23
|
+
* to a root pipe); no schema → `smithy.api#Unit`
|
|
24
|
+
* • nullability (3.0 `nullable`, 3.1 `type: [..., "null"]`, 2.0
|
|
25
|
+
* `x-nullable`, `oneOf`/`anyOf` null branches) → member-level
|
|
26
|
+
* `com.distilled.openapi#nullable` trait
|
|
27
|
+
* • sensitive string members (x-sensitive or name-pattern match) →
|
|
28
|
+
* `smithy.api#sensitive` on the member (direction-agnostic; the SdkSpec
|
|
29
|
+
* decides the input/output treatment)
|
|
30
|
+
* • per-op non-2xx statuses (outside `defaultErrorStatuses`) → typed error
|
|
31
|
+
* shapes with `smithy.api#error`/`smithy.api#httpError` and
|
|
32
|
+
* `com.distilled.openapi#errorMatchers` status matchers
|
|
33
|
+
* • v0 pagination detection (`pagination.cursor` / `.next` / `.next_page`,
|
|
34
|
+
* `next_token`/`NextToken`/`nextToken`, `next_page` + input alias lists +
|
|
35
|
+
* first top-level array property) → `smithy.api#paginated` on the op
|
|
36
|
+
* (with the non-standard `mode` member the core runtime dispatches on)
|
|
37
|
+
*/
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Trait ids (the `com.distilled.openapi` vocabulary)
|
|
40
|
+
// ============================================================================
|
|
41
|
+
/** Member-level nullability (`S.NullOr` + `| null` via `SdkSpec.nullableTrait`). */
|
|
42
|
+
export const NULLABLE_TRAIT = "com.distilled.openapi#nullable";
|
|
43
|
+
/**
|
|
44
|
+
* Marks the sole member of a synthesized response wrapper for bare
|
|
45
|
+
* array/scalar response bodies. SdkSpecs bind it with a `rootPipe`
|
|
46
|
+
* (`T.RawResponseRoot()` from `core/protocol-rest`), so the emitted response
|
|
47
|
+
* type IS the payload type.
|
|
48
|
+
*/
|
|
49
|
+
export const RAW_RESPONSE_TRAIT = "com.distilled.openapi#rawResponse";
|
|
50
|
+
/**
|
|
51
|
+
* Wire-matching rules for a generated error class (see
|
|
52
|
+
* `SdkSpec.errorMatchersTrait` + `applyErrorMatchers`). The converter stamps
|
|
53
|
+
* `[{ status }]` from the response status the class was mapped from.
|
|
54
|
+
*/
|
|
55
|
+
export const ERROR_MATCHERS_TRAIT = "com.distilled.openapi#errorMatchers";
|
|
56
|
+
/**
|
|
57
|
+
* Non-JSON request body encoding chosen by the v0 content-type precedence
|
|
58
|
+
* (json > form-urlencoded > multipart). Value: `"form-urlencoded"` |
|
|
59
|
+
* `"multipart"`. `"multipart"` is also merged into the `smithy.api#http`
|
|
60
|
+
* trait's `contentType` (which core's `buildRequest` understands);
|
|
61
|
+
* form-urlencoded is left to provider SdkSpecs/protocols.
|
|
62
|
+
*/
|
|
63
|
+
export const CONTENT_TYPE_TRAIT = "com.distilled.openapi#contentType";
|
|
64
|
+
/**
|
|
65
|
+
* The `apiVersion` recorded on every operation when
|
|
66
|
+
* {@link OpenApiConvertOptions.apiVersion} is set (Azure ARM style). The
|
|
67
|
+
* matching `api-version` query parameter is dropped from inputs; the
|
|
68
|
+
* provider's protocol injects it at request time.
|
|
69
|
+
*/
|
|
70
|
+
export const API_VERSION_TRAIT = "com.distilled.openapi#apiVersion";
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Sensitive-field name patterns (distilled v0's SENSITIVE_FIELD_PATTERNS)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
export const SENSITIVE_FIELD_PATTERNS = [
|
|
75
|
+
/password/i,
|
|
76
|
+
/^secret$/i,
|
|
77
|
+
/secret[-_]?key/i,
|
|
78
|
+
/[-_]secret$/i,
|
|
79
|
+
/^client[-_]?secret$/i,
|
|
80
|
+
/^access[-_]?token$/i,
|
|
81
|
+
/^refresh[-_]?token$/i,
|
|
82
|
+
/^api[-_]?key$/i,
|
|
83
|
+
/^api[-_]?key[-_]?secret$/i,
|
|
84
|
+
/^api[-_]?token$/i,
|
|
85
|
+
/^private[-_]?key$/i,
|
|
86
|
+
/^secret[-_]?access[-_]?key$/i,
|
|
87
|
+
/^session[-_]?token$/i,
|
|
88
|
+
/^access[-_]?key[-_]?id$/i,
|
|
89
|
+
/^one[-_]?time[-_]?password$/i,
|
|
90
|
+
/^connection[-_]?string$/i,
|
|
91
|
+
/^connection[-_]?uri$/i,
|
|
92
|
+
/^plain[-_]?text$/i,
|
|
93
|
+
/^plain[-_]?text[-_]?refresh[-_]?token$/i,
|
|
94
|
+
];
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// String helpers (same conventions as cloudflare's spec-to-smithy)
|
|
97
|
+
// ============================================================================
|
|
98
|
+
const PRELUDE = {
|
|
99
|
+
Unit: "smithy.api#Unit",
|
|
100
|
+
String: "smithy.api#String",
|
|
101
|
+
Boolean: "smithy.api#Boolean",
|
|
102
|
+
Double: "smithy.api#Double",
|
|
103
|
+
Integer: "smithy.api#Integer",
|
|
104
|
+
Document: "smithy.api#Document",
|
|
105
|
+
};
|
|
106
|
+
/** snake/kebab/space/camel → PascalCase identifier (inner caps preserved). */
|
|
107
|
+
const pascal = (s) => {
|
|
108
|
+
const parts = s.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
109
|
+
let out = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
110
|
+
if (out === "")
|
|
111
|
+
out = "Shape";
|
|
112
|
+
if (/^[0-9]/.test(out))
|
|
113
|
+
out = `_${out}`;
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
/** Keep a JSON field name if it's a valid Smithy member identifier, else sanitize. */
|
|
117
|
+
const memberIdent = (name) => {
|
|
118
|
+
let out = name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
119
|
+
if (/^[0-9]/.test(out))
|
|
120
|
+
out = `_${out}`;
|
|
121
|
+
return out || "_";
|
|
122
|
+
};
|
|
123
|
+
const enumMemberName = (value) => {
|
|
124
|
+
let out = value
|
|
125
|
+
.toUpperCase()
|
|
126
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
127
|
+
.replace(/^_+|_+$/g, "");
|
|
128
|
+
if (out === "")
|
|
129
|
+
out = "VALUE";
|
|
130
|
+
if (/^[0-9]/.test(out))
|
|
131
|
+
out = `_${out}`;
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Conversion context
|
|
136
|
+
// ============================================================================
|
|
137
|
+
const MAX_SCHEMA_DEPTH = 40;
|
|
138
|
+
const uniqueName = (ctx, base) => {
|
|
139
|
+
const want = pascal(base);
|
|
140
|
+
let name = want;
|
|
141
|
+
let n = 2;
|
|
142
|
+
while (ctx.names.has(name))
|
|
143
|
+
name = `${want}${n++}`;
|
|
144
|
+
ctx.names.add(name);
|
|
145
|
+
return name;
|
|
146
|
+
};
|
|
147
|
+
const addShape = (ctx, base, def) => {
|
|
148
|
+
const id = `${ctx.ns}#${uniqueName(ctx, base)}`;
|
|
149
|
+
ctx.shapes[id] = def;
|
|
150
|
+
return id;
|
|
151
|
+
};
|
|
152
|
+
const detectVersion = (spec) => {
|
|
153
|
+
if (spec?.swagger === "2.0")
|
|
154
|
+
return "2.0";
|
|
155
|
+
const v = spec?.openapi;
|
|
156
|
+
if (typeof v === "string" && v.startsWith("3.1"))
|
|
157
|
+
return "3.1";
|
|
158
|
+
if (typeof v === "string" && v.startsWith("3.0"))
|
|
159
|
+
return "3.0";
|
|
160
|
+
throw new Error(`Unsupported OpenAPI version (swagger=${spec?.swagger}, openapi=${spec?.openapi})`);
|
|
161
|
+
};
|
|
162
|
+
/** Resolve a local `#/...` JSON pointer against the spec document. */
|
|
163
|
+
const resolvePointer = (spec, ref) => {
|
|
164
|
+
if (typeof ref !== "string" || !ref.startsWith("#/"))
|
|
165
|
+
return undefined;
|
|
166
|
+
let cur = spec;
|
|
167
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
168
|
+
const seg = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
169
|
+
if (cur === null || typeof cur !== "object")
|
|
170
|
+
return undefined;
|
|
171
|
+
cur = cur[seg];
|
|
172
|
+
}
|
|
173
|
+
return cur;
|
|
174
|
+
};
|
|
175
|
+
/** Follow `$ref` chains (bounded) to the underlying schema object. */
|
|
176
|
+
const deref = (ctx, def) => {
|
|
177
|
+
let cur = def;
|
|
178
|
+
for (let i = 0; i < 16 && cur && typeof cur === "object" && cur.$ref; i++) {
|
|
179
|
+
cur = resolvePointer(ctx.spec, cur.$ref);
|
|
180
|
+
}
|
|
181
|
+
return cur;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Flatten a schema into a property bag: `$ref`s resolved, `allOf` merged
|
|
185
|
+
* (properties + required unioned across resolved subschemas). A
|
|
186
|
+
* `oneOf`/`anyOf` encountered INSIDE an `allOf` merges its branches'
|
|
187
|
+
* properties as OPTIONAL members — the loosest satisfiable reading of the
|
|
188
|
+
* intersection — rather than dropping them; a top-level union is left to
|
|
189
|
+
* the union conversion path.
|
|
190
|
+
*/
|
|
191
|
+
const flattenObject = (ctx, def, depth = 0) => {
|
|
192
|
+
const out = {
|
|
193
|
+
properties: {},
|
|
194
|
+
required: new Set(),
|
|
195
|
+
isObject: false,
|
|
196
|
+
};
|
|
197
|
+
// Revisiting a `$ref` under the same flags merges nothing new (property
|
|
198
|
+
// merge is first-wins) — dedupe so shared/cyclic allOf bases are walked
|
|
199
|
+
// once per flag combination instead of exponentially (MongoDB Atlas's
|
|
200
|
+
// polymorphic allOf/oneOf graph never finished under the bare depth cap).
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
const visit = (d, dep, inAllOf, inUnion) => {
|
|
203
|
+
if (dep > MAX_SCHEMA_DEPTH)
|
|
204
|
+
return;
|
|
205
|
+
if (d && typeof d === "object" && typeof d.$ref === "string") {
|
|
206
|
+
const key = `${d.$ref}|${inAllOf}|${inUnion}`;
|
|
207
|
+
if (seen.has(key))
|
|
208
|
+
return;
|
|
209
|
+
seen.add(key);
|
|
210
|
+
}
|
|
211
|
+
const r = deref(ctx, d);
|
|
212
|
+
if (!r || typeof r !== "object")
|
|
213
|
+
return;
|
|
214
|
+
if (Array.isArray(r.allOf)) {
|
|
215
|
+
for (const sub of r.allOf)
|
|
216
|
+
visit(sub, dep + 1, true, inUnion);
|
|
217
|
+
}
|
|
218
|
+
if (inAllOf) {
|
|
219
|
+
const branches = r.oneOf ?? r.anyOf;
|
|
220
|
+
if (Array.isArray(branches)) {
|
|
221
|
+
for (const b of branches) {
|
|
222
|
+
if (!isNullBranch(ctx, b))
|
|
223
|
+
visit(b, dep + 1, inAllOf, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (r.properties && typeof r.properties === "object") {
|
|
228
|
+
out.isObject = true;
|
|
229
|
+
for (const [k, v] of Object.entries(r.properties)) {
|
|
230
|
+
if (!(k in out.properties))
|
|
231
|
+
out.properties[k] = v;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (r.type === "object")
|
|
235
|
+
out.isObject = true;
|
|
236
|
+
if (out.additionalProperties === undefined &&
|
|
237
|
+
r.additionalProperties !== undefined &&
|
|
238
|
+
r.additionalProperties !== false) {
|
|
239
|
+
out.additionalProperties = r.additionalProperties;
|
|
240
|
+
}
|
|
241
|
+
if (!inUnion && Array.isArray(r.required)) {
|
|
242
|
+
for (const k of r.required)
|
|
243
|
+
out.required.add(k);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
visit(def, depth, false, false);
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
/** Whether a `oneOf`/`anyOf` branch represents JSON null. */
|
|
250
|
+
const isNullBranch = (ctx, branch) => {
|
|
251
|
+
const r = deref(ctx, branch);
|
|
252
|
+
if (!r || typeof r !== "object")
|
|
253
|
+
return false;
|
|
254
|
+
if (r.type === "null")
|
|
255
|
+
return true;
|
|
256
|
+
if (Array.isArray(r.type) && r.type.every((t) => t === "null")) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(r.enum) &&
|
|
260
|
+
r.enum.length > 0 &&
|
|
261
|
+
r.enum.every((v) => v === null)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
};
|
|
266
|
+
/** Intrinsic nullability flags on a schema node (not union null branches). */
|
|
267
|
+
const ownNullable = (ctx, def) => {
|
|
268
|
+
if (!def || typeof def !== "object")
|
|
269
|
+
return false;
|
|
270
|
+
// `nullable: true` is 3.0 vocabulary but appears in real 3.1 (and even
|
|
271
|
+
// 2.0) documents — honor it everywhere rather than silently dropping the
|
|
272
|
+
// null arm.
|
|
273
|
+
if (def.nullable === true)
|
|
274
|
+
return true;
|
|
275
|
+
if (def["x-nullable"] === true)
|
|
276
|
+
return true;
|
|
277
|
+
if (Array.isArray(def.type) && def.type.includes("null"))
|
|
278
|
+
return true;
|
|
279
|
+
return false;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Whether a property schema is excluded from the given direction:
|
|
283
|
+
* `readOnly: true` properties never appear in requests, `writeOnly: true`
|
|
284
|
+
* properties never appear in responses. Checks the property node itself and
|
|
285
|
+
* its `$ref` resolution.
|
|
286
|
+
*/
|
|
287
|
+
const dirExcluded = (ctx, prop, dir) => {
|
|
288
|
+
const flag = dir === "in" ? "readOnly" : "writeOnly";
|
|
289
|
+
if (prop && typeof prop === "object" && prop[flag] === true)
|
|
290
|
+
return true;
|
|
291
|
+
const r = deref(ctx, prop);
|
|
292
|
+
return !!r && typeof r === "object" && r[flag] === true;
|
|
293
|
+
};
|
|
294
|
+
/** Child schema slots the direction-sensitivity walk descends through. */
|
|
295
|
+
const schemaChildren = (d) => [
|
|
296
|
+
...(Array.isArray(d.allOf) ? d.allOf : []),
|
|
297
|
+
...(Array.isArray(d.oneOf) ? d.oneOf : []),
|
|
298
|
+
...(Array.isArray(d.anyOf) ? d.anyOf : []),
|
|
299
|
+
...(d.properties && typeof d.properties === "object"
|
|
300
|
+
? Object.values(d.properties)
|
|
301
|
+
: []),
|
|
302
|
+
...(d.items ? [d.items] : []),
|
|
303
|
+
...(d.additionalProperties && typeof d.additionalProperties === "object"
|
|
304
|
+
? [d.additionalProperties]
|
|
305
|
+
: []),
|
|
306
|
+
];
|
|
307
|
+
/**
|
|
308
|
+
* Walk an INLINE schema subtree (stopping at `$ref` nodes, which are
|
|
309
|
+
* appended to `refs` instead of followed). Returns whether the flag
|
|
310
|
+
* appears inline. Inline subtrees are JSON trees — no cycles.
|
|
311
|
+
*/
|
|
312
|
+
const scanInlineForFlag = (d, flag, refs) => {
|
|
313
|
+
if (!d || typeof d !== "object")
|
|
314
|
+
return false;
|
|
315
|
+
if (typeof d.$ref === "string") {
|
|
316
|
+
refs.push(d.$ref);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (d[flag] === true)
|
|
320
|
+
return true;
|
|
321
|
+
for (const sub of schemaChildren(d)) {
|
|
322
|
+
if (scanInlineForFlag(sub, flag, refs))
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Compute, for the WHOLE document at once, the set of `$ref` pointers that
|
|
329
|
+
* are direction-sensitive: the flag appears somewhere in the schema graph
|
|
330
|
+
* reachable from the ref's target. One inline scan per distinct ref target
|
|
331
|
+
* (local flag + outgoing ref edges), then reverse propagation from the
|
|
332
|
+
* locally-flagged nodes — linear in spec size, and cycles / diamond-shared
|
|
333
|
+
* refs cost nothing. (This replaces a per-call DFS whose memoization was
|
|
334
|
+
* disabled by ANY re-encountered ref; on large specs where every schema
|
|
335
|
+
* shares refs — e.g. MongoDB Atlas's ubiquitous `links` — that re-walked
|
|
336
|
+
* the graph for each of thousands of `$ref` sites and never finished.)
|
|
337
|
+
*/
|
|
338
|
+
const buildDirSensitiveRefs = (ctx, dir) => {
|
|
339
|
+
const flag = dir === "in" ? "readOnly" : "writeOnly";
|
|
340
|
+
// Every distinct `$ref` pointer in the document.
|
|
341
|
+
const allRefs = new Set();
|
|
342
|
+
const collect = (d) => {
|
|
343
|
+
if (Array.isArray(d)) {
|
|
344
|
+
for (const v of d)
|
|
345
|
+
collect(v);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!d || typeof d !== "object")
|
|
349
|
+
return;
|
|
350
|
+
if (typeof d.$ref === "string")
|
|
351
|
+
allRefs.add(d.$ref);
|
|
352
|
+
for (const v of Object.values(d))
|
|
353
|
+
collect(v);
|
|
354
|
+
};
|
|
355
|
+
collect(ctx.spec);
|
|
356
|
+
// ref → refs reachable in one hop; refs whose target carries the flag inline.
|
|
357
|
+
const reverse = new Map();
|
|
358
|
+
const flagged = [];
|
|
359
|
+
for (const ref of allRefs) {
|
|
360
|
+
const outgoing = [];
|
|
361
|
+
if (scanInlineForFlag(resolvePointer(ctx.spec, ref), flag, outgoing)) {
|
|
362
|
+
flagged.push(ref);
|
|
363
|
+
continue; // already sensitive; its edges can't add anything
|
|
364
|
+
}
|
|
365
|
+
for (const to of outgoing) {
|
|
366
|
+
let from = reverse.get(to);
|
|
367
|
+
if (!from)
|
|
368
|
+
reverse.set(to, (from = []));
|
|
369
|
+
from.push(ref);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Sensitivity propagates from flagged targets to every ref that reaches them.
|
|
373
|
+
const sensitive = new Set(flagged);
|
|
374
|
+
const queue = [...flagged];
|
|
375
|
+
while (queue.length > 0) {
|
|
376
|
+
const next = queue.pop();
|
|
377
|
+
for (const from of reverse.get(next) ?? []) {
|
|
378
|
+
if (!sensitive.has(from)) {
|
|
379
|
+
sensitive.add(from);
|
|
380
|
+
queue.push(from);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return sensitive;
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Whether a schema subtree is direction-sensitive — contains
|
|
388
|
+
* `readOnly: true` (request direction) or `writeOnly: true` (response
|
|
389
|
+
* direction) anywhere, following local `$ref`s. Direction-sensitive named
|
|
390
|
+
* components convert to separate request/response shape variants
|
|
391
|
+
* (`<Name>Input`/`<Name>Output`); everything else is shared.
|
|
392
|
+
*/
|
|
393
|
+
const dirSensitive = (ctx, def, dir) => {
|
|
394
|
+
let sensitiveRefs = ctx.dirSensitiveRefs.get(dir);
|
|
395
|
+
if (sensitiveRefs === undefined) {
|
|
396
|
+
sensitiveRefs = buildDirSensitiveRefs(ctx, dir);
|
|
397
|
+
ctx.dirSensitiveRefs.set(dir, sensitiveRefs);
|
|
398
|
+
}
|
|
399
|
+
const refs = [];
|
|
400
|
+
if (scanInlineForFlag(def, dir === "in" ? "readOnly" : "writeOnly", refs)) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
return refs.some((ref) => sensitiveRefs.has(ref));
|
|
404
|
+
};
|
|
405
|
+
/** Single non-array type from a possibly 3.1-style `type` value. */
|
|
406
|
+
const typeOf = (def) => {
|
|
407
|
+
const t = def?.type;
|
|
408
|
+
if (typeof t === "string")
|
|
409
|
+
return t;
|
|
410
|
+
if (Array.isArray(t)) {
|
|
411
|
+
const real = t.filter((x) => x !== "null");
|
|
412
|
+
if (real.length === 1 && typeof real[0] === "string")
|
|
413
|
+
return real[0];
|
|
414
|
+
}
|
|
415
|
+
return undefined;
|
|
416
|
+
};
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Schema conversion
|
|
419
|
+
// ============================================================================
|
|
420
|
+
/**
|
|
421
|
+
* Whether a schema definition warrants a NAMED shape (vs an inline prelude
|
|
422
|
+
* target). Named shapes can participate in reference cycles, so `$ref`s to
|
|
423
|
+
* nameable schemas reserve their name before converting.
|
|
424
|
+
*/
|
|
425
|
+
const isNameable = (ctx, def) => {
|
|
426
|
+
if (!def || typeof def !== "object")
|
|
427
|
+
return false;
|
|
428
|
+
if (def.$ref)
|
|
429
|
+
return isNameable(ctx, deref(ctx, def));
|
|
430
|
+
if (Array.isArray(def.enum)) {
|
|
431
|
+
const values = def.enum.filter((v) => v !== null);
|
|
432
|
+
return (values.length > 0 &&
|
|
433
|
+
(values.every((v) => typeof v === "string") ||
|
|
434
|
+
values.every((v) => typeof v === "number")));
|
|
435
|
+
}
|
|
436
|
+
if (Array.isArray(def.allOf))
|
|
437
|
+
return true;
|
|
438
|
+
const branches = def.oneOf ?? def.anyOf;
|
|
439
|
+
if (Array.isArray(branches)) {
|
|
440
|
+
return branches.filter((b) => !isNullBranch(ctx, b)).length >= 2;
|
|
441
|
+
}
|
|
442
|
+
const t = typeOf(def);
|
|
443
|
+
if (t === "object" || def.properties || def.additionalProperties)
|
|
444
|
+
return true;
|
|
445
|
+
if (t === "array")
|
|
446
|
+
return true;
|
|
447
|
+
return false;
|
|
448
|
+
};
|
|
449
|
+
/**
|
|
450
|
+
* Convert one schema to a shape target. `dir` is the conversion direction
|
|
451
|
+
* (request vs response position — readOnly/writeOnly members are dropped
|
|
452
|
+
* accordingly). `reservedId` names the top-level shape when the caller
|
|
453
|
+
* pre-registered it (named `$ref` targets); otherwise anonymous shapes
|
|
454
|
+
* synthesize names from `hint`.
|
|
455
|
+
*/
|
|
456
|
+
const convertSchema = (ctx, def, hint, depth, dir, reservedId) => {
|
|
457
|
+
const emit = (shapeDef, base) => {
|
|
458
|
+
if (reservedId !== undefined) {
|
|
459
|
+
ctx.shapes[reservedId] = shapeDef;
|
|
460
|
+
return reservedId;
|
|
461
|
+
}
|
|
462
|
+
return addShape(ctx, base, shapeDef);
|
|
463
|
+
};
|
|
464
|
+
const inline = (target, nullable) => {
|
|
465
|
+
// A reserved placeholder that turned out to be inline (scalar/alias):
|
|
466
|
+
// drop the placeholder; the burned name is harmless.
|
|
467
|
+
if (reservedId !== undefined)
|
|
468
|
+
delete ctx.shapes[reservedId];
|
|
469
|
+
return { target, nullable };
|
|
470
|
+
};
|
|
471
|
+
if (depth > MAX_SCHEMA_DEPTH)
|
|
472
|
+
return inline(PRELUDE.Document, false);
|
|
473
|
+
if (def === true || def === undefined || def === null) {
|
|
474
|
+
return inline(PRELUDE.Document, false);
|
|
475
|
+
}
|
|
476
|
+
if (typeof def !== "object")
|
|
477
|
+
return inline(PRELUDE.Document, false);
|
|
478
|
+
// --- $ref → named (or cached inline) shape --------------------------------
|
|
479
|
+
if (def.$ref) {
|
|
480
|
+
// Sibling nullability next to the $ref (common tool output even where
|
|
481
|
+
// the spec says siblings are ignored) survives onto the member.
|
|
482
|
+
const siteNullable = ownNullable(ctx, def);
|
|
483
|
+
// Direction-sensitive components (readOnly members in request position,
|
|
484
|
+
// writeOnly in response position) get per-direction variants; the rest
|
|
485
|
+
// share one shape across both directions.
|
|
486
|
+
const sensitive = dirSensitive(ctx, def, dir);
|
|
487
|
+
const cacheKey = sensitive ? `${dir}:${def.$ref}` : `*:${def.$ref}`;
|
|
488
|
+
const cached = ctx.refs.get(cacheKey);
|
|
489
|
+
if (cached)
|
|
490
|
+
return inline(cached.target, cached.nullable || siteNullable);
|
|
491
|
+
const resolved = resolvePointer(ctx.spec, def.$ref);
|
|
492
|
+
if (resolved === undefined)
|
|
493
|
+
return inline(PRELUDE.Document, siteNullable);
|
|
494
|
+
const refName = (String(def.$ref).split("/").pop() ?? "Shape") +
|
|
495
|
+
(sensitive ? (dir === "in" ? "Input" : "Output") : "");
|
|
496
|
+
if (!isNameable(ctx, resolved)) {
|
|
497
|
+
// Scalars and aliases can't cycle — convert inline and cache.
|
|
498
|
+
const entry = { target: PRELUDE.Document, nullable: false };
|
|
499
|
+
ctx.refs.set(cacheKey, entry);
|
|
500
|
+
const r = convertSchema(ctx, resolved, pascal(refName), depth + 1, dir);
|
|
501
|
+
entry.target = r.target;
|
|
502
|
+
entry.nullable = r.nullable;
|
|
503
|
+
return inline(r.target, r.nullable || siteNullable);
|
|
504
|
+
}
|
|
505
|
+
// Reserve the component name before converting so cycles resolve.
|
|
506
|
+
const id = `${ctx.ns}#${uniqueName(ctx, refName)}`;
|
|
507
|
+
const entry = { target: id, nullable: false };
|
|
508
|
+
ctx.refs.set(cacheKey, entry);
|
|
509
|
+
ctx.shapes[id] = { type: "structure", members: {} }; // placeholder
|
|
510
|
+
const r = convertSchema(ctx, resolved, pascal(refName), depth + 1, dir, id);
|
|
511
|
+
entry.target = r.target;
|
|
512
|
+
entry.nullable = r.nullable;
|
|
513
|
+
return inline(r.target, r.nullable || siteNullable);
|
|
514
|
+
}
|
|
515
|
+
let nullable = ownNullable(ctx, def);
|
|
516
|
+
const doc = typeof def.description === "string" ? def.description : undefined;
|
|
517
|
+
const docTraits = doc ? { "smithy.api#documentation": doc } : {};
|
|
518
|
+
// --- oneOf / anyOf --------------------------------------------------------
|
|
519
|
+
const branches = def.oneOf ?? def.anyOf;
|
|
520
|
+
if (Array.isArray(branches)) {
|
|
521
|
+
const real = [];
|
|
522
|
+
for (const b of branches) {
|
|
523
|
+
if (isNullBranch(ctx, b))
|
|
524
|
+
nullable = true;
|
|
525
|
+
else
|
|
526
|
+
real.push(b);
|
|
527
|
+
}
|
|
528
|
+
if (real.length === 0)
|
|
529
|
+
return inline(PRELUDE.Document, nullable);
|
|
530
|
+
if (real.length === 1) {
|
|
531
|
+
const r = convertSchema(ctx, real[0], hint, depth + 1, dir, reservedId);
|
|
532
|
+
return { target: r.target, nullable: nullable || r.nullable };
|
|
533
|
+
}
|
|
534
|
+
// Distinct branches → a union shape. Members are synthesized case names;
|
|
535
|
+
// duplicate targets are deduped.
|
|
536
|
+
const members = {};
|
|
537
|
+
const seenTargets = new Set();
|
|
538
|
+
real.forEach((b, i) => {
|
|
539
|
+
const branchName = typeof b?.$ref === "string"
|
|
540
|
+
? pascal(String(b.$ref).split("/").pop() ?? `Case${i}`)
|
|
541
|
+
: `Case${i}`;
|
|
542
|
+
const r = convertSchema(ctx, b, `${hint}${branchName}`, depth + 1, dir);
|
|
543
|
+
if (seenTargets.has(r.target))
|
|
544
|
+
return;
|
|
545
|
+
seenTargets.add(r.target);
|
|
546
|
+
let mn = memberIdent(branchName);
|
|
547
|
+
let k = 2;
|
|
548
|
+
while (mn in members)
|
|
549
|
+
mn = `${memberIdent(branchName)}_${k++}`;
|
|
550
|
+
members[mn] = { target: r.target };
|
|
551
|
+
});
|
|
552
|
+
const targets = Object.values(members);
|
|
553
|
+
if (targets.length === 1) {
|
|
554
|
+
return inline(targets[0].target, nullable);
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
target: emit({ type: "union", members, traits: docTraits }, hint),
|
|
558
|
+
nullable,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
// --- allOf ----------------------------------------------------------------
|
|
562
|
+
if (Array.isArray(def.allOf)) {
|
|
563
|
+
// Single-entry allOf over a $ref: passthrough (v0 semantics), carrying
|
|
564
|
+
// the parent's nullability/description.
|
|
565
|
+
if (def.allOf.length === 1 && !def.properties) {
|
|
566
|
+
const r = convertSchema(ctx, def.allOf[0], hint, depth + 1, dir, reservedId);
|
|
567
|
+
return { target: r.target, nullable: nullable || r.nullable };
|
|
568
|
+
}
|
|
569
|
+
const flat = flattenObject(ctx, def, depth);
|
|
570
|
+
if (Object.keys(flat.properties).length === 0) {
|
|
571
|
+
// A property-less intersection of map schemas is a map, not an empty
|
|
572
|
+
// struct.
|
|
573
|
+
if (flat.additionalProperties !== undefined) {
|
|
574
|
+
const r = convertSchema(ctx, { type: "object", additionalProperties: flat.additionalProperties }, hint, depth + 1, dir, reservedId);
|
|
575
|
+
return { target: r.target, nullable: nullable || r.nullable };
|
|
576
|
+
}
|
|
577
|
+
if (!flat.isObject)
|
|
578
|
+
return inline(PRELUDE.Document, nullable);
|
|
579
|
+
}
|
|
580
|
+
const members = buildMembers(ctx, flat.properties, flat.required, hint, depth + 1, dir);
|
|
581
|
+
return {
|
|
582
|
+
target: emit({ type: "structure", members, traits: docTraits }, hint),
|
|
583
|
+
nullable,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// --- enum -----------------------------------------------------------------
|
|
587
|
+
if (Array.isArray(def.enum) && def.enum.length > 0) {
|
|
588
|
+
const values = def.enum.filter((v) => v !== null);
|
|
589
|
+
if (values.some((v) => v === null))
|
|
590
|
+
nullable = true;
|
|
591
|
+
if (values.length === 0)
|
|
592
|
+
return inline(PRELUDE.Document, true);
|
|
593
|
+
if (values.every((v) => typeof v === "string")) {
|
|
594
|
+
const members = {};
|
|
595
|
+
const used = new Set();
|
|
596
|
+
for (const lit of values) {
|
|
597
|
+
let mn = enumMemberName(lit);
|
|
598
|
+
let k = 2;
|
|
599
|
+
while (used.has(mn))
|
|
600
|
+
mn = `${enumMemberName(lit)}_${k++}`;
|
|
601
|
+
used.add(mn);
|
|
602
|
+
members[mn] = {
|
|
603
|
+
target: PRELUDE.Unit,
|
|
604
|
+
traits: { "smithy.api#enumValue": lit },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
target: emit({ type: "enum", members, traits: docTraits }, hint),
|
|
609
|
+
nullable,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
// Numeric enums → intEnum shapes (closed numeric literal unions in the
|
|
613
|
+
// generated TS; the schema stays a plain number).
|
|
614
|
+
if (values.every((v) => typeof v === "number")) {
|
|
615
|
+
const members = {};
|
|
616
|
+
const used = new Set();
|
|
617
|
+
for (const lit of values) {
|
|
618
|
+
let mn = enumMemberName(String(lit));
|
|
619
|
+
let k = 2;
|
|
620
|
+
while (used.has(mn))
|
|
621
|
+
mn = `${enumMemberName(String(lit))}_${k++}`;
|
|
622
|
+
used.add(mn);
|
|
623
|
+
members[mn] = {
|
|
624
|
+
target: PRELUDE.Unit,
|
|
625
|
+
traits: { "smithy.api#enumValue": lit },
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
target: emit({ type: "intEnum", members, traits: docTraits }, hint),
|
|
630
|
+
nullable,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
if (values.every((v) => typeof v === "boolean")) {
|
|
634
|
+
return inline(PRELUDE.Boolean, nullable);
|
|
635
|
+
}
|
|
636
|
+
return inline(PRELUDE.Document, nullable);
|
|
637
|
+
}
|
|
638
|
+
const t = typeOf(def);
|
|
639
|
+
// --- array ----------------------------------------------------------------
|
|
640
|
+
if (t === "array") {
|
|
641
|
+
const item = convertSchema(ctx, def.items, `${hint}Item`, depth + 1, dir);
|
|
642
|
+
return {
|
|
643
|
+
target: emit({ type: "list", member: { target: item.target }, traits: docTraits }, reservedId !== undefined ? hint : `${hint}List`),
|
|
644
|
+
nullable,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
// --- object ---------------------------------------------------------------
|
|
648
|
+
if (t === "object" || def.properties || def.additionalProperties) {
|
|
649
|
+
if (def.properties && Object.keys(def.properties).length > 0) {
|
|
650
|
+
const required = new Set(Array.isArray(def.required) ? def.required : []);
|
|
651
|
+
const members = buildMembers(ctx, def.properties, required, hint, depth + 1, dir);
|
|
652
|
+
return {
|
|
653
|
+
target: emit({ type: "structure", members, traits: docTraits }, hint),
|
|
654
|
+
nullable,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const ap = def.additionalProperties;
|
|
658
|
+
if (ap !== undefined && ap !== false) {
|
|
659
|
+
const value = ap === true || (typeof ap === "object" && Object.keys(ap).length === 0)
|
|
660
|
+
? { target: PRELUDE.Document, nullable: false }
|
|
661
|
+
: convertSchema(ctx, ap, `${hint}Value`, depth + 1, dir);
|
|
662
|
+
return {
|
|
663
|
+
target: emit({
|
|
664
|
+
type: "map",
|
|
665
|
+
key: { target: PRELUDE.String },
|
|
666
|
+
value: { target: value.target },
|
|
667
|
+
traits: docTraits,
|
|
668
|
+
}, reservedId !== undefined ? hint : `${hint}Map`),
|
|
669
|
+
nullable,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return inline(PRELUDE.Document, nullable);
|
|
673
|
+
}
|
|
674
|
+
// --- scalars --------------------------------------------------------------
|
|
675
|
+
switch (t) {
|
|
676
|
+
case "string":
|
|
677
|
+
return inline(PRELUDE.String, nullable);
|
|
678
|
+
case "boolean":
|
|
679
|
+
return inline(PRELUDE.Boolean, nullable);
|
|
680
|
+
case "integer":
|
|
681
|
+
return inline(PRELUDE.Integer, nullable);
|
|
682
|
+
case "number":
|
|
683
|
+
return inline(PRELUDE.Double, nullable);
|
|
684
|
+
case "null":
|
|
685
|
+
return inline(PRELUDE.Document, true);
|
|
686
|
+
default:
|
|
687
|
+
return inline(PRELUDE.Document, nullable);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
/** Whether a property is a sensitive string (x-sensitive or name pattern). */
|
|
691
|
+
const isSensitiveProperty = (ctx, name, def) => {
|
|
692
|
+
const r = deref(ctx, def);
|
|
693
|
+
if (!r || typeof r !== "object")
|
|
694
|
+
return false;
|
|
695
|
+
const isString = typeOf(r) === "string" && !Array.isArray(r.enum);
|
|
696
|
+
if (!isString)
|
|
697
|
+
return false;
|
|
698
|
+
if (r["x-sensitive"] === true || def?.["x-sensitive"] === true)
|
|
699
|
+
return true;
|
|
700
|
+
return ctx.sensitivePatterns.some((p) => p.test(name));
|
|
701
|
+
};
|
|
702
|
+
/**
|
|
703
|
+
* Build a structure's members from an OpenAPI property map. Direction-
|
|
704
|
+
* excluded properties (`readOnly` in requests, `writeOnly` in responses)
|
|
705
|
+
* are dropped — along with any response-side `required` they carried.
|
|
706
|
+
*/
|
|
707
|
+
const buildMembers = (ctx, properties, required, hint, depth, dir) => {
|
|
708
|
+
const members = {};
|
|
709
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
710
|
+
if (dirExcluded(ctx, prop, dir))
|
|
711
|
+
continue;
|
|
712
|
+
let mn = memberIdent(name);
|
|
713
|
+
let k = 2;
|
|
714
|
+
while (mn in members)
|
|
715
|
+
mn = `${memberIdent(name)}_${k++}`;
|
|
716
|
+
const conv = convertSchema(ctx, prop, `${hint}${pascal(name)}`, depth, dir);
|
|
717
|
+
const traits = {};
|
|
718
|
+
const doc = prop && typeof prop === "object" && typeof prop.description === "string"
|
|
719
|
+
? prop.description
|
|
720
|
+
: undefined;
|
|
721
|
+
if (doc)
|
|
722
|
+
traits["smithy.api#documentation"] = doc;
|
|
723
|
+
if (required.has(name))
|
|
724
|
+
traits["smithy.api#required"] = {};
|
|
725
|
+
if (mn !== name)
|
|
726
|
+
traits["smithy.api#jsonName"] = name;
|
|
727
|
+
if (conv.nullable)
|
|
728
|
+
traits[NULLABLE_TRAIT] = {};
|
|
729
|
+
if (isSensitiveProperty(ctx, name, prop)) {
|
|
730
|
+
traits["smithy.api#sensitive"] = {};
|
|
731
|
+
}
|
|
732
|
+
members[mn] = {
|
|
733
|
+
target: conv.target,
|
|
734
|
+
...(Object.keys(traits).length ? { traits } : {}),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return members;
|
|
738
|
+
};
|
|
739
|
+
/** Normalize a (possibly `$ref`'d) parameter; Swagger 2.0 carries the schema inline. */
|
|
740
|
+
const normalizeParam = (ctx, raw) => {
|
|
741
|
+
const p = raw?.$ref ? resolvePointer(ctx.spec, raw.$ref) : raw;
|
|
742
|
+
if (!p || typeof p !== "object" || typeof p.name !== "string") {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
// Swagger 2.0: `in: body` parameters carry their (usually `$ref`'d)
|
|
746
|
+
// schema under `schema`; all other locations describe the type inline on
|
|
747
|
+
// the parameter itself.
|
|
748
|
+
const schema = ctx.version === "2.0"
|
|
749
|
+
? p.in === "body"
|
|
750
|
+
? (p.schema ?? {})
|
|
751
|
+
: {
|
|
752
|
+
type: p.type,
|
|
753
|
+
enum: p.enum,
|
|
754
|
+
items: p.items,
|
|
755
|
+
format: p.format,
|
|
756
|
+
"x-nullable": p["x-nullable"],
|
|
757
|
+
}
|
|
758
|
+
: (p.schema ?? { type: "string" });
|
|
759
|
+
return {
|
|
760
|
+
in: p.in,
|
|
761
|
+
name: p.name,
|
|
762
|
+
required: p.required === true || p.in === "path",
|
|
763
|
+
description: typeof p.description === "string" ? p.description : undefined,
|
|
764
|
+
schema,
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Path-level parameters merged before operation-level ones; an op-level
|
|
769
|
+
* parameter overrides a path-level one with the same (in, name). Header and
|
|
770
|
+
* cookie parameters are dropped (v0 parity); `in: body` (2.0) is handled by
|
|
771
|
+
* the request-body path.
|
|
772
|
+
*/
|
|
773
|
+
const collectParams = (ctx, pathItem, op) => {
|
|
774
|
+
const byKey = new Map();
|
|
775
|
+
for (const raw of [
|
|
776
|
+
...(Array.isArray(pathItem?.parameters) ? pathItem.parameters : []),
|
|
777
|
+
...(Array.isArray(op?.parameters) ? op.parameters : []),
|
|
778
|
+
]) {
|
|
779
|
+
const p = normalizeParam(ctx, raw);
|
|
780
|
+
if (!p)
|
|
781
|
+
continue;
|
|
782
|
+
byKey.set(`${p.in}${p.name}`, p);
|
|
783
|
+
}
|
|
784
|
+
return [...byKey.values()].filter((p) => p.in === "path" || p.in === "query" || p.in === "body");
|
|
785
|
+
};
|
|
786
|
+
/**
|
|
787
|
+
* Simple-type coercion for path labels (Smithy restricts label targets):
|
|
788
|
+
* enum → enum shape, integer → Integer, number → Double, boolean → Boolean,
|
|
789
|
+
* everything else → String.
|
|
790
|
+
*/
|
|
791
|
+
const labelTarget = (ctx, schema, hint) => {
|
|
792
|
+
const r = deref(ctx, schema);
|
|
793
|
+
if (Array.isArray(r?.enum) &&
|
|
794
|
+
r.enum.length > 0 &&
|
|
795
|
+
r.enum.every((v) => typeof v === "string")) {
|
|
796
|
+
return convertSchema(ctx, r, hint, 0, "in").target;
|
|
797
|
+
}
|
|
798
|
+
switch (typeOf(r)) {
|
|
799
|
+
case "integer":
|
|
800
|
+
return PRELUDE.Integer;
|
|
801
|
+
case "number":
|
|
802
|
+
return PRELUDE.Double;
|
|
803
|
+
case "boolean":
|
|
804
|
+
return PRELUDE.Boolean;
|
|
805
|
+
default:
|
|
806
|
+
return PRELUDE.String;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
// ============================================================================
|
|
810
|
+
// Docs
|
|
811
|
+
// ============================================================================
|
|
812
|
+
/**
|
|
813
|
+
* v0 trimming rules: stop at a `### Authorization` heading, strip markdown
|
|
814
|
+
* table rows (lines starting with `|`).
|
|
815
|
+
*/
|
|
816
|
+
const trimDescription = (desc) => {
|
|
817
|
+
if (typeof desc !== "string" || !desc)
|
|
818
|
+
return undefined;
|
|
819
|
+
const lines = [];
|
|
820
|
+
for (const line of desc.split(/\r?\n/)) {
|
|
821
|
+
if (line.trim().startsWith("### Authorization"))
|
|
822
|
+
break;
|
|
823
|
+
if (line.trim().startsWith("|"))
|
|
824
|
+
continue;
|
|
825
|
+
lines.push(line);
|
|
826
|
+
}
|
|
827
|
+
const out = lines.join("\n").trim();
|
|
828
|
+
return out || undefined;
|
|
829
|
+
};
|
|
830
|
+
const opDoc = (op) => {
|
|
831
|
+
const summary = typeof op.summary === "string" ? op.summary.trim() : undefined;
|
|
832
|
+
const desc = trimDescription(op.description);
|
|
833
|
+
const parts = [summary, desc].filter((s) => s !== undefined && s !== "");
|
|
834
|
+
return parts.length ? parts.join("\n\n") : undefined;
|
|
835
|
+
};
|
|
836
|
+
// ============================================================================
|
|
837
|
+
// Responses
|
|
838
|
+
// ============================================================================
|
|
839
|
+
/** 200 → 201 → 204 precedence; response-level `$ref` resolved; JSON only. */
|
|
840
|
+
const successSchema = (ctx, responses) => {
|
|
841
|
+
for (const code of ["200", "201", "204"]) {
|
|
842
|
+
const raw = responses?.[code];
|
|
843
|
+
if (!raw)
|
|
844
|
+
continue;
|
|
845
|
+
const resp = raw.$ref ? resolvePointer(ctx.spec, raw.$ref) : raw;
|
|
846
|
+
if (!resp || typeof resp !== "object")
|
|
847
|
+
return { schema: undefined };
|
|
848
|
+
if (ctx.version === "2.0") {
|
|
849
|
+
return { schema: resp.schema };
|
|
850
|
+
}
|
|
851
|
+
return { schema: resp.content?.["application/json"]?.schema };
|
|
852
|
+
}
|
|
853
|
+
return { schema: undefined };
|
|
854
|
+
};
|
|
855
|
+
const PAGINATION_INPUT_ALIASES = {
|
|
856
|
+
cursor: ["cursor", "page_token", "pageToken"],
|
|
857
|
+
token: ["next_token", "NextToken", "nextToken"],
|
|
858
|
+
page: ["page"],
|
|
859
|
+
};
|
|
860
|
+
const detectPagination = (ctx, params, responseSchema) => {
|
|
861
|
+
if (!responseSchema)
|
|
862
|
+
return undefined;
|
|
863
|
+
const bag = flattenObject(ctx, responseSchema).properties;
|
|
864
|
+
if (Object.keys(bag).length === 0)
|
|
865
|
+
return undefined;
|
|
866
|
+
let mode;
|
|
867
|
+
let outputToken;
|
|
868
|
+
const pag = bag["pagination"]
|
|
869
|
+
? flattenObject(ctx, bag["pagination"]).properties
|
|
870
|
+
: undefined;
|
|
871
|
+
if (pag?.["cursor"]) {
|
|
872
|
+
mode = "cursor";
|
|
873
|
+
outputToken = "pagination.cursor";
|
|
874
|
+
}
|
|
875
|
+
else if (pag?.["next"]) {
|
|
876
|
+
mode = "cursor";
|
|
877
|
+
outputToken = "pagination.next";
|
|
878
|
+
}
|
|
879
|
+
else if (pag?.["next_page"]) {
|
|
880
|
+
mode = "page";
|
|
881
|
+
outputToken = "pagination.next_page";
|
|
882
|
+
}
|
|
883
|
+
else if (bag["next_token"]) {
|
|
884
|
+
mode = "token";
|
|
885
|
+
outputToken = "next_token";
|
|
886
|
+
}
|
|
887
|
+
else if (bag["NextToken"]) {
|
|
888
|
+
mode = "token";
|
|
889
|
+
outputToken = "NextToken";
|
|
890
|
+
}
|
|
891
|
+
else if (bag["nextToken"]) {
|
|
892
|
+
mode = "token";
|
|
893
|
+
outputToken = "nextToken";
|
|
894
|
+
}
|
|
895
|
+
else if (bag["next_page"]) {
|
|
896
|
+
mode = "page";
|
|
897
|
+
outputToken = "next_page";
|
|
898
|
+
}
|
|
899
|
+
if (!mode || !outputToken)
|
|
900
|
+
return undefined;
|
|
901
|
+
const aliases = PAGINATION_INPUT_ALIASES[mode];
|
|
902
|
+
const inputToken = params.find((p) => p.in === "query" && aliases.includes(p.name))?.name;
|
|
903
|
+
if (!inputToken)
|
|
904
|
+
return undefined;
|
|
905
|
+
let items;
|
|
906
|
+
for (const [k, v] of Object.entries(bag)) {
|
|
907
|
+
if (k === "pagination" || k === "next_token" || k === "NextToken")
|
|
908
|
+
continue;
|
|
909
|
+
if (typeOf(deref(ctx, v)) === "array") {
|
|
910
|
+
items = k;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!items)
|
|
915
|
+
return undefined;
|
|
916
|
+
return { mode, inputToken, outputToken, items };
|
|
917
|
+
};
|
|
918
|
+
// ============================================================================
|
|
919
|
+
// Main conversion
|
|
920
|
+
// ============================================================================
|
|
921
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
922
|
+
const DEFAULT_STATUS_TO_ERROR_CLASS = {
|
|
923
|
+
"400": "BadRequest",
|
|
924
|
+
"403": "Forbidden",
|
|
925
|
+
"404": "NotFound",
|
|
926
|
+
"409": "Conflict",
|
|
927
|
+
"422": "UnprocessableEntity",
|
|
928
|
+
};
|
|
929
|
+
const DEFAULT_ERROR_STATUSES = ["401", "429", "500", "503"];
|
|
930
|
+
export const convertOpenApiToSmithy = (spec, options) => {
|
|
931
|
+
const doc = spec;
|
|
932
|
+
const version = detectVersion(doc);
|
|
933
|
+
const ctx = {
|
|
934
|
+
spec: doc,
|
|
935
|
+
version,
|
|
936
|
+
ns: options.namespace,
|
|
937
|
+
shapes: {},
|
|
938
|
+
names: new Set(),
|
|
939
|
+
refs: new Map(),
|
|
940
|
+
dirSensitiveRefs: new Map(),
|
|
941
|
+
sensitivePatterns: options.sensitivePatterns ?? SENSITIVE_FIELD_PATTERNS,
|
|
942
|
+
};
|
|
943
|
+
const statusToErrorClass = options.statusToErrorClass ?? DEFAULT_STATUS_TO_ERROR_CLASS;
|
|
944
|
+
const defaultErrorStatuses = new Set(options.defaultErrorStatuses ?? DEFAULT_ERROR_STATUSES);
|
|
945
|
+
const skipDeprecated = options.skipDeprecated ?? true;
|
|
946
|
+
// Error class names and the service name are reserved up front so schema
|
|
947
|
+
// components can never steal them.
|
|
948
|
+
const errorIds = new Map(); // class name → shape id
|
|
949
|
+
for (const cls of new Set(Object.values(statusToErrorClass))) {
|
|
950
|
+
errorIds.set(cls, `${ctx.ns}#${uniqueName(ctx, cls)}`);
|
|
951
|
+
}
|
|
952
|
+
const serviceName = uniqueName(ctx, options.serviceName);
|
|
953
|
+
const usedErrors = new Map(); // class name → first status
|
|
954
|
+
const serviceOps = [];
|
|
955
|
+
for (const [rawPath, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
956
|
+
if (!pathItem || typeof pathItem !== "object")
|
|
957
|
+
continue;
|
|
958
|
+
for (const method of HTTP_METHODS) {
|
|
959
|
+
const op = pathItem[method];
|
|
960
|
+
if (!op || typeof op !== "object")
|
|
961
|
+
continue;
|
|
962
|
+
if (skipDeprecated && op.deprecated === true)
|
|
963
|
+
continue;
|
|
964
|
+
const opName = pascal(typeof op.operationId === "string" && op.operationId
|
|
965
|
+
? op.operationId
|
|
966
|
+
: `${method}_${rawPath}`);
|
|
967
|
+
const params = collectParams(ctx, pathItem, op);
|
|
968
|
+
// ---- URI + labels (sanitize placeholder names to member idents) ----
|
|
969
|
+
let uri = rawPath.split(/[?#]/)[0];
|
|
970
|
+
const rawLabels = Array.from(uri.matchAll(/\{([^}]+)\}/g)).map((m) => m[1]);
|
|
971
|
+
const members = {};
|
|
972
|
+
const addMember = (name, member) => {
|
|
973
|
+
if (name in members)
|
|
974
|
+
return false;
|
|
975
|
+
members[name] = member;
|
|
976
|
+
return true;
|
|
977
|
+
};
|
|
978
|
+
for (const raw of rawLabels) {
|
|
979
|
+
const san = memberIdent(raw);
|
|
980
|
+
if (san !== raw)
|
|
981
|
+
uri = uri.split(`{${raw}}`).join(`{${san}}`);
|
|
982
|
+
const p = params.find((x) => x.in === "path" && x.name === raw);
|
|
983
|
+
addMember(san, {
|
|
984
|
+
target: labelTarget(ctx, p?.schema, `${opName}Request${pascal(san)}`),
|
|
985
|
+
traits: {
|
|
986
|
+
"smithy.api#httpLabel": {},
|
|
987
|
+
"smithy.api#required": {},
|
|
988
|
+
...(p?.description
|
|
989
|
+
? { "smithy.api#documentation": p.description }
|
|
990
|
+
: {}),
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
// ---- Query params ----
|
|
995
|
+
for (const p of params) {
|
|
996
|
+
if (p.in !== "query")
|
|
997
|
+
continue;
|
|
998
|
+
if (options.apiVersion !== undefined && p.name === "api-version") {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const san = memberIdent(p.name);
|
|
1002
|
+
const conv = convertSchema(ctx, p.schema, `${opName}Request${pascal(p.name)}`, 0, "in");
|
|
1003
|
+
addMember(san, {
|
|
1004
|
+
target: conv.target,
|
|
1005
|
+
traits: {
|
|
1006
|
+
"smithy.api#httpQuery": p.name,
|
|
1007
|
+
...(p.required ? { "smithy.api#required": {} } : {}),
|
|
1008
|
+
...(p.description
|
|
1009
|
+
? { "smithy.api#documentation": p.description }
|
|
1010
|
+
: {}),
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
// ---- Request body (json > form-urlencoded > multipart) ----
|
|
1015
|
+
let contentType;
|
|
1016
|
+
let bodySchema;
|
|
1017
|
+
let bodyRequired = false;
|
|
1018
|
+
if (version === "2.0") {
|
|
1019
|
+
const bodyParam = params.find((p) => p.in === "body");
|
|
1020
|
+
bodySchema = bodyParam?.schema;
|
|
1021
|
+
bodyRequired = bodyParam?.required === true;
|
|
1022
|
+
}
|
|
1023
|
+
else if (op.requestBody) {
|
|
1024
|
+
const rb = op.requestBody.$ref
|
|
1025
|
+
? resolvePointer(doc, op.requestBody.$ref)
|
|
1026
|
+
: op.requestBody;
|
|
1027
|
+
bodyRequired = rb?.required === true;
|
|
1028
|
+
const content = rb?.content ?? {};
|
|
1029
|
+
if (content["application/json"]) {
|
|
1030
|
+
bodySchema = content["application/json"].schema;
|
|
1031
|
+
}
|
|
1032
|
+
else if (content["application/x-www-form-urlencoded"]) {
|
|
1033
|
+
bodySchema = content["application/x-www-form-urlencoded"].schema;
|
|
1034
|
+
contentType = "form-urlencoded";
|
|
1035
|
+
}
|
|
1036
|
+
else if (content["multipart/form-data"]) {
|
|
1037
|
+
bodySchema = content["multipart/form-data"].schema;
|
|
1038
|
+
contentType = "multipart";
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (bodySchema !== undefined) {
|
|
1042
|
+
const flat = flattenObject(ctx, bodySchema);
|
|
1043
|
+
if (Object.keys(flat.properties).length > 0) {
|
|
1044
|
+
const bodyMembers = buildMembers(ctx, flat.properties, flat.required, `${opName}Request`, 0, "in");
|
|
1045
|
+
for (const [mn, m] of Object.entries(bodyMembers)) {
|
|
1046
|
+
addMember(mn, m); // labels win, then query, then body
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
// Non-flattenable body (bare array/scalar/map/union) → sole TYPED
|
|
1051
|
+
// `body` member sent as the entire request body
|
|
1052
|
+
// (`smithy.api#httpPayload`). A schema-less JSON body (empty or
|
|
1053
|
+
// bare-object schema → Document) gets NO body member at all — the
|
|
1054
|
+
// runtime's unknown-key passthrough is the escape hatch, and an
|
|
1055
|
+
// opaque `body: unknown` payload member would swallow the typed
|
|
1056
|
+
// surface.
|
|
1057
|
+
const conv = convertSchema(ctx, bodySchema, `${opName}RequestBody`, 0, "in");
|
|
1058
|
+
if (conv.target !== PRELUDE.Document) {
|
|
1059
|
+
addMember("body", {
|
|
1060
|
+
target: conv.target,
|
|
1061
|
+
traits: {
|
|
1062
|
+
"smithy.api#httpPayload": {},
|
|
1063
|
+
...(bodyRequired ? { "smithy.api#required": {} } : {}),
|
|
1064
|
+
...(conv.nullable ? { [NULLABLE_TRAIT]: {} } : {}),
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// ---- Input shape ----
|
|
1071
|
+
const inputTarget = Object.keys(members).length > 0
|
|
1072
|
+
? addShape(ctx, `${opName}Request`, {
|
|
1073
|
+
type: "structure",
|
|
1074
|
+
members,
|
|
1075
|
+
traits: { "smithy.api#input": {} },
|
|
1076
|
+
})
|
|
1077
|
+
: PRELUDE.Unit;
|
|
1078
|
+
// ---- Output shape ----
|
|
1079
|
+
const { schema: respSchema } = successSchema(ctx, op.responses);
|
|
1080
|
+
let outputTarget = PRELUDE.Unit;
|
|
1081
|
+
if (respSchema !== undefined) {
|
|
1082
|
+
const flat = flattenObject(ctx, respSchema);
|
|
1083
|
+
if (Object.keys(flat.properties).length > 0) {
|
|
1084
|
+
const resolved = deref(ctx, respSchema);
|
|
1085
|
+
const isPlainRef = typeof respSchema.$ref === "string" &&
|
|
1086
|
+
!Array.isArray(resolved?.allOf) &&
|
|
1087
|
+
isNameable(ctx, resolved);
|
|
1088
|
+
if (isPlainRef) {
|
|
1089
|
+
// Reuse the named component shape as the output directly.
|
|
1090
|
+
outputTarget = convertSchema(ctx, respSchema, opName, 0, "out").target;
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
outputTarget = addShape(ctx, `${opName}Response`, {
|
|
1094
|
+
type: "structure",
|
|
1095
|
+
members: buildMembers(ctx, flat.properties, flat.required, `${opName}Response`, 0, "out"),
|
|
1096
|
+
traits: { "smithy.api#output": {} },
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
// Non-flattenable response (bare array/scalar/map/union, or an
|
|
1102
|
+
// opaque object) → wrapper whose sole TYPED member IS the payload;
|
|
1103
|
+
// the SdkSpec's rootPipe collapses the wrapper.
|
|
1104
|
+
const conv = convertSchema(ctx, respSchema, `${opName}ResponseBody`, 0, "out");
|
|
1105
|
+
if (conv.target !== PRELUDE.Document || deref(ctx, respSchema)) {
|
|
1106
|
+
outputTarget = addShape(ctx, `${opName}Response`, {
|
|
1107
|
+
type: "structure",
|
|
1108
|
+
members: {
|
|
1109
|
+
body: {
|
|
1110
|
+
target: conv.target,
|
|
1111
|
+
traits: {
|
|
1112
|
+
[RAW_RESPONSE_TRAIT]: {},
|
|
1113
|
+
"smithy.api#required": {},
|
|
1114
|
+
...(conv.nullable ? { [NULLABLE_TRAIT]: {} } : {}),
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
traits: { "smithy.api#output": {} },
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
// ---- Errors ----
|
|
1124
|
+
const errors = [];
|
|
1125
|
+
for (const status of Object.keys(op.responses ?? {})) {
|
|
1126
|
+
if (!/^[45]\d\d$/.test(status))
|
|
1127
|
+
continue;
|
|
1128
|
+
if (defaultErrorStatuses.has(status))
|
|
1129
|
+
continue;
|
|
1130
|
+
const cls = statusToErrorClass[status];
|
|
1131
|
+
if (!cls)
|
|
1132
|
+
continue;
|
|
1133
|
+
const id = errorIds.get(cls);
|
|
1134
|
+
if (!usedErrors.has(cls))
|
|
1135
|
+
usedErrors.set(cls, Number(status));
|
|
1136
|
+
if (!errors.some((e) => e.target === id))
|
|
1137
|
+
errors.push({ target: id });
|
|
1138
|
+
}
|
|
1139
|
+
// ---- Pagination ----
|
|
1140
|
+
const pagination = detectPagination(ctx, params, respSchema);
|
|
1141
|
+
// ---- Operation shape ----
|
|
1142
|
+
const httpTrait = {
|
|
1143
|
+
method: method.toUpperCase(),
|
|
1144
|
+
uri,
|
|
1145
|
+
code: 200,
|
|
1146
|
+
};
|
|
1147
|
+
if (contentType === "multipart")
|
|
1148
|
+
httpTrait.contentType = "multipart";
|
|
1149
|
+
const traits = { "smithy.api#http": httpTrait };
|
|
1150
|
+
const documentation = opDoc(op);
|
|
1151
|
+
if (documentation)
|
|
1152
|
+
traits["smithy.api#documentation"] = documentation;
|
|
1153
|
+
if (pagination)
|
|
1154
|
+
traits["smithy.api#paginated"] = pagination;
|
|
1155
|
+
if (contentType)
|
|
1156
|
+
traits[CONTENT_TYPE_TRAIT] = contentType;
|
|
1157
|
+
if (options.apiVersion !== undefined) {
|
|
1158
|
+
traits[API_VERSION_TRAIT] = options.apiVersion;
|
|
1159
|
+
}
|
|
1160
|
+
const opId = addShape(ctx, opName, {
|
|
1161
|
+
type: "operation",
|
|
1162
|
+
input: { target: inputTarget },
|
|
1163
|
+
output: { target: outputTarget },
|
|
1164
|
+
...(errors.length ? { errors } : {}),
|
|
1165
|
+
traits,
|
|
1166
|
+
});
|
|
1167
|
+
serviceOps.push({ target: opId });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// ---- Error shapes (one per used class) ----
|
|
1171
|
+
for (const [cls, status] of usedErrors) {
|
|
1172
|
+
const id = errorIds.get(cls);
|
|
1173
|
+
const base = {
|
|
1174
|
+
type: "structure",
|
|
1175
|
+
members: {},
|
|
1176
|
+
traits: {
|
|
1177
|
+
"smithy.api#error": status < 500 ? "client" : "server",
|
|
1178
|
+
"smithy.api#httpError": status,
|
|
1179
|
+
[ERROR_MATCHERS_TRAIT]: [{ status }],
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
const override = options.errorShapes?.[cls];
|
|
1183
|
+
ctx.shapes[id] = override
|
|
1184
|
+
? {
|
|
1185
|
+
...base,
|
|
1186
|
+
...override,
|
|
1187
|
+
traits: { ...base.traits, ...(override.traits ?? {}) },
|
|
1188
|
+
}
|
|
1189
|
+
: base;
|
|
1190
|
+
}
|
|
1191
|
+
// ---- Service shape ----
|
|
1192
|
+
ctx.shapes[`${ctx.ns}#${serviceName}`] = {
|
|
1193
|
+
type: "service",
|
|
1194
|
+
version: options.serviceVersion ??
|
|
1195
|
+
(typeof doc.info?.version === "string" ? doc.info.version : "1.0"),
|
|
1196
|
+
operations: serviceOps,
|
|
1197
|
+
traits: {
|
|
1198
|
+
"smithy.api#title": typeof doc.info?.title === "string"
|
|
1199
|
+
? doc.info.title
|
|
1200
|
+
: options.serviceName,
|
|
1201
|
+
...(typeof doc.info?.description === "string"
|
|
1202
|
+
? {
|
|
1203
|
+
"smithy.api#documentation": trimDescription(doc.info.description),
|
|
1204
|
+
}
|
|
1205
|
+
: {}),
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
return {
|
|
1209
|
+
smithy: "2.0",
|
|
1210
|
+
metadata: {
|
|
1211
|
+
suppressions: [
|
|
1212
|
+
{ id: "HttpUriConflict", namespace: "*" },
|
|
1213
|
+
{ id: "HttpMethodSemantics", namespace: "*" },
|
|
1214
|
+
{ id: "UnreferencedShape", namespace: "*" },
|
|
1215
|
+
],
|
|
1216
|
+
},
|
|
1217
|
+
shapes: ctx.shapes,
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
//# sourceMappingURL=openapi.js.map
|