@distilled.cloud/core 1.0.0-rc.1 → 1.0.0-rc.11
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/LICENSE +201 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +16 -4
- package/lib/api.js.map +1 -1
- package/lib/category.d.ts +4 -4
- package/lib/category.js +4 -4
- package/lib/codegen/boolean-string-enums.d.ts +36 -0
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.js +94 -0
- package/lib/codegen/boolean-string-enums.js.map +1 -0
- package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
- package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.test.js +147 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -0
- package/lib/codegen/cli.d.ts +56 -6
- package/lib/codegen/cli.d.ts.map +1 -1
- package/lib/codegen/cli.js +59 -75
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/emit.d.ts +3 -7
- package/lib/codegen/emit.d.ts.map +1 -1
- package/lib/codegen/emit.js +6 -10
- package/lib/codegen/emit.js.map +1 -1
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +142 -20
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/graphql-client.d.ts +62 -0
- package/lib/codegen/graphql-client.d.ts.map +1 -0
- package/lib/codegen/graphql-client.js +294 -0
- package/lib/codegen/graphql-client.js.map +1 -0
- package/lib/codegen/graphql-client.test.d.ts +2 -0
- package/lib/codegen/graphql-client.test.d.ts.map +1 -0
- package/lib/codegen/graphql-client.test.js +311 -0
- package/lib/codegen/graphql-client.test.js.map +1 -0
- package/lib/codegen/graphql.d.ts +207 -0
- package/lib/codegen/graphql.d.ts.map +1 -0
- package/lib/codegen/graphql.js +799 -0
- package/lib/codegen/graphql.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +17 -3
- package/lib/codegen/openapi-cli.d.ts.map +1 -1
- package/lib/codegen/openapi-cli.js +50 -48
- package/lib/codegen/openapi-cli.js.map +1 -1
- package/lib/codegen/openapi.d.ts +69 -6
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +173 -16
- package/lib/codegen/openapi.js.map +1 -1
- package/lib/codegen/patches.d.ts +65 -0
- package/lib/codegen/patches.d.ts.map +1 -0
- package/lib/codegen/patches.js +236 -0
- package/lib/codegen/patches.js.map +1 -0
- package/lib/codegen/patches.test.d.ts +2 -0
- package/lib/codegen/patches.test.d.ts.map +1 -0
- package/lib/codegen/patches.test.js +105 -0
- package/lib/codegen/patches.test.js.map +1 -0
- package/lib/codegen/proto.d.ts +121 -0
- package/lib/codegen/proto.d.ts.map +1 -0
- package/lib/codegen/proto.js +962 -0
- package/lib/codegen/proto.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
- package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.js +1079 -0
- package/lib/codegen/rewrite-operation-ids.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.js +533 -0
- package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
- package/lib/codegen/spec-path.d.ts +16 -0
- package/lib/codegen/spec-path.d.ts.map +1 -0
- package/lib/codegen/spec-path.js +101 -0
- package/lib/codegen/spec-path.js.map +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +17 -13
- package/lib/errors.js.map +1 -1
- package/lib/graphql.d.ts +284 -0
- package/lib/graphql.d.ts.map +1 -0
- package/lib/graphql.fixture.d.ts +249 -0
- package/lib/graphql.fixture.d.ts.map +1 -0
- package/lib/graphql.fixture.js +240 -0
- package/lib/graphql.fixture.js.map +1 -0
- package/lib/graphql.js +718 -0
- package/lib/graphql.js.map +1 -0
- package/lib/graphql.test.d.ts +2 -0
- package/lib/graphql.test.d.ts.map +1 -0
- package/lib/graphql.test.js +780 -0
- package/lib/graphql.test.js.map +1 -0
- package/lib/graphql.types.d.ts +2 -0
- package/lib/graphql.types.d.ts.map +1 -0
- package/lib/graphql.types.js +45 -0
- package/lib/graphql.types.js.map +1 -0
- package/lib/json-patch.d.ts +18 -11
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +63 -25
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +43 -3
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +91 -5
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +62 -26
- package/lib/protocol-http.js.map +1 -1
- package/lib/protocol-http.test.d.ts +2 -0
- package/lib/protocol-http.test.d.ts.map +1 -0
- package/lib/protocol-http.test.js +88 -0
- package/lib/protocol-http.test.js.map +1 -0
- package/lib/protocol-rest.d.ts +12 -2
- package/lib/protocol-rest.d.ts.map +1 -1
- package/lib/protocol-rest.js +17 -3
- package/lib/protocol-rest.js.map +1 -1
- package/lib/retry.js +1 -1
- package/lib/schema.d.ts +1 -1
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +1 -1
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +27 -3
- package/lib/trait.d.ts.map +1 -1
- package/lib/trait.js +17 -1
- package/lib/trait.js.map +1 -1
- package/package.json +13 -10
- package/src/api.ts +18 -4
- package/src/category.ts +4 -4
- package/src/codegen/boolean-string-enums.test.ts +168 -0
- package/src/codegen/boolean-string-enums.ts +106 -0
- package/src/codegen/cli.ts +127 -110
- package/src/codegen/emit.ts +6 -10
- package/src/codegen/generator.ts +151 -21
- package/src/codegen/graphql-client.test.ts +386 -0
- package/src/codegen/graphql-client.ts +419 -0
- package/src/codegen/graphql.ts +1217 -0
- package/src/codegen/openapi-cli.ts +75 -59
- package/src/codegen/openapi.ts +255 -16
- package/src/codegen/patches.test.ts +130 -0
- package/src/codegen/patches.ts +291 -0
- package/src/codegen/proto.ts +1128 -0
- package/src/codegen/rewrite-operation-ids.test.ts +563 -0
- package/src/codegen/rewrite-operation-ids.ts +1206 -0
- package/src/codegen/spec-path.ts +115 -0
- package/src/errors.ts +20 -25
- package/src/graphql.fixture.ts +371 -0
- package/src/graphql.test.ts +974 -0
- package/src/graphql.ts +1321 -0
- package/src/graphql.types.ts +185 -0
- package/src/json-patch.ts +82 -25
- package/src/pagination.ts +134 -7
- package/src/protocol-http.test.ts +107 -0
- package/src/protocol-http.ts +67 -31
- package/src/protocol-rest.ts +28 -4
- package/src/retry.ts +1 -1
- package/src/schema.ts +2 -2
- package/src/trait.ts +39 -3
|
@@ -2,29 +2,29 @@
|
|
|
2
2
|
* OpenAPI → Smithy pipeline helper (dev-time only, provider-agnostic).
|
|
3
3
|
*
|
|
4
4
|
* Owns the spec-side pipeline every OpenAPI-sourced provider shares: read the
|
|
5
|
-
* spec file, apply
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* spec file, apply OpenAPI RFC-6902 ops to the document, convert with
|
|
6
|
+
* {@link convertOpenApiToSmithy} (which owns verbNoun naming), apply Smithy
|
|
7
|
+
* RFC-6902 ops (`/shapes`, `/metadata`) to the model, and write
|
|
8
|
+
* `.generated-specs/<name>.json`. Stale targets fail unless
|
|
9
|
+
* `onStalePatch: "warn"`.
|
|
10
10
|
*
|
|
11
|
-
* A provider's `scripts/convert.ts` is: a `runOpenApiConvert` call.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `patches/` should pass `patchesDir: false` to `runGeneratorCli` — the
|
|
15
|
-
* patch chain applies HERE, to the OpenAPI document, not to the Smithy model.
|
|
11
|
+
* A provider's `scripts/convert.ts` is: a `runOpenApiConvert` call.
|
|
12
|
+
* `scripts/generate.ts` compiles the already-patched models and does not
|
|
13
|
+
* apply patches.
|
|
16
14
|
*/
|
|
17
15
|
import * as fs from "node:fs/promises";
|
|
18
16
|
import * as path from "node:path";
|
|
19
|
-
import {
|
|
20
|
-
applyOperation,
|
|
21
|
-
isStaleTargetError,
|
|
22
|
-
type PatchFile,
|
|
23
|
-
} from "../json-patch.ts";
|
|
24
17
|
import {
|
|
25
18
|
convertOpenApiToSmithy,
|
|
26
19
|
type OpenApiConvertOptions,
|
|
27
20
|
} from "./openapi.ts";
|
|
21
|
+
import {
|
|
22
|
+
applyRfc6902Files,
|
|
23
|
+
finalizeConvert,
|
|
24
|
+
isSmithyPatchPath,
|
|
25
|
+
listRfc6902PatchFiles,
|
|
26
|
+
} from "./patches.ts";
|
|
27
|
+
import { resolveSpecPath } from "./spec-path.ts";
|
|
28
28
|
|
|
29
29
|
export interface OpenApiSpecEntry {
|
|
30
30
|
/** Output model name — written to `<outDir>/<name>.json`. */
|
|
@@ -35,7 +35,7 @@ export interface OpenApiSpecEntry {
|
|
|
35
35
|
* Hook between patching and conversion (e.g. path prefixing, server
|
|
36
36
|
* rewrites). May mutate the spec in place or return a replacement.
|
|
37
37
|
*/
|
|
38
|
-
readonly preprocess?: (spec: any) => unknown | void
|
|
38
|
+
readonly preprocess?: (spec: any) => unknown | void | Promise<unknown | void>;
|
|
39
39
|
/** Per-spec converter option overrides (merged over the shared options). */
|
|
40
40
|
readonly options?: Partial<OpenApiConvertOptions>;
|
|
41
41
|
}
|
|
@@ -46,9 +46,10 @@ export interface RunOpenApiConvertOptions {
|
|
|
46
46
|
readonly specs: readonly OpenApiSpecEntry[];
|
|
47
47
|
/**
|
|
48
48
|
* RFC-6902 patch chain root, relative to `root`. Default `"patches"`;
|
|
49
|
-
* `false` disables. Layout: `<patchesDir>/<name>/*.
|
|
49
|
+
* `false` disables. Layout: `<patchesDir>/<name>/*.json` when the
|
|
50
50
|
* per-spec directory exists; for single-spec providers a flat
|
|
51
|
-
* `<patchesDir>/*.
|
|
51
|
+
* `<patchesDir>/*.json` also works. Ops under `/shapes` or `/metadata`
|
|
52
|
+
* apply to the Smithy model after conversion; the rest apply to OpenAPI.
|
|
52
53
|
*/
|
|
53
54
|
readonly patchesDir?: string | false;
|
|
54
55
|
/** Output directory, relative to `root`. Default `".generated-specs"`. */
|
|
@@ -60,6 +61,19 @@ export interface RunOpenApiConvertOptions {
|
|
|
60
61
|
readonly parse?: (text: string, specPath: string) => unknown;
|
|
61
62
|
/** Shared converter options (per-spec `options` merge over these). */
|
|
62
63
|
readonly options: OpenApiConvertOptions;
|
|
64
|
+
/**
|
|
65
|
+
* What to do when a patch JSON pointer does not resolve. Default `"fail"`:
|
|
66
|
+
* silent skip is how a whole patch chain can vanish after an upstream path
|
|
67
|
+
* prefix change. `"warn"` restores the old skip-and-continue behaviour.
|
|
68
|
+
*/
|
|
69
|
+
readonly onStalePatch?: "fail" | "warn";
|
|
70
|
+
/**
|
|
71
|
+
* Run {@link finalizeConvert} on the written models (reference check +
|
|
72
|
+
* finalized marker; naming already happened in the converter). Default
|
|
73
|
+
* true. Pass false when the caller finalizes once after several convert
|
|
74
|
+
* steps (Fly machines + sprites + addons).
|
|
75
|
+
*/
|
|
76
|
+
readonly finalize?: boolean;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
const exists = async (p: string): Promise<boolean> => {
|
|
@@ -81,6 +95,7 @@ export const runOpenApiConvert = async (
|
|
|
81
95
|
? undefined
|
|
82
96
|
: path.resolve(o.root, o.patchesDir ?? "patches");
|
|
83
97
|
const parse = o.parse ?? ((text: string) => JSON.parse(text));
|
|
98
|
+
const onStalePatch = o.onStalePatch ?? "fail";
|
|
84
99
|
|
|
85
100
|
await fs.mkdir(outDir, { recursive: true });
|
|
86
101
|
|
|
@@ -88,10 +103,11 @@ export const runOpenApiConvert = async (
|
|
|
88
103
|
console.log(` Output: ${outDir}`);
|
|
89
104
|
|
|
90
105
|
for (const entry of o.specs) {
|
|
91
|
-
|
|
106
|
+
// Production path by default; `specs/.local` under DISTILLED_SPECS_LOCAL.
|
|
107
|
+
const specPath = resolveSpecPath(o.root, entry.specPath);
|
|
92
108
|
let spec: any = parse(await fs.readFile(specPath, "utf8"), specPath);
|
|
93
109
|
|
|
94
|
-
// ---- RFC-6902
|
|
110
|
+
// ---- RFC-6902: OpenAPI ops before convert, Smithy ops after ----
|
|
95
111
|
let patchDir: string | undefined;
|
|
96
112
|
if (patchRoot && (await exists(patchRoot))) {
|
|
97
113
|
const perSpec = path.join(patchRoot, entry.name);
|
|
@@ -101,66 +117,66 @@ export const runOpenApiConvert = async (
|
|
|
101
117
|
patchDir = patchRoot;
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
) as PatchFile;
|
|
115
|
-
for (const patchOp of parsed.patches ?? []) {
|
|
116
|
-
try {
|
|
117
|
-
applyOperation(spec, patchOp);
|
|
118
|
-
} catch (e) {
|
|
119
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
120
|
-
if (isStaleTargetError(msg)) {
|
|
121
|
-
// Spec drift — the patch target no longer exists upstream.
|
|
122
|
-
staleOps++;
|
|
123
|
-
console.warn(
|
|
124
|
-
` ⚠️ stale: ${entry.name}/${pf} [${patchOp.op} ${patchOp.path}]`,
|
|
125
|
-
);
|
|
126
|
-
} else {
|
|
127
|
-
badPatches.push(
|
|
128
|
-
`${entry.name}/${pf} [${patchOp.op} ${patchOp.path}]: ${msg}`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
patchFileCount++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (badPatches.length) {
|
|
137
|
-
for (const b of badPatches) console.error(`❌ bad patch: ${b}`);
|
|
120
|
+
const patchFiles = patchDir ? await listRfc6902PatchFiles(patchDir) : [];
|
|
121
|
+
const patchLabel = (file: string) => `${entry.name}/${path.basename(file)}`;
|
|
122
|
+
const openapiPatches = await applyRfc6902Files(spec, patchFiles, {
|
|
123
|
+
onStalePatch,
|
|
124
|
+
include: (op) => !isSmithyPatchPath(op.path),
|
|
125
|
+
label: patchLabel,
|
|
126
|
+
});
|
|
127
|
+
if (openapiPatches.errors.length) {
|
|
128
|
+
for (const b of openapiPatches.errors)
|
|
129
|
+
console.error(`❌ bad patch: ${b}`);
|
|
138
130
|
throw new Error(
|
|
139
|
-
`${
|
|
131
|
+
`${openapiPatches.errors.length} patch operation(s) failed — fix the pointers or delete the patch`,
|
|
140
132
|
);
|
|
141
133
|
}
|
|
142
134
|
|
|
143
135
|
// ---- Preprocess hook ----
|
|
144
136
|
if (entry.preprocess) {
|
|
145
|
-
const replaced = entry.preprocess(spec);
|
|
137
|
+
const replaced = await entry.preprocess(spec);
|
|
146
138
|
if (replaced !== undefined) spec = replaced;
|
|
147
139
|
}
|
|
148
140
|
|
|
149
|
-
// ---- Convert
|
|
141
|
+
// ---- Convert, Smithy patches, write ----
|
|
150
142
|
const model = convertOpenApiToSmithy(spec, {
|
|
151
143
|
...o.options,
|
|
152
144
|
...entry.options,
|
|
153
145
|
});
|
|
146
|
+
const smithyPatches = await applyRfc6902Files(model, patchFiles, {
|
|
147
|
+
onStalePatch,
|
|
148
|
+
include: (op) => isSmithyPatchPath(op.path),
|
|
149
|
+
label: patchLabel,
|
|
150
|
+
});
|
|
151
|
+
if (smithyPatches.errors.length) {
|
|
152
|
+
for (const b of smithyPatches.errors) console.error(`❌ bad patch: ${b}`);
|
|
153
|
+
throw new Error(
|
|
154
|
+
`${smithyPatches.errors.length} Smithy patch operation(s) failed — fix the pointers or delete the patch`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
154
157
|
const opCount = Object.values(model.shapes).filter(
|
|
155
158
|
(s: any) => s.type === "operation",
|
|
156
159
|
).length;
|
|
157
160
|
const outPath = path.join(outDir, `${entry.name}.json`);
|
|
158
161
|
await fs.writeFile(outPath, JSON.stringify(model, null, 2) + "\n");
|
|
162
|
+
const staleOps = openapiPatches.stale + smithyPatches.stale;
|
|
159
163
|
console.log(
|
|
160
164
|
` ✅ ${entry.name}: ${opCount} operations, ${Object.keys(model.shapes).length} shapes` +
|
|
161
|
-
(
|
|
162
|
-
? ` (${
|
|
165
|
+
(patchFiles.length
|
|
166
|
+
? ` (${patchFiles.length} patch file(s) applied${staleOps ? `, ${staleOps} stale op(s) skipped` : ""})`
|
|
163
167
|
: ""),
|
|
164
168
|
);
|
|
165
169
|
}
|
|
170
|
+
|
|
171
|
+
if (o.finalize !== false) {
|
|
172
|
+
const written = new Set(o.specs.map((s) => s.name));
|
|
173
|
+
await finalizeConvert({
|
|
174
|
+
root: o.root,
|
|
175
|
+
outDir,
|
|
176
|
+
// Patches and naming already ran above.
|
|
177
|
+
patchesDir: false,
|
|
178
|
+
operationNaming: "as-is",
|
|
179
|
+
include: (resource) => written.has(resource),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
166
182
|
};
|
package/src/codegen/openapi.ts
CHANGED
|
@@ -7,20 +7,28 @@
|
|
|
7
7
|
* `generateService` compiler. Conversion fidelity follows distilled v0's
|
|
8
8
|
* `generate-openapi.ts` feature matrix:
|
|
9
9
|
*
|
|
10
|
-
* • one operation per (path × get/post/put/patch/delete
|
|
11
|
-
*
|
|
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"`
|
|
12
15
|
* • input `<Op>Request`: path params → `smithy.api#httpLabel` (+required),
|
|
13
|
-
* query params → `smithy.api#httpQuery`, header
|
|
14
|
-
*
|
|
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);
|
|
15
21
|
* non-object bodies become a sole `body` member with `smithy.api#httpPayload`
|
|
16
22
|
* • `$ref`s become NAMED shapes (`components/schemas/X` → `<ns>#X`), reused
|
|
17
23
|
* across operations; anonymous nested objects synthesize names from the
|
|
18
24
|
* parent + member path
|
|
19
|
-
* • responses: 200 → 201 → 204 precedence
|
|
25
|
+
* • responses: 200 → 201 → 204 precedence (`successStatuses` overrides),
|
|
26
|
+
* `application/json` only; object
|
|
20
27
|
* results become `<Op>Response` structures (a sole `$ref` reuses the named
|
|
21
28
|
* shape); bare array/scalar results wrap in a structure whose single
|
|
22
29
|
* member carries `com.distilled.openapi#rawResponse` (the SdkSpec maps it
|
|
23
|
-
* to a root pipe); no schema → `smithy.api#Unit`
|
|
30
|
+
* to a root pipe); no schema → `smithy.api#Unit`; `head` is always
|
|
31
|
+
* `smithy.api#Unit`, whatever content the spec declares
|
|
24
32
|
* • nullability (3.0 `nullable`, 3.1 `type: [..., "null"]`, 2.0
|
|
25
33
|
* `x-nullable`, `oneOf`/`anyOf` null branches) → member-level
|
|
26
34
|
* `com.distilled.openapi#nullable` trait
|
|
@@ -36,6 +44,15 @@
|
|
|
36
44
|
* (with the non-standard `mode` member the core runtime dispatches on)
|
|
37
45
|
*/
|
|
38
46
|
|
|
47
|
+
import {
|
|
48
|
+
isMechanicalOperationId,
|
|
49
|
+
isVerbatimRouteId,
|
|
50
|
+
pathToVerbNoun,
|
|
51
|
+
resolveOperationName,
|
|
52
|
+
toVerbNoun,
|
|
53
|
+
type OperationIdRewrite,
|
|
54
|
+
} from "./rewrite-operation-ids.ts";
|
|
55
|
+
|
|
39
56
|
// ============================================================================
|
|
40
57
|
// Trait ids (the `com.distilled.openapi` vocabulary)
|
|
41
58
|
// ============================================================================
|
|
@@ -122,6 +139,37 @@ export interface OpenApiConvertOptions {
|
|
|
122
139
|
readonly defaultErrorStatuses?: Iterable<string>;
|
|
123
140
|
/** Skip operations marked `deprecated: true`. Default true. */
|
|
124
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[];
|
|
125
173
|
/**
|
|
126
174
|
* Azure-style fixed `api-version`: drops `api-version` query params and
|
|
127
175
|
* stamps {@link API_VERSION_TRAIT} on every operation.
|
|
@@ -140,6 +188,29 @@ export interface OpenApiConvertOptions {
|
|
|
140
188
|
* {@link ERROR_MATCHERS_TRAIT} traits; overrides are merged over it.
|
|
141
189
|
*/
|
|
142
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;
|
|
143
214
|
}
|
|
144
215
|
|
|
145
216
|
export interface SmithyModel {
|
|
@@ -177,6 +248,24 @@ const memberIdent = (name: string): string => {
|
|
|
177
248
|
return out || "_";
|
|
178
249
|
};
|
|
179
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
|
+
|
|
180
269
|
const enumMemberName = (value: string): string => {
|
|
181
270
|
let out = value
|
|
182
271
|
.toUpperCase()
|
|
@@ -763,7 +852,14 @@ const convertSchema = (
|
|
|
763
852
|
const item = convertSchema(ctx, def.items, `${hint}Item`, depth + 1, dir);
|
|
764
853
|
return {
|
|
765
854
|
target: emit(
|
|
766
|
-
{
|
|
855
|
+
{
|
|
856
|
+
type: "list",
|
|
857
|
+
member: {
|
|
858
|
+
target: item.target,
|
|
859
|
+
...(item.nullable ? { traits: { [NULLABLE_TRAIT]: {} } } : {}),
|
|
860
|
+
},
|
|
861
|
+
traits: docTraits,
|
|
862
|
+
},
|
|
767
863
|
reservedId !== undefined ? hint : `${hint}List`,
|
|
768
864
|
),
|
|
769
865
|
nullable,
|
|
@@ -800,7 +896,10 @@ const convertSchema = (
|
|
|
800
896
|
{
|
|
801
897
|
type: "map",
|
|
802
898
|
key: { target: PRELUDE.String },
|
|
803
|
-
value: {
|
|
899
|
+
value: {
|
|
900
|
+
target: value.target,
|
|
901
|
+
...(value.nullable ? { traits: { [NULLABLE_TRAIT]: {} } } : {}),
|
|
902
|
+
},
|
|
804
903
|
traits: docTraits,
|
|
805
904
|
},
|
|
806
905
|
reservedId !== undefined ? hint : `${hint}Map`,
|
|
@@ -937,7 +1036,11 @@ const collectParams = (ctx: Ctx, pathItem: any, op: any): Param[] => {
|
|
|
937
1036
|
byKey.set(`${p.in}${p.name}`, p);
|
|
938
1037
|
}
|
|
939
1038
|
return [...byKey.values()].filter(
|
|
940
|
-
(p) =>
|
|
1039
|
+
(p) =>
|
|
1040
|
+
p.in === "path" ||
|
|
1041
|
+
p.in === "query" ||
|
|
1042
|
+
p.in === "body" ||
|
|
1043
|
+
p.in === "header",
|
|
941
1044
|
);
|
|
942
1045
|
};
|
|
943
1046
|
|
|
@@ -1001,12 +1104,13 @@ const opDoc = (op: any): string | undefined => {
|
|
|
1001
1104
|
// Responses
|
|
1002
1105
|
// ============================================================================
|
|
1003
1106
|
|
|
1004
|
-
/**
|
|
1107
|
+
/** First declared status in `order` wins; response-level `$ref` resolved; JSON only. */
|
|
1005
1108
|
const successSchema = (
|
|
1006
1109
|
ctx: Ctx,
|
|
1007
1110
|
responses: any,
|
|
1111
|
+
order: readonly string[],
|
|
1008
1112
|
): { schema: any | undefined } => {
|
|
1009
|
-
for (const code of
|
|
1113
|
+
for (const code of order) {
|
|
1010
1114
|
const raw = responses?.[code];
|
|
1011
1115
|
if (!raw) continue;
|
|
1012
1116
|
const resp = raw.$ref ? resolvePointer(ctx.spec, raw.$ref) : raw;
|
|
@@ -1019,6 +1123,37 @@ const successSchema = (
|
|
|
1019
1123
|
return { schema: undefined };
|
|
1020
1124
|
};
|
|
1021
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
|
+
|
|
1022
1157
|
// ============================================================================
|
|
1023
1158
|
// Pagination detection (v0 detectPagination)
|
|
1024
1159
|
// ============================================================================
|
|
@@ -1099,6 +1234,14 @@ const detectPagination = (
|
|
|
1099
1234
|
|
|
1100
1235
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
|
|
1101
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
|
+
|
|
1102
1245
|
const DEFAULT_STATUS_TO_ERROR_CLASS: Readonly<Record<string, string>> = {
|
|
1103
1246
|
"400": "BadRequest",
|
|
1104
1247
|
"403": "Forbidden",
|
|
@@ -1109,6 +1252,8 @@ const DEFAULT_STATUS_TO_ERROR_CLASS: Readonly<Record<string, string>> = {
|
|
|
1109
1252
|
|
|
1110
1253
|
const DEFAULT_ERROR_STATUSES = ["401", "429", "500", "503"];
|
|
1111
1254
|
|
|
1255
|
+
const DEFAULT_SUCCESS_STATUSES = ["200", "201", "204"];
|
|
1256
|
+
|
|
1112
1257
|
export const convertOpenApiToSmithy = (
|
|
1113
1258
|
spec: unknown,
|
|
1114
1259
|
options: OpenApiConvertOptions,
|
|
@@ -1131,6 +1276,13 @@ export const convertOpenApiToSmithy = (
|
|
|
1131
1276
|
options.defaultErrorStatuses ?? DEFAULT_ERROR_STATUSES,
|
|
1132
1277
|
);
|
|
1133
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
|
+
];
|
|
1134
1286
|
|
|
1135
1287
|
// Error class names and the service name are reserved up front so schema
|
|
1136
1288
|
// components can never steal them.
|
|
@@ -1145,16 +1297,65 @@ export const convertOpenApiToSmithy = (
|
|
|
1145
1297
|
|
|
1146
1298
|
for (const [rawPath, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
1147
1299
|
if (!pathItem || typeof pathItem !== "object") continue;
|
|
1148
|
-
for (const method of
|
|
1300
|
+
for (const method of httpMethods) {
|
|
1149
1301
|
const op = (pathItem as any)[method];
|
|
1150
1302
|
if (!op || typeof op !== "object") continue;
|
|
1151
1303
|
if (skipDeprecated && op.deprecated === true) continue;
|
|
1152
1304
|
|
|
1153
|
-
const
|
|
1305
|
+
const rawOperationId =
|
|
1154
1306
|
typeof op.operationId === "string" && op.operationId
|
|
1155
1307
|
? op.operationId
|
|
1156
|
-
: `${method}_${rawPath}
|
|
1157
|
-
|
|
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);
|
|
1158
1359
|
|
|
1159
1360
|
const params = collectParams(ctx, pathItem, op);
|
|
1160
1361
|
|
|
@@ -1212,6 +1413,32 @@ export const convertOpenApiToSmithy = (
|
|
|
1212
1413
|
});
|
|
1213
1414
|
}
|
|
1214
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
|
+
|
|
1215
1442
|
// ---- Request body (json > form-urlencoded > multipart) ----
|
|
1216
1443
|
let contentType: "form-urlencoded" | "multipart" | undefined;
|
|
1217
1444
|
let bodySchema: any;
|
|
@@ -1289,7 +1516,19 @@ export const convertOpenApiToSmithy = (
|
|
|
1289
1516
|
: PRELUDE.Unit;
|
|
1290
1517
|
|
|
1291
1518
|
// ---- Output shape ----
|
|
1292
|
-
|
|
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);
|
|
1293
1532
|
let outputTarget: string = PRELUDE.Unit;
|
|
1294
1533
|
if (respSchema !== undefined) {
|
|
1295
1534
|
const flat = flattenObject(ctx, respSchema);
|