@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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation-level smithy helpers for the SDK generators (dev-time only).
|
|
3
|
+
*/
|
|
4
|
+
import { local } from "./naming.ts";
|
|
5
|
+
import type { ShapeMap } from "./graph.ts";
|
|
6
|
+
|
|
7
|
+
export interface OpEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
def: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** All operation shapes, sorted by local name. */
|
|
13
|
+
export const collectOperations = (shapes: ShapeMap): OpEntry[] => {
|
|
14
|
+
const operations: OpEntry[] = [];
|
|
15
|
+
for (const [id, def] of Object.entries(shapes)) {
|
|
16
|
+
if (def.type === "operation") operations.push({ id, def });
|
|
17
|
+
}
|
|
18
|
+
operations.sort((a, b) => local(a.id).localeCompare(local(b.id)));
|
|
19
|
+
return operations;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The model's shape namespace (from the first operation, else any shape). */
|
|
23
|
+
export const modelNamespace = (
|
|
24
|
+
operations: readonly OpEntry[],
|
|
25
|
+
shapes: ShapeMap,
|
|
26
|
+
fallback: string,
|
|
27
|
+
): string =>
|
|
28
|
+
operations.length
|
|
29
|
+
? operations[0].id.split("#")[0]
|
|
30
|
+
: (Object.keys(shapes)[0]?.split("#")[0] ?? fallback);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an operation's input/output targets, synthesizing an empty named
|
|
34
|
+
* structure for `smithy.api#Unit` so every operation has a shape that can
|
|
35
|
+
* carry operation-level traits (mutates `shapes`).
|
|
36
|
+
*/
|
|
37
|
+
export const ensureNamedIo = (
|
|
38
|
+
shapes: ShapeMap,
|
|
39
|
+
op: OpEntry,
|
|
40
|
+
ns: string,
|
|
41
|
+
): { input: string; output: string } => {
|
|
42
|
+
const opName = local(op.id);
|
|
43
|
+
let input = op.def.input?.target ?? "smithy.api#Unit";
|
|
44
|
+
let output = op.def.output?.target ?? "smithy.api#Unit";
|
|
45
|
+
if (input === "smithy.api#Unit") {
|
|
46
|
+
input = `${ns}#${opName}Request`;
|
|
47
|
+
shapes[input] = { type: "structure", members: {} };
|
|
48
|
+
}
|
|
49
|
+
if (output === "smithy.api#Unit") {
|
|
50
|
+
output = `${ns}#${opName}Response`;
|
|
51
|
+
shapes[output] = { type: "structure", members: {} };
|
|
52
|
+
}
|
|
53
|
+
return { input, output };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every error shape id referenced by the operations' `errors` lists —
|
|
58
|
+
* deduplicated, existing in the model, sorted by local name.
|
|
59
|
+
*/
|
|
60
|
+
export const collectOpErrorIds = (
|
|
61
|
+
operations: readonly OpEntry[],
|
|
62
|
+
shapes: ShapeMap,
|
|
63
|
+
): string[] => {
|
|
64
|
+
const errorIds: string[] = [];
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
for (const op of operations) {
|
|
67
|
+
for (const e of op.def.errors ?? []) {
|
|
68
|
+
if (!seen.has(e.target) && shapes[e.target]) {
|
|
69
|
+
seen.add(e.target);
|
|
70
|
+
errorIds.push(e.target);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
errorIds.sort((a, b) => local(a).localeCompare(local(b)));
|
|
75
|
+
return errorIds;
|
|
76
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pagination-trait handling for the SDK generators (dev-time only).
|
|
3
|
+
*
|
|
4
|
+
* Both SDKs source `smithy.api#paginated`; AWS merges operation-level
|
|
5
|
+
* config over service-level defaults, cloudflare validates the trait
|
|
6
|
+
* against the operation's actual members (degrading to a plain op when it
|
|
7
|
+
* can't work). Both behaviors live here.
|
|
8
|
+
*/
|
|
9
|
+
import type { PaginatedTrait } from "../pagination.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merge an operation-level paginated trait over service-level defaults
|
|
13
|
+
* (Smithy semantics: operations may specify partial pagination and inherit
|
|
14
|
+
* the rest from the service).
|
|
15
|
+
*/
|
|
16
|
+
export const mergePaginated = (
|
|
17
|
+
op: Partial<PaginatedTrait> | undefined,
|
|
18
|
+
service: Partial<PaginatedTrait> | undefined,
|
|
19
|
+
): PaginatedTrait | undefined => {
|
|
20
|
+
if (!op) return undefined;
|
|
21
|
+
const merged = {
|
|
22
|
+
mode: op.mode ?? service?.mode,
|
|
23
|
+
inputToken: op.inputToken ?? service?.inputToken,
|
|
24
|
+
outputToken: op.outputToken ?? service?.outputToken,
|
|
25
|
+
items: op.items ?? service?.items,
|
|
26
|
+
pageSize: op.pageSize ?? service?.pageSize,
|
|
27
|
+
};
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(merged).filter(([, v]) => v !== undefined),
|
|
30
|
+
) as PaginatedTrait;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface ValidatePaginatedOptions {
|
|
34
|
+
readonly trait: Partial<PaginatedTrait>;
|
|
35
|
+
/** TS-facing member names of the operation input shape. */
|
|
36
|
+
readonly inputNames: ReadonlySet<string>;
|
|
37
|
+
/** TS-facing member names of the operation output shape. */
|
|
38
|
+
readonly outputNames: ReadonlySet<string>;
|
|
39
|
+
/** Items path fallback when the trait omits `items` (e.g. `"result"`). */
|
|
40
|
+
readonly itemsFallback?: string;
|
|
41
|
+
/** Output names accepted even when not modeled (e.g. `"resultInfo"`). */
|
|
42
|
+
readonly syntheticOutputs?: ReadonlySet<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ValidatedPagination {
|
|
46
|
+
readonly ok: boolean;
|
|
47
|
+
/** Root member of the items path (`"result.items"` → `"result"`). */
|
|
48
|
+
readonly itemsRoot: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A paginated op must actually carry its page/cursor token on the input
|
|
53
|
+
* and its items member on the output — otherwise `.pages()` would loop or
|
|
54
|
+
* yield nothing. Returns the items-path root for emitters that promote it
|
|
55
|
+
* to required.
|
|
56
|
+
*/
|
|
57
|
+
export const validatePaginated = (
|
|
58
|
+
o: ValidatePaginatedOptions,
|
|
59
|
+
): ValidatedPagination => {
|
|
60
|
+
const itemsRoot = String(o.trait.items ?? o.itemsFallback ?? "").split(
|
|
61
|
+
".",
|
|
62
|
+
)[0];
|
|
63
|
+
const tokenOk =
|
|
64
|
+
o.trait.mode === "single" ||
|
|
65
|
+
(typeof o.trait.inputToken === "string" &&
|
|
66
|
+
o.inputNames.has(o.trait.inputToken));
|
|
67
|
+
const itemsOk =
|
|
68
|
+
o.outputNames.has(itemsRoot) ||
|
|
69
|
+
(o.syntheticOutputs?.has(itemsRoot) ?? false);
|
|
70
|
+
return { ok: tokenOk && itemsOk, itemsRoot };
|
|
71
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smithy prelude scalar baselines + reference resolvers for the SDK
|
|
3
|
+
* generators (dev-time only).
|
|
4
|
+
*
|
|
5
|
+
* The JSON flavor maps timestamps/blobs to plain strings — the baseline for
|
|
6
|
+
* SDKs whose wire format is JSON-over-docs (cloudflare). Generators with
|
|
7
|
+
* richer models (AWS) emit real timestamp/blob schemas themselves and only
|
|
8
|
+
* fall back here for untyped scalars.
|
|
9
|
+
*/
|
|
10
|
+
import { isPrelude, local } from "./naming.ts";
|
|
11
|
+
import { isForwardRef } from "./graph.ts";
|
|
12
|
+
|
|
13
|
+
/** Prelude shape → schema expression (JSON-wire flavor). */
|
|
14
|
+
export const JSON_PRELUDE: Record<string, string> = {
|
|
15
|
+
String: "S.String",
|
|
16
|
+
Boolean: "S.Boolean",
|
|
17
|
+
Double: "S.Number",
|
|
18
|
+
Float: "S.Number",
|
|
19
|
+
Integer: "S.Number",
|
|
20
|
+
Long: "S.Number",
|
|
21
|
+
BigInteger: "S.Number",
|
|
22
|
+
BigDecimal: "S.Number",
|
|
23
|
+
Timestamp: "S.String",
|
|
24
|
+
Blob: "S.String",
|
|
25
|
+
Document: "S.Unknown",
|
|
26
|
+
Unit: "S.Struct({})",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** TypeScript type for each prelude shape, mirroring {@link JSON_PRELUDE}. */
|
|
30
|
+
export const TS_JSON_PRELUDE: Record<string, string> = {
|
|
31
|
+
String: "string",
|
|
32
|
+
Boolean: "boolean",
|
|
33
|
+
Double: "number",
|
|
34
|
+
Float: "number",
|
|
35
|
+
Integer: "number",
|
|
36
|
+
Long: "number",
|
|
37
|
+
BigInteger: "number",
|
|
38
|
+
BigDecimal: "number",
|
|
39
|
+
Timestamp: "string",
|
|
40
|
+
Blob: "string",
|
|
41
|
+
Document: "unknown",
|
|
42
|
+
Unit: "{}",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Schema reference resolver: prelude shapes map through the prelude table,
|
|
47
|
+
* forward references (including into cycles) wrap in `S.suspend`.
|
|
48
|
+
*/
|
|
49
|
+
export const makeSchemaRef =
|
|
50
|
+
(
|
|
51
|
+
prelude: Record<string, string>,
|
|
52
|
+
indexOf: ReadonlyMap<string, number>,
|
|
53
|
+
): ((target: string, selfIdx: number) => string) =>
|
|
54
|
+
(target, selfIdx) => {
|
|
55
|
+
if (isPrelude(target)) return prelude[local(target)] ?? "S.Unknown";
|
|
56
|
+
const name = local(target);
|
|
57
|
+
if (isForwardRef(indexOf, target, selfIdx)) {
|
|
58
|
+
return `S.suspend(() => ${name})`;
|
|
59
|
+
}
|
|
60
|
+
return name;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* TypeScript type reference resolver. Named shapes all get an emitted
|
|
65
|
+
* interface / type alias, so forward references are always fine.
|
|
66
|
+
*/
|
|
67
|
+
export const makeTsRef =
|
|
68
|
+
(tsPrelude: Record<string, string>): ((target: string) => string) =>
|
|
69
|
+
(target) =>
|
|
70
|
+
isPrelude(target) ? (tsPrelude[local(target)] ?? "unknown") : local(target);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Effect, Predicate } from "effect";
|
|
2
|
+
|
|
3
|
+
export const categoriesKey = "@distilled/meta/error-categories";
|
|
4
|
+
|
|
5
|
+
export const withCategory =
|
|
6
|
+
<Categories extends Array<PropertyKey>>(...categories: Categories) =>
|
|
7
|
+
<Args extends Array<any>, Ret, C extends { new (...args: Args): Ret }>(
|
|
8
|
+
C: C,
|
|
9
|
+
): C & {
|
|
10
|
+
new (...args: Args): Ret & {
|
|
11
|
+
[categoriesKey]: { [Cat in Categories[number]]: true };
|
|
12
|
+
};
|
|
13
|
+
} => {
|
|
14
|
+
// @ts-expect-error
|
|
15
|
+
const Mixed = class extends C {};
|
|
16
|
+
|
|
17
|
+
for (const category of categories) {
|
|
18
|
+
if (!(categoriesKey in Mixed.prototype)) {
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
Mixed.prototype[categoriesKey] = {};
|
|
21
|
+
}
|
|
22
|
+
// @ts-expect-error
|
|
23
|
+
Mixed.prototype[categoriesKey][category] = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Mixed as any;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AllKeys<E> = E extends { [categoriesKey]: infer Q }
|
|
30
|
+
? keyof Q
|
|
31
|
+
: never;
|
|
32
|
+
|
|
33
|
+
export type ExtractAll<E, Cats extends PropertyKey> = Cats extends any
|
|
34
|
+
? Extract<E, { [categoriesKey]: { [K in Cats]: any } }>
|
|
35
|
+
: never;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runtime check: does `value` carry the given category?
|
|
39
|
+
* Works on instances tagged via `withCategory(...)`.
|
|
40
|
+
*/
|
|
41
|
+
export const hasCategory =
|
|
42
|
+
<const Cat extends PropertyKey>(category: Cat) =>
|
|
43
|
+
(
|
|
44
|
+
value: unknown,
|
|
45
|
+
): value is { readonly [categoriesKey]: { readonly [K in Cat]: true } } => {
|
|
46
|
+
if (!Predicate.isObject(value)) return false;
|
|
47
|
+
if (!Predicate.hasProperty(categoriesKey)(value)) return false;
|
|
48
|
+
return category in (value as any)[categoriesKey];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** True if `value` has any of the given categories. */
|
|
52
|
+
export const hasAnyCategory =
|
|
53
|
+
(...categories: ReadonlyArray<PropertyKey>) =>
|
|
54
|
+
(value: unknown): boolean => {
|
|
55
|
+
if (!Predicate.isObject(value)) return false;
|
|
56
|
+
if (!Predicate.hasProperty(categoriesKey)(value)) return false;
|
|
57
|
+
const cats = (value as any)[categoriesKey];
|
|
58
|
+
for (const c of categories) if (c in cats) return true;
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const catchCategory =
|
|
63
|
+
<E, const Categories extends Array<AllKeys<E>>, A2, E2, R2>(
|
|
64
|
+
...args: [
|
|
65
|
+
...Categories,
|
|
66
|
+
f: (err: ExtractAll<E, Categories[number]>) => Effect.Effect<A2, E2, R2>,
|
|
67
|
+
]
|
|
68
|
+
) =>
|
|
69
|
+
<A, R>(
|
|
70
|
+
effect: Effect.Effect<A, E, R>,
|
|
71
|
+
): Effect.Effect<
|
|
72
|
+
A | A2,
|
|
73
|
+
E2 | Exclude<E, ExtractAll<E, Categories[number]>>,
|
|
74
|
+
R | R2
|
|
75
|
+
> => {
|
|
76
|
+
const f = args.pop()! as (
|
|
77
|
+
err: ExtractAll<E, Categories[number]>,
|
|
78
|
+
) => Effect.Effect<A2, E2, R2>;
|
|
79
|
+
const categories = args as Array<PropertyKey>;
|
|
80
|
+
const matches = hasAnyCategory(...categories);
|
|
81
|
+
return Effect.catchIf(effect, matches, (e) =>
|
|
82
|
+
f(e as ExtractAll<E, Categories[number]>),
|
|
83
|
+
) as any;
|
|
84
|
+
};
|
package/src/errors.ts
CHANGED
package/src/json-patch.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON Patch (RFC 6902) Implementation
|
|
3
3
|
*
|
|
4
|
-
* Provides a unified spec patching system for all SDKs.
|
|
5
|
-
*
|
|
6
|
-
* to add error
|
|
4
|
+
* Provides a unified spec patching system for all SDKs. Patches are applied to
|
|
5
|
+
* generated intermediary specs (Smithy models, OpenAPI docs, …) before code
|
|
6
|
+
* generation to add typed error shapes, rename operations, mark nullable
|
|
7
|
+
* fields, etc. Ported from the distilled repo's `core/json-patch`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* import { applyAllPatches } from "@distilled.cloud/core/json-patch";
|
|
11
|
-
*
|
|
12
|
-
* const spec = JSON.parse(fs.readFileSync("openapi.json", "utf-8"));
|
|
13
|
-
* const { applied, errors } = applyAllPatches(spec, "./patches");
|
|
14
|
-
* ```
|
|
9
|
+
* Pure functions only — callers load patch files themselves (the generators
|
|
10
|
+
* use Effect's FileSystem) and hand the parsed operations to `applyPatch`.
|
|
15
11
|
*/
|
|
16
|
-
import * as fs from "fs";
|
|
17
|
-
import * as path from "path";
|
|
18
12
|
|
|
19
13
|
// ============================================================================
|
|
20
14
|
// Types
|
|
@@ -30,7 +24,7 @@ export interface JsonPatchOperation {
|
|
|
30
24
|
export type JsonPatch = JsonPatchOperation[];
|
|
31
25
|
|
|
32
26
|
export interface PatchFile {
|
|
33
|
-
description
|
|
27
|
+
description?: string;
|
|
34
28
|
patches: JsonPatch;
|
|
35
29
|
}
|
|
36
30
|
|
|
@@ -38,9 +32,7 @@ export interface PatchFile {
|
|
|
38
32
|
// JSON Pointer (RFC 6901)
|
|
39
33
|
// ============================================================================
|
|
40
34
|
|
|
41
|
-
/**
|
|
42
|
-
* Parse a JSON Pointer (RFC 6901) path into segments.
|
|
43
|
-
*/
|
|
35
|
+
/** Parse a JSON Pointer (RFC 6901) path into segments. */
|
|
44
36
|
export function parseJsonPointer(pointer: string): string[] {
|
|
45
37
|
if (pointer === "") return [];
|
|
46
38
|
if (!pointer.startsWith("/")) {
|
|
@@ -52,9 +44,7 @@ export function parseJsonPointer(pointer: string): string[] {
|
|
|
52
44
|
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
/**
|
|
56
|
-
* Get a value at a JSON Pointer path.
|
|
57
|
-
*/
|
|
47
|
+
/** Get a value at a JSON Pointer path. */
|
|
58
48
|
export function getValueAtPath(obj: unknown, pointer: string): unknown {
|
|
59
49
|
const segments = parseJsonPointer(pointer);
|
|
60
50
|
let current: unknown = obj;
|
|
@@ -74,9 +64,7 @@ export function getValueAtPath(obj: unknown, pointer: string): unknown {
|
|
|
74
64
|
return current;
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
/**
|
|
78
|
-
* Set a value at a JSON Pointer path.
|
|
79
|
-
*/
|
|
67
|
+
/** Set a value at a JSON Pointer path. */
|
|
80
68
|
export function setValueAtPath(
|
|
81
69
|
obj: unknown,
|
|
82
70
|
pointer: string,
|
|
@@ -95,8 +83,7 @@ export function setValueAtPath(
|
|
|
95
83
|
throw new Error(`Cannot traverse path ${pointer}: not an object`);
|
|
96
84
|
}
|
|
97
85
|
if (Array.isArray(current)) {
|
|
98
|
-
|
|
99
|
-
current = current[index];
|
|
86
|
+
current = current[parseInt(segment, 10)];
|
|
100
87
|
} else {
|
|
101
88
|
current = (current as Record<string, unknown>)[segment];
|
|
102
89
|
}
|
|
@@ -110,21 +97,17 @@ export function setValueAtPath(
|
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
if (Array.isArray(current)) {
|
|
113
|
-
const index =
|
|
114
|
-
lastSegment === "-" ? current.length : parseInt(lastSegment, 10);
|
|
115
100
|
if (lastSegment === "-") {
|
|
116
101
|
current.push(value);
|
|
117
102
|
} else {
|
|
118
|
-
current[
|
|
103
|
+
current[parseInt(lastSegment, 10)] = value;
|
|
119
104
|
}
|
|
120
105
|
} else {
|
|
121
106
|
(current as Record<string, unknown>)[lastSegment] = value;
|
|
122
107
|
}
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
/**
|
|
126
|
-
* Remove a value at a JSON Pointer path.
|
|
127
|
-
*/
|
|
110
|
+
/** Remove a value at a JSON Pointer path. */
|
|
128
111
|
export function removeValueAtPath(obj: unknown, pointer: string): void {
|
|
129
112
|
const segments = parseJsonPointer(pointer);
|
|
130
113
|
if (segments.length === 0) {
|
|
@@ -163,9 +146,7 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
|
|
|
163
146
|
// Patch Operations
|
|
164
147
|
// ============================================================================
|
|
165
148
|
|
|
166
|
-
/**
|
|
167
|
-
* Apply a single JSON Patch operation.
|
|
168
|
-
*/
|
|
149
|
+
/** Apply a single JSON Patch operation (mutates `obj` in place). */
|
|
169
150
|
export function applyOperation(
|
|
170
151
|
obj: unknown,
|
|
171
152
|
operation: JsonPatchOperation,
|
|
@@ -185,6 +166,11 @@ export function applyOperation(
|
|
|
185
166
|
case "move": {
|
|
186
167
|
if (!operation.from) throw new Error("move operation requires 'from'");
|
|
187
168
|
const moveValue = getValueAtPath(obj, operation.from);
|
|
169
|
+
if (moveValue === undefined) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Cannot move from path ${operation.from}: not an object`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
188
174
|
removeValueAtPath(obj, operation.from);
|
|
189
175
|
setValueAtPath(obj, operation.path, moveValue);
|
|
190
176
|
break;
|
|
@@ -213,9 +199,7 @@ export function applyOperation(
|
|
|
213
199
|
}
|
|
214
200
|
}
|
|
215
201
|
|
|
216
|
-
/**
|
|
217
|
-
* Apply a JSON Patch to an object (mutates in place).
|
|
218
|
-
*/
|
|
202
|
+
/** Apply a JSON Patch to an object (mutates in place). */
|
|
219
203
|
export function applyPatch(obj: unknown, patch: JsonPatch): void {
|
|
220
204
|
for (const operation of patch) {
|
|
221
205
|
applyOperation(obj, operation);
|
|
@@ -224,83 +208,15 @@ export function applyPatch(obj: unknown, patch: JsonPatch): void {
|
|
|
224
208
|
|
|
225
209
|
/**
|
|
226
210
|
* Whether a per-operation failure is caused by the target location being
|
|
227
|
-
* absent from the spec (i.e.
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* whole generate; a patch that only adds an error
|
|
231
|
-
*
|
|
211
|
+
* absent from the spec (i.e. spec drift — the operation/shape the patch
|
|
212
|
+
* targets was renamed or removed upstream) as opposed to a malformed patch.
|
|
213
|
+
* Stale targets should be skipped with a warning rather than aborting the
|
|
214
|
+
* whole generate; a patch that only adds an error to an operation that no
|
|
215
|
+
* longer exists is harmless to drop.
|
|
232
216
|
*/
|
|
233
|
-
function isStaleTargetError(message: string): boolean {
|
|
217
|
+
export function isStaleTargetError(message: string): boolean {
|
|
234
218
|
return (
|
|
235
219
|
message.includes("not an object") ||
|
|
236
220
|
message.includes("parent is not an object")
|
|
237
221
|
);
|
|
238
222
|
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Load and apply all patches from a directory.
|
|
242
|
-
* Finds all *.patch.json files and applies them.
|
|
243
|
-
*
|
|
244
|
-
* Each operation within a patch file is applied independently. Operations
|
|
245
|
-
* whose target path no longer exists in the spec (vendor spec drift) are
|
|
246
|
-
* collected as `skipped` warnings and do not abort generation. Only genuine
|
|
247
|
-
* failures (malformed JSON, invalid pointers, failed `test` ops) are reported
|
|
248
|
-
* as `errors`.
|
|
249
|
-
*/
|
|
250
|
-
export function applyAllPatches(
|
|
251
|
-
spec: unknown,
|
|
252
|
-
patchDir: string,
|
|
253
|
-
): { applied: string[]; skipped: string[]; errors: string[] } {
|
|
254
|
-
const applied: string[] = [];
|
|
255
|
-
const skipped: string[] = [];
|
|
256
|
-
const errors: string[] = [];
|
|
257
|
-
|
|
258
|
-
if (!fs.existsSync(patchDir)) {
|
|
259
|
-
return { applied, skipped, errors };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Find all .patch.json files
|
|
263
|
-
const files = fs
|
|
264
|
-
.readdirSync(patchDir)
|
|
265
|
-
.filter((f) => f.endsWith(".patch.json"))
|
|
266
|
-
.sort(); // Sort for deterministic application order
|
|
267
|
-
|
|
268
|
-
for (const file of files) {
|
|
269
|
-
const filePath = path.join(patchDir, file);
|
|
270
|
-
let patchFile: PatchFile;
|
|
271
|
-
try {
|
|
272
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
273
|
-
patchFile = JSON.parse(content);
|
|
274
|
-
} catch (error) {
|
|
275
|
-
errors.push(
|
|
276
|
-
`${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
277
|
-
);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let appliedAny = false;
|
|
282
|
-
for (const operation of patchFile.patches) {
|
|
283
|
-
try {
|
|
284
|
-
applyOperation(spec, operation);
|
|
285
|
-
appliedAny = true;
|
|
286
|
-
} catch (error) {
|
|
287
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
-
if (isStaleTargetError(message)) {
|
|
289
|
-
skipped.push(
|
|
290
|
-
`${file} [${operation.op} ${operation.path}]: ${message}`,
|
|
291
|
-
);
|
|
292
|
-
} else {
|
|
293
|
-
errors.push(
|
|
294
|
-
`${file} [${operation.op} ${operation.path}]: ${message}`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (appliedAny) {
|
|
301
|
-
applied.push(`${file}: ${patchFile.description}`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { applied, skipped, errors };
|
|
306
|
-
}
|