@distilled.cloud/core 1.0.0-rc.7 → 1.0.0-rc.9
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/codegen/cli.d.ts +8 -6
- package/lib/codegen/cli.d.ts.map +1 -1
- package/lib/codegen/cli.js +6 -62
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/graphql.d.ts +6 -0
- package/lib/codegen/graphql.d.ts.map +1 -1
- package/lib/codegen/graphql.js +14 -4
- package/lib/codegen/graphql.js.map +1 -1
- 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 +27 -1
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +81 -3
- 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/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/package.json +4 -4
- package/src/codegen/cli.ts +14 -91
- package/src/codegen/graphql.ts +25 -3
- package/src/codegen/openapi-cli.ts +75 -59
- package/src/codegen/openapi.ts +118 -4
- 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/json-patch.ts +82 -25
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec path resolution — the seam between a production spec mirror and a
|
|
3
|
+
* local working copy of one (dev-time only).
|
|
4
|
+
*
|
|
5
|
+
* Every SDK package reads its specs from `packages/<pkg>/specs/`. In
|
|
6
|
+
* production that directory holds a submodule of the package's mirror
|
|
7
|
+
* repository, `distilled-mirror/spec-mirror-<pkg>`, whose layout is fixed:
|
|
8
|
+
*
|
|
9
|
+
* packages/<pkg>/specs/spec-mirror-<pkg>/
|
|
10
|
+
* ├── .meta/ fetch-specs.ts + its package.json
|
|
11
|
+
* └── specs/ what the generator reads
|
|
12
|
+
*
|
|
13
|
+
* A contributor adding a provider cannot create that repository — it lives in
|
|
14
|
+
* an org they have no write access to, and it is only created once the
|
|
15
|
+
* `distilled-submodules` stack deploys on merge to main. So `pnpm specs:local
|
|
16
|
+
* <pkg>` runs the same `fetch-specs.ts` into a gitignored directory shaped
|
|
17
|
+
* exactly like the mirror:
|
|
18
|
+
*
|
|
19
|
+
* packages/<pkg>/specs/.local/
|
|
20
|
+
* ├── .meta/
|
|
21
|
+
* └── specs/
|
|
22
|
+
*
|
|
23
|
+
* Setting {@link LOCAL_ENV} then re-roots every spec read into it. Nothing in
|
|
24
|
+
* the package's source changes — the `specPath` a `convert.ts` declares stays
|
|
25
|
+
* the production one — so there is no `.local` reference that could be
|
|
26
|
+
* committed by accident, and the switch is impossible to leave on: it lives
|
|
27
|
+
* in the environment of one command, never in a file.
|
|
28
|
+
*
|
|
29
|
+
* The opt-in is deliberate rather than a fallback-when-missing. A silent
|
|
30
|
+
* fallback would regenerate an SDK from a stale `.local` copy the moment a
|
|
31
|
+
* submodule failed to check out, and the result — committed, formatted,
|
|
32
|
+
* plausible — would be indistinguishable from a real regeneration.
|
|
33
|
+
*/
|
|
34
|
+
import * as fs from "node:fs";
|
|
35
|
+
import * as path from "node:path";
|
|
36
|
+
|
|
37
|
+
/** Set to a non-empty value to read specs from `specs/.local` instead. */
|
|
38
|
+
export const LOCAL_ENV = "DISTILLED_SPECS_LOCAL";
|
|
39
|
+
|
|
40
|
+
/** The local working copy of a mirror, relative to a package root. */
|
|
41
|
+
export const LOCAL_DIR = path.join("specs", ".local");
|
|
42
|
+
|
|
43
|
+
/** Repository name of the mirror feeding `packages/<pkg>`. */
|
|
44
|
+
export const mirrorRepositoryName = (pkg: string) => `spec-mirror-${pkg}`;
|
|
45
|
+
|
|
46
|
+
/** Whether this process reads specs from `specs/.local`. */
|
|
47
|
+
export const isLocalSpecs = (): boolean =>
|
|
48
|
+
(process.env[LOCAL_ENV] ?? "").trim() !== "";
|
|
49
|
+
|
|
50
|
+
let announced = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The path within a mirror's `specs/` directory that `specPath` names.
|
|
54
|
+
*
|
|
55
|
+
* Spec paths take one of two shapes — `specs/<mirror>/specs/<tail>` for a
|
|
56
|
+
* submodule and `specs/<tail>` for a committed file — and in both the tail is
|
|
57
|
+
* everything after the LAST `specs` segment. That one rule covers the
|
|
58
|
+
* submodule form, the committed form, the nested forms
|
|
59
|
+
* (`specs/<mirror>/specs/models/<service>/...`) and the directory form
|
|
60
|
+
* (`specs/<mirror>/specs`, whose tail is empty — the mirror's `specs/`
|
|
61
|
+
* directory itself) without needing to know which it was handed.
|
|
62
|
+
*
|
|
63
|
+
* `undefined` means there is no `specs` segment at all, which local mode
|
|
64
|
+
* cannot interpret.
|
|
65
|
+
*/
|
|
66
|
+
const tailWithinSpecs = (specPath: string): string | undefined => {
|
|
67
|
+
const segments = specPath.split(/[/\\]/).filter((s) => s !== "" && s !== ".");
|
|
68
|
+
const last = segments.lastIndexOf("specs");
|
|
69
|
+
if (last === -1) return undefined;
|
|
70
|
+
return segments.slice(last + 1).join(path.sep);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a package-relative spec path to an absolute one.
|
|
75
|
+
*
|
|
76
|
+
* @param root absolute package root (`path.resolve(import.meta.dir, "..")`)
|
|
77
|
+
* @param specPath the production path, relative to `root` (or absolute)
|
|
78
|
+
*/
|
|
79
|
+
export const resolveSpecPath = (root: string, specPath: string): string => {
|
|
80
|
+
const production = path.resolve(root, specPath);
|
|
81
|
+
if (!isLocalSpecs() || path.isAbsolute(specPath)) return production;
|
|
82
|
+
|
|
83
|
+
const tail = tailWithinSpecs(specPath);
|
|
84
|
+
if (tail === undefined) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${LOCAL_ENV} is set, but "${specPath}" has no \`specs/\` segment to ` +
|
|
87
|
+
`re-root. Local mode only understands paths under the package's ` +
|
|
88
|
+
`specs/ directory.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!announced) {
|
|
93
|
+
announced = true;
|
|
94
|
+
// stderr, not stdout: generate-all captures stdout per package and only
|
|
95
|
+
// prints it on failure, and this is exactly the line you need to see on
|
|
96
|
+
// a run that succeeded against the wrong specs.
|
|
97
|
+
console.error(
|
|
98
|
+
`⚠ ${LOCAL_ENV} — reading specs from ${LOCAL_DIR}, not the mirror submodule.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const local = path.join(root, LOCAL_DIR, "specs", tail);
|
|
103
|
+
if (!fs.existsSync(local)) {
|
|
104
|
+
// A missing local spec is a setup mistake, not a generator bug, and the
|
|
105
|
+
// ENOENT the caller would otherwise raise names a path nobody recognises.
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${local} does not exist. Materialise this package's mirror first:\n` +
|
|
108
|
+
` pnpm specs:local ${path.basename(root)}\n` +
|
|
109
|
+
`If that succeeds and this path is still missing, the package's ` +
|
|
110
|
+
`declared spec path (${specPath}) is not mirror-shaped yet — it must ` +
|
|
111
|
+
`read \`specs/<mirror>/specs/<file>\` for local mode to find it.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return local;
|
|
115
|
+
};
|
package/src/json-patch.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON Patch (RFC 6902) Implementation
|
|
3
3
|
*
|
|
4
|
-
* Provides a unified spec patching system for all SDKs. Patches
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Provides a unified spec patching system for all SDKs. Patches apply in
|
|
5
|
+
* `convert` so `.generated-specs` is the patched Smithy model. generate does
|
|
6
|
+
* not patch. Prefer `operationNaming: "verbNoun"` for operationId renames —
|
|
7
|
+
* JSON pointers at `/paths/~1foo/operationId` go stale when upstream prefixes
|
|
8
|
+
* paths.
|
|
8
9
|
*
|
|
9
10
|
* Pure functions only — callers load patch files themselves (the generators
|
|
10
11
|
* use Effect's FileSystem) and hand the parsed operations to `applyPatch`.
|
|
@@ -45,19 +46,44 @@ export function parseJsonPointer(pointer: string): string[] {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** Get a value at a JSON Pointer path. */
|
|
49
|
+
/**
|
|
50
|
+
* The target location of a patch operation is absent from the document —
|
|
51
|
+
* spec drift (renamed/removed upstream), not a malformed patch.
|
|
52
|
+
*/
|
|
53
|
+
export class StaleTargetError extends Error {
|
|
54
|
+
override readonly name = "StaleTargetError";
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export function getValueAtPath(obj: unknown, pointer: string): unknown {
|
|
49
58
|
const segments = parseJsonPointer(pointer);
|
|
50
59
|
let current: unknown = obj;
|
|
51
60
|
|
|
52
61
|
for (const segment of segments) {
|
|
53
|
-
if (
|
|
54
|
-
|
|
62
|
+
if (
|
|
63
|
+
current === null ||
|
|
64
|
+
current === undefined ||
|
|
65
|
+
typeof current !== "object"
|
|
66
|
+
) {
|
|
67
|
+
throw new StaleTargetError(
|
|
68
|
+
`JSON pointer ${pointer} missing (at '${segment}'): not an object`,
|
|
69
|
+
);
|
|
55
70
|
}
|
|
56
71
|
if (Array.isArray(current)) {
|
|
57
72
|
const index = segment === "-" ? current.length : parseInt(segment, 10);
|
|
73
|
+
if (index < 0 || index >= current.length) {
|
|
74
|
+
throw new StaleTargetError(
|
|
75
|
+
`JSON pointer ${pointer} missing index '${segment}'`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
58
78
|
current = current[index];
|
|
59
79
|
} else {
|
|
60
|
-
|
|
80
|
+
const record = current as Record<string, unknown>;
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
82
|
+
throw new StaleTargetError(
|
|
83
|
+
`JSON pointer ${pointer} missing key '${segment}'`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
current = record[segment];
|
|
61
87
|
}
|
|
62
88
|
}
|
|
63
89
|
|
|
@@ -80,7 +106,9 @@ export function setValueAtPath(
|
|
|
80
106
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
81
107
|
const segment = segments[i]!;
|
|
82
108
|
if (current === null || typeof current !== "object") {
|
|
83
|
-
throw new
|
|
109
|
+
throw new StaleTargetError(
|
|
110
|
+
`Cannot traverse path ${pointer}: not an object`,
|
|
111
|
+
);
|
|
84
112
|
}
|
|
85
113
|
if (Array.isArray(current)) {
|
|
86
114
|
current = current[parseInt(segment, 10)];
|
|
@@ -91,7 +119,7 @@ export function setValueAtPath(
|
|
|
91
119
|
|
|
92
120
|
const lastSegment = segments[segments.length - 1]!;
|
|
93
121
|
if (current === null || typeof current !== "object") {
|
|
94
|
-
throw new
|
|
122
|
+
throw new StaleTargetError(
|
|
95
123
|
`Cannot set value at path ${pointer}: parent is not an object`,
|
|
96
124
|
);
|
|
97
125
|
}
|
|
@@ -119,7 +147,9 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
|
|
|
119
147
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
120
148
|
const segment = segments[i]!;
|
|
121
149
|
if (current === null || typeof current !== "object") {
|
|
122
|
-
throw new
|
|
150
|
+
throw new StaleTargetError(
|
|
151
|
+
`Cannot traverse path ${pointer}: not an object`,
|
|
152
|
+
);
|
|
123
153
|
}
|
|
124
154
|
if (Array.isArray(current)) {
|
|
125
155
|
current = current[parseInt(segment, 10)];
|
|
@@ -130,15 +160,28 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
|
|
|
130
160
|
|
|
131
161
|
const lastSegment = segments[segments.length - 1]!;
|
|
132
162
|
if (current === null || typeof current !== "object") {
|
|
133
|
-
throw new
|
|
163
|
+
throw new StaleTargetError(
|
|
134
164
|
`Cannot remove at path ${pointer}: parent is not an object`,
|
|
135
165
|
);
|
|
136
166
|
}
|
|
137
167
|
|
|
168
|
+
// RFC 6902 §4.2: the target location MUST exist.
|
|
138
169
|
if (Array.isArray(current)) {
|
|
139
|
-
|
|
170
|
+
const index = parseInt(lastSegment, 10);
|
|
171
|
+
if (Number.isNaN(index) || index < 0 || index >= current.length) {
|
|
172
|
+
throw new StaleTargetError(
|
|
173
|
+
`JSON pointer ${pointer} missing index '${lastSegment}'`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
current.splice(index, 1);
|
|
140
177
|
} else {
|
|
141
|
-
|
|
178
|
+
const record = current as Record<string, unknown>;
|
|
179
|
+
if (!Object.prototype.hasOwnProperty.call(record, lastSegment)) {
|
|
180
|
+
throw new StaleTargetError(
|
|
181
|
+
`JSON pointer ${pointer} missing key '${lastSegment}'`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
delete record[lastSegment];
|
|
142
185
|
}
|
|
143
186
|
}
|
|
144
187
|
|
|
@@ -158,16 +201,21 @@ export function applyOperation(
|
|
|
158
201
|
case "remove":
|
|
159
202
|
removeValueAtPath(obj, operation.path);
|
|
160
203
|
break;
|
|
161
|
-
case "replace":
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
case "replace": {
|
|
205
|
+
const existing = getValueAtPath(obj, operation.path);
|
|
206
|
+
if (existing === undefined) {
|
|
207
|
+
throw new StaleTargetError(
|
|
208
|
+
`JSON pointer ${operation.path} does not exist`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
164
211
|
setValueAtPath(obj, operation.path, operation.value);
|
|
165
212
|
break;
|
|
213
|
+
}
|
|
166
214
|
case "move": {
|
|
167
215
|
if (!operation.from) throw new Error("move operation requires 'from'");
|
|
168
216
|
const moveValue = getValueAtPath(obj, operation.from);
|
|
169
217
|
if (moveValue === undefined) {
|
|
170
|
-
throw new
|
|
218
|
+
throw new StaleTargetError(
|
|
171
219
|
`Cannot move from path ${operation.from}: not an object`,
|
|
172
220
|
);
|
|
173
221
|
}
|
|
@@ -207,16 +255,25 @@ export function applyPatch(obj: unknown, patch: JsonPatch): void {
|
|
|
207
255
|
}
|
|
208
256
|
|
|
209
257
|
/**
|
|
210
|
-
* Whether a per-operation failure is
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* longer exists is harmless to drop.
|
|
258
|
+
* Whether a per-operation failure is a {@link StaleTargetError} (the
|
|
259
|
+
* operation/shape the patch targets was renamed or removed upstream) as
|
|
260
|
+
* opposed to a malformed patch. Accepts the thrown value or its message.
|
|
261
|
+
* Convert fails the run on these by default (`onStalePatch: "fail"`);
|
|
262
|
+
* `"warn"` restores skip-and-continue.
|
|
216
263
|
*/
|
|
217
|
-
export function isStaleTargetError(
|
|
264
|
+
export function isStaleTargetError(error: unknown): boolean {
|
|
265
|
+
if (error instanceof StaleTargetError) return true;
|
|
266
|
+
const message =
|
|
267
|
+
typeof error === "string"
|
|
268
|
+
? error
|
|
269
|
+
: error instanceof Error
|
|
270
|
+
? error.message
|
|
271
|
+
: "";
|
|
218
272
|
return (
|
|
219
273
|
message.includes("not an object") ||
|
|
220
|
-
message.includes("parent is not an object")
|
|
274
|
+
message.includes("parent is not an object") ||
|
|
275
|
+
message.includes("missing key") ||
|
|
276
|
+
message.includes("missing index") ||
|
|
277
|
+
message.includes("does not exist")
|
|
221
278
|
);
|
|
222
279
|
}
|