@distilled.cloud/core 1.0.0-rc.11 → 1.0.0-rc.13
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 +4 -4
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +1 -1
- package/lib/api.js.map +1 -1
- package/lib/category.js +1 -1
- package/lib/category.js.map +1 -1
- package/lib/codegen/boolean-string-enums.d.ts +8 -8
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -1
- package/lib/codegen/boolean-string-enums.js +84 -27
- package/lib/codegen/boolean-string-enums.js.map +1 -1
- package/lib/codegen/boolean-string-enums.test.js +130 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -1
- package/lib/codegen/cli.js +2 -2
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/generator.d.ts +20 -3
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +36 -9
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/generator.test.d.ts +2 -0
- package/lib/codegen/generator.test.d.ts.map +1 -0
- package/lib/codegen/generator.test.js +126 -0
- package/lib/codegen/generator.test.js.map +1 -0
- package/lib/codegen/graphql-client.d.ts +7 -1
- package/lib/codegen/graphql-client.d.ts.map +1 -1
- package/lib/codegen/graphql-client.js +262 -125
- package/lib/codegen/graphql-client.js.map +1 -1
- package/lib/codegen/graphql-client.test.js +104 -236
- package/lib/codegen/graphql-client.test.js.map +1 -1
- package/lib/codegen/openapi-binary.test.d.ts +2 -0
- package/lib/codegen/openapi-binary.test.d.ts.map +1 -0
- package/lib/codegen/openapi-binary.test.js +67 -0
- package/lib/codegen/openapi-binary.test.js.map +1 -0
- package/lib/codegen/openapi.d.ts +7 -1
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +15 -4
- package/lib/codegen/openapi.js.map +1 -1
- package/lib/codegen/patches.d.ts +16 -1
- package/lib/codegen/patches.d.ts.map +1 -1
- package/lib/codegen/patches.js +70 -4
- package/lib/codegen/patches.js.map +1 -1
- package/lib/codegen/patches.test.js +48 -2
- package/lib/codegen/patches.test.js.map +1 -1
- package/lib/codegen/untagged-unions.test.d.ts +2 -0
- package/lib/codegen/untagged-unions.test.d.ts.map +1 -0
- package/lib/codegen/untagged-unions.test.js +80 -0
- package/lib/codegen/untagged-unions.test.js.map +1 -0
- package/lib/errors.d.ts +2 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +5 -2
- package/lib/errors.js.map +1 -1
- package/lib/fixtures/xml/upstream-validator.json +397 -0
- package/lib/graphql.d.ts +296 -205
- package/lib/graphql.d.ts.map +1 -1
- package/lib/graphql.js +968 -631
- package/lib/graphql.js.map +1 -1
- package/lib/graphql.test.js +134 -763
- package/lib/graphql.test.js.map +1 -1
- package/lib/graphql.types.d.ts +10 -0
- package/lib/graphql.types.d.ts.map +1 -1
- package/lib/graphql.types.js +4 -44
- package/lib/graphql.types.js.map +1 -1
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +27 -0
- package/lib/index.js.map +1 -0
- package/lib/pagination.d.ts +2 -1
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +24 -16
- package/lib/pagination.js.map +1 -1
- package/lib/pagination.test.d.ts +2 -0
- package/lib/pagination.test.d.ts.map +1 -0
- package/lib/pagination.test.js +134 -0
- package/lib/pagination.test.js.map +1 -0
- package/lib/protocol-http.d.ts +12 -17
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +100 -45
- package/lib/protocol-http.js.map +1 -1
- package/lib/protocol-http.test.js +130 -0
- package/lib/protocol-http.test.js.map +1 -1
- package/lib/protocol-rest.d.ts +18 -1
- package/lib/protocol-rest.d.ts.map +1 -1
- package/lib/protocol-rest.js +24 -4
- package/lib/protocol-rest.js.map +1 -1
- package/lib/query.d.ts +29 -0
- package/lib/query.d.ts.map +1 -0
- package/lib/query.js +27 -0
- package/lib/query.js.map +1 -0
- package/lib/response-validation.d.ts +67 -0
- package/lib/response-validation.d.ts.map +1 -0
- package/lib/response-validation.js +76 -0
- package/lib/response-validation.js.map +1 -0
- package/lib/response-validation.test.d.ts +2 -0
- package/lib/response-validation.test.d.ts.map +1 -0
- package/lib/response-validation.test.js +67 -0
- package/lib/response-validation.test.js.map +1 -0
- package/lib/testing.d.ts +39 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +41 -0
- package/lib/testing.js.map +1 -0
- package/lib/trait.d.ts +29 -9
- package/lib/trait.d.ts.map +1 -1
- package/lib/trait.js +8 -0
- package/lib/trait.js.map +1 -1
- package/lib/xml.d.ts +44 -0
- package/lib/xml.d.ts.map +1 -0
- package/lib/xml.js +315 -0
- package/lib/xml.js.map +1 -0
- package/lib/xml.test.d.ts +2 -0
- package/lib/xml.test.d.ts.map +1 -0
- package/lib/xml.test.js +211 -0
- package/lib/xml.test.js.map +1 -0
- package/package.json +4 -4
- package/src/api.ts +4 -4
- package/src/category.ts +1 -1
- package/src/codegen/boolean-string-enums.test.ts +147 -0
- package/src/codegen/boolean-string-enums.ts +84 -27
- package/src/codegen/cli.ts +2 -2
- package/src/codegen/generator.test.ts +151 -0
- package/src/codegen/generator.ts +70 -12
- package/src/codegen/graphql-client.test.ts +116 -283
- package/src/codegen/graphql-client.ts +326 -169
- package/src/codegen/openapi-binary.test.ts +98 -0
- package/src/codegen/openapi.ts +26 -4
- package/src/codegen/patches.test.ts +66 -1
- package/src/codegen/patches.ts +84 -4
- package/src/codegen/untagged-unions.test.ts +89 -0
- package/src/errors.ts +5 -2
- package/src/fixtures/xml/upstream-validator.json +397 -0
- package/src/graphql.test.ts +174 -948
- package/src/graphql.ts +1592 -1167
- package/src/graphql.types.ts +92 -163
- package/src/index.ts +26 -0
- package/src/pagination.test.ts +191 -0
- package/src/pagination.ts +35 -25
- package/src/protocol-http.test.ts +163 -0
- package/src/protocol-http.ts +122 -46
- package/src/protocol-rest.ts +62 -13
- package/src/query.ts +39 -0
- package/src/response-validation.test.ts +95 -0
- package/src/response-validation.ts +116 -0
- package/src/testing.ts +78 -0
- package/src/trait.ts +39 -8
- package/src/xml.test.ts +269 -0
- package/src/xml.ts +339 -0
- package/lib/graphql.fixture.d.ts +0 -249
- package/lib/graphql.fixture.d.ts.map +0 -1
- package/lib/graphql.fixture.js +0 -240
- package/lib/graphql.fixture.js.map +0 -1
- package/src/graphql.fixture.ts +0 -371
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@distilled.cloud/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.13",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/alchemy-run/distilled",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@effect/platform-bun": "
|
|
31
|
+
"@effect/platform-bun": "^4.0.0",
|
|
32
32
|
"@types/bun": "1.3.14",
|
|
33
33
|
"@types/node": "26.2.0",
|
|
34
|
-
"effect": "
|
|
34
|
+
"effect": "^4.0.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"effect": "
|
|
37
|
+
"effect": "^4.0.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"graphql": "16.11.0"
|
package/src/api.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type * as Stream from "effect/Stream";
|
|
|
11
11
|
import { SingleShotGen } from "effect/Utils";
|
|
12
12
|
import * as Pagination from "./pagination.ts";
|
|
13
13
|
import { makeDefault, type Policy as RetryPolicy } from "./retry.ts";
|
|
14
|
-
import * as HttpClient from "effect/
|
|
15
|
-
import type * as HttpClientError from "effect/
|
|
16
|
-
import type * as HttpClientRequest from "effect/
|
|
17
|
-
import type * as HttpClientResponse from "effect/
|
|
14
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
15
|
+
import type * as HttpClientError from "effect/http/HttpClientError";
|
|
16
|
+
import type * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
17
|
+
import type * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
18
18
|
|
|
19
19
|
//#region Protocol
|
|
20
20
|
|
package/src/category.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import * as Effect from "effect/Effect";
|
|
22
22
|
import * as Predicate from "effect/Predicate";
|
|
23
|
-
import * as HttpClientError from "effect/
|
|
23
|
+
import * as HttpClientError from "effect/http/HttpClientError";
|
|
24
24
|
|
|
25
25
|
// ============================================================================
|
|
26
26
|
// Error Category Constants
|
|
@@ -123,6 +123,108 @@ describe("booleanStringEnums", () => {
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
describe("generateService", () => {
|
|
126
|
+
test("member schema overrides retain nullability, query bindings and protocol traits", () => {
|
|
127
|
+
const { code } = generateService(
|
|
128
|
+
model({
|
|
129
|
+
"ns#Example": {
|
|
130
|
+
type: "service",
|
|
131
|
+
version: "1",
|
|
132
|
+
operations: [{ target: "ns#GetThing" }],
|
|
133
|
+
},
|
|
134
|
+
"ns#GetThing": {
|
|
135
|
+
type: "operation",
|
|
136
|
+
input: { target: "ns#Input" },
|
|
137
|
+
output: { target: "smithy.api#Unit" },
|
|
138
|
+
traits: { "smithy.api#http": { method: "GET", uri: "/things" } },
|
|
139
|
+
},
|
|
140
|
+
"ns#Input": {
|
|
141
|
+
type: "structure",
|
|
142
|
+
members: {
|
|
143
|
+
secret: {
|
|
144
|
+
target: "smithy.api#String",
|
|
145
|
+
traits: {
|
|
146
|
+
"smithy.api#httpQuery": "secret",
|
|
147
|
+
"smithy.api#sensitive": {},
|
|
148
|
+
"ns#nullable": {},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
{
|
|
155
|
+
nullableTrait: "ns#nullable",
|
|
156
|
+
memberSchema: (member, ref) =>
|
|
157
|
+
`S.Union([${ref(member.target)}, S.Redacted(${ref(member.target)})])`,
|
|
158
|
+
memberTraitPipes: { "smithy.api#sensitive": "T.SensitiveValue" },
|
|
159
|
+
operationDecl: {
|
|
160
|
+
contextType: "ExampleOpContext",
|
|
161
|
+
commonErrorType: "ExampleOpError",
|
|
162
|
+
commonErrorClasses: [],
|
|
163
|
+
protocol: "ExampleProtocol",
|
|
164
|
+
retry: "Retry.Retry",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
expect(code).toContain(
|
|
169
|
+
'"secret": S.optional(S.NullOr(S.Union([S.String, S.Redacted(S.String)])).pipe(T.Query(), T.SensitiveValue({})))',
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("service-free schemas preserve Codec annotations across structures, collections and raw outputs", () => {
|
|
174
|
+
const input = model({
|
|
175
|
+
"ns#Example": {
|
|
176
|
+
type: "service",
|
|
177
|
+
version: "1",
|
|
178
|
+
operations: [{ target: "ns#GetThing" }],
|
|
179
|
+
},
|
|
180
|
+
"ns#GetThing": {
|
|
181
|
+
type: "operation",
|
|
182
|
+
input: { target: "ns#Input" },
|
|
183
|
+
output: { target: "ns#Output" },
|
|
184
|
+
traits: { "smithy.api#http": { method: "GET", uri: "/things" } },
|
|
185
|
+
},
|
|
186
|
+
"ns#Input": {
|
|
187
|
+
type: "structure",
|
|
188
|
+
members: { list: { target: "ns#List" }, map: { target: "ns#Map" } },
|
|
189
|
+
},
|
|
190
|
+
"ns#List": { type: "list", member: { target: "smithy.api#String" } },
|
|
191
|
+
"ns#Map": {
|
|
192
|
+
type: "map",
|
|
193
|
+
key: { target: "smithy.api#String" },
|
|
194
|
+
value: { target: "smithy.api#String" },
|
|
195
|
+
},
|
|
196
|
+
"ns#Output": {
|
|
197
|
+
type: "structure",
|
|
198
|
+
members: { body: { target: "ns#List", traits: { "ns#raw": {} } } },
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const spec = {
|
|
202
|
+
extraBindings: [
|
|
203
|
+
{
|
|
204
|
+
trait: "ns#raw",
|
|
205
|
+
binding: "rawResponse",
|
|
206
|
+
pipe: "T.RawResponse()",
|
|
207
|
+
rootPipe: "T.RawResponseRoot()",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
operationDecl: {
|
|
211
|
+
contextType: "ExampleOpContext",
|
|
212
|
+
commonErrorType: "ExampleOpError",
|
|
213
|
+
commonErrorClasses: [],
|
|
214
|
+
protocol: "ExampleProtocol",
|
|
215
|
+
retry: "Retry.Retry",
|
|
216
|
+
},
|
|
217
|
+
} as const;
|
|
218
|
+
const { code } = generateService(input, { ...spec, schemaType: "Codec" });
|
|
219
|
+
for (const name of ["Input", "List", "Map", "Output"]) {
|
|
220
|
+
expect(code).toContain(`as any as S.Codec<${name}>`);
|
|
221
|
+
}
|
|
222
|
+
expect(code).not.toContain("S.Schema<");
|
|
223
|
+
expect(generateService(input, spec).code).toContain(
|
|
224
|
+
"as any as S.Schema<Input>",
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
126
228
|
test("emits a boolean piped through T.StringEncoded()", () => {
|
|
127
229
|
const { code } = generateService(
|
|
128
230
|
model({
|
|
@@ -166,3 +268,48 @@ describe("generateService", () => {
|
|
|
166
268
|
expect(code).not.toContain('"true" | "false"');
|
|
167
269
|
});
|
|
168
270
|
});
|
|
271
|
+
|
|
272
|
+
describe("nested structures", () => {
|
|
273
|
+
const nested = (sharedWithResponse: boolean) =>
|
|
274
|
+
model({
|
|
275
|
+
"com.example.x#GetThing": {
|
|
276
|
+
type: "operation",
|
|
277
|
+
input: { target: "com.example.x#GetThingRequest" },
|
|
278
|
+
output: { target: "com.example.x#GetThingResponse" },
|
|
279
|
+
},
|
|
280
|
+
"com.example.x#GetThingRequest": {
|
|
281
|
+
type: "structure",
|
|
282
|
+
traits: { "smithy.api#input": {} },
|
|
283
|
+
members: { profile: { target: "com.example.x#Profile" } },
|
|
284
|
+
},
|
|
285
|
+
"com.example.x#GetThingResponse": {
|
|
286
|
+
type: "structure",
|
|
287
|
+
traits: { "smithy.api#output": {} },
|
|
288
|
+
members: sharedWithResponse
|
|
289
|
+
? { profile: { target: "com.example.x#Profile" } }
|
|
290
|
+
: {},
|
|
291
|
+
},
|
|
292
|
+
"com.example.x#Profile": {
|
|
293
|
+
type: "structure",
|
|
294
|
+
members: { dynamicMemoryEnabled: { target: "com.example.x#Flag" } },
|
|
295
|
+
},
|
|
296
|
+
"com.example.x#Flag": boolEnum,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("retargets a member of a structure only requests reach", () => {
|
|
300
|
+
const m = nested(false);
|
|
301
|
+
expect(booleanStringEnums(m)).toEqual({ members: 1, lists: 0 });
|
|
302
|
+
const member =
|
|
303
|
+
m.shapes["com.example.x#Profile"].members.dynamicMemoryEnabled;
|
|
304
|
+
expect(member.target).toBe("smithy.api#Boolean");
|
|
305
|
+
expect(member.traits[STRING_ENCODED_TRAIT]).toEqual({});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("leaves a structure a response also delivers", () => {
|
|
309
|
+
const m = nested(true);
|
|
310
|
+
expect(booleanStringEnums(m)).toEqual({ members: 0, lists: 0 });
|
|
311
|
+
expect(
|
|
312
|
+
m.shapes["com.example.x#Profile"].members.dynamicMemoryEnabled.target,
|
|
313
|
+
).toBe("com.example.x#Flag");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
* {@link generateService} runs this pass over the loaded model before
|
|
11
11
|
* emitting: REQUEST members of such an enum are retargeted to a real
|
|
12
12
|
* `smithy.api#Boolean` and stamped {@link STRING_ENCODED_TRAIT}, which the
|
|
13
|
-
* generator emits as `T.StringEncoded()` and the protocol
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* generator emits as `T.StringEncoded()` and the protocol honors by sending
|
|
14
|
+
* the value's string spelling. The TS surface becomes `boolean` with the
|
|
15
|
+
* wire unchanged. The model on disk keeps the string enum the description
|
|
16
|
+
* documents.
|
|
17
17
|
*
|
|
18
18
|
* Deliberately narrow:
|
|
19
19
|
*
|
|
20
|
-
* • request shapes
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* • request-only shapes — an operation input, or a structure or list the
|
|
21
|
+
* inputs reach and no output or error does. A member a response can
|
|
22
|
+
* deliver would need the mirror-image decode, which the protocol does
|
|
23
|
+
* not do, so a shape used both ways keeps its documented string type;
|
|
24
24
|
* • the enum must be exactly `{"true", "false"}` — a three-value enum
|
|
25
25
|
* that happens to include them is left alone.
|
|
26
26
|
*/
|
|
@@ -56,40 +56,97 @@ const booleanEnumIds = (shapes: Record<string, any>): Set<string> => {
|
|
|
56
56
|
return out;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/** Every shape id a shape points at: members, list/map entries, mixins. */
|
|
60
|
+
const targetsOf = (shape: any): string[] => {
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
for (const member of Object.values<any>(shape?.members ?? {})) {
|
|
63
|
+
if (member?.target) out.push(member.target);
|
|
64
|
+
}
|
|
65
|
+
for (const key of ["member", "value", "key"] as const) {
|
|
66
|
+
const target = shape?.[key]?.target;
|
|
67
|
+
if (target) out.push(target);
|
|
68
|
+
}
|
|
69
|
+
for (const mixin of shape?.mixins ?? []) {
|
|
70
|
+
if (mixin?.target) out.push(mixin.target);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const reachable = (
|
|
76
|
+
shapes: Record<string, any>,
|
|
77
|
+
roots: ReadonlyArray<string | undefined>,
|
|
78
|
+
): Set<string> => {
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
const stack = roots.filter((r): r is string => r !== undefined);
|
|
81
|
+
while (stack.length > 0) {
|
|
82
|
+
const id = stack.pop()!;
|
|
83
|
+
if (seen.has(id) || shapes[id] === undefined) continue;
|
|
84
|
+
seen.add(id);
|
|
85
|
+
stack.push(...targetsOf(shapes[id]));
|
|
86
|
+
}
|
|
87
|
+
return seen;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Shapes an operation input reaches that no output or error does. Operation
|
|
92
|
+
* I/O is the spine; the `smithy.api#input`/`#output`/`#error` traits cover
|
|
93
|
+
* models whose shapes are not wired through an operation, and a structure
|
|
94
|
+
* the inputs never reach counts as a response root — unreachable from a
|
|
95
|
+
* request, it can only describe something the API hands back.
|
|
96
|
+
*/
|
|
97
|
+
const requestOnlyShapes = (shapes: Record<string, any>): Set<string> => {
|
|
98
|
+
const requestRoots: Array<string | undefined> = [];
|
|
99
|
+
const responseRoots: Array<string | undefined> = [];
|
|
100
|
+
for (const [id, shape] of Object.entries<any>(shapes)) {
|
|
101
|
+
if (shape?.type === "operation") {
|
|
102
|
+
requestRoots.push(shape.input?.target);
|
|
103
|
+
responseRoots.push(shape.output?.target);
|
|
104
|
+
for (const error of shape.errors ?? []) responseRoots.push(error?.target);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const traits = shape?.traits ?? {};
|
|
108
|
+
if ("smithy.api#input" in traits) requestRoots.push(id);
|
|
109
|
+
if ("smithy.api#output" in traits || "smithy.api#error" in traits) {
|
|
110
|
+
responseRoots.push(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const fromRequests = reachable(shapes, requestRoots);
|
|
114
|
+
for (const [id, shape] of Object.entries<any>(shapes)) {
|
|
115
|
+
if (shape?.type === "structure" && !fromRequests.has(id)) {
|
|
116
|
+
responseRoots.push(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const fromResponses = reachable(shapes, responseRoots);
|
|
120
|
+
const out = new Set<string>();
|
|
121
|
+
for (const id of fromRequests) {
|
|
122
|
+
if (!fromResponses.has(id)) out.add(id);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
61
126
|
|
|
62
127
|
export const booleanStringEnums = (model: any): BooleanStringEnumResult => {
|
|
63
128
|
const shapes: Record<string, any> = model?.shapes ?? {};
|
|
64
129
|
const boolEnums = booleanEnumIds(shapes);
|
|
65
130
|
if (boolEnums.size === 0) return { members: 0, lists: 0 };
|
|
66
131
|
|
|
67
|
-
|
|
68
|
-
// refers to them (a shared list keeps its documented string type).
|
|
132
|
+
const requestOnly = requestOnlyShapes(shapes);
|
|
69
133
|
const boolLists = new Set<string>();
|
|
70
134
|
for (const [id, shape] of Object.entries(shapes)) {
|
|
71
|
-
if (
|
|
135
|
+
if (
|
|
136
|
+
shape?.type === "list" &&
|
|
137
|
+
boolEnums.has(shape.member?.target) &&
|
|
138
|
+
requestOnly.has(id)
|
|
139
|
+
) {
|
|
72
140
|
boolLists.add(id);
|
|
73
141
|
}
|
|
74
142
|
}
|
|
75
|
-
const sharedLists = new Set<string>();
|
|
76
|
-
for (const shape of Object.values(shapes)) {
|
|
77
|
-
if (isRequestShape(shape)) continue;
|
|
78
|
-
for (const member of Object.values<any>(shape?.members ?? {})) {
|
|
79
|
-
if (boolLists.has(member?.target)) sharedLists.add(member.target);
|
|
80
|
-
}
|
|
81
|
-
if (boolLists.has(shape?.member?.target)) {
|
|
82
|
-
sharedLists.add(shape.member.target);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
143
|
|
|
86
144
|
let members = 0;
|
|
87
145
|
const retargeted = new Set<string>();
|
|
88
|
-
for (const shape of Object.
|
|
89
|
-
if (!
|
|
146
|
+
for (const [id, shape] of Object.entries<any>(shapes)) {
|
|
147
|
+
if (shape?.type !== "structure" || !requestOnly.has(id)) continue;
|
|
90
148
|
for (const member of Object.values<any>(shape.members ?? {})) {
|
|
91
|
-
const isList =
|
|
92
|
-
boolLists.has(member?.target) && !sharedLists.has(member.target);
|
|
149
|
+
const isList = boolLists.has(member?.target);
|
|
93
150
|
if (!boolEnums.has(member?.target) && !isList) continue;
|
|
94
151
|
if (isList) {
|
|
95
152
|
retargeted.add(member.target);
|
package/src/codegen/cli.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
|
14
14
|
import { Console, Effect } from "effect";
|
|
15
15
|
import * as FileSystem from "effect/FileSystem";
|
|
16
16
|
import * as Path from "effect/Path";
|
|
17
|
-
import { Flag } from "effect/
|
|
18
|
-
import { Command } from "effect/
|
|
17
|
+
import { Flag } from "effect/cli";
|
|
18
|
+
import { Command } from "effect/cli";
|
|
19
19
|
import { barrel } from "./emit.ts";
|
|
20
20
|
import { formatGenerated } from "./format.ts";
|
|
21
21
|
import { generateService, type SdkSpec } from "./generator.ts";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { generateService, type SdkSpec } from "./generator.ts";
|
|
3
|
+
|
|
4
|
+
const primitiveUnionModel = (kind: "enum" | "intEnum", request: boolean) => ({
|
|
5
|
+
smithy: "2.0",
|
|
6
|
+
shapes: {
|
|
7
|
+
"com.example.union#Example": {
|
|
8
|
+
type: "service",
|
|
9
|
+
version: "2024-01-01",
|
|
10
|
+
operations: [{ target: "com.example.union#GetValue" }],
|
|
11
|
+
},
|
|
12
|
+
"com.example.union#GetValue": {
|
|
13
|
+
type: "operation",
|
|
14
|
+
input: { target: "com.example.union#Request" },
|
|
15
|
+
output: { target: "com.example.union#Response" },
|
|
16
|
+
traits: { "smithy.api#http": { method: "POST", uri: "/value" } },
|
|
17
|
+
},
|
|
18
|
+
"com.example.union#Request": {
|
|
19
|
+
type: "structure",
|
|
20
|
+
traits: { "smithy.api#input": {} },
|
|
21
|
+
members: request ? { value: { target: "com.example.union#Value" } } : {},
|
|
22
|
+
},
|
|
23
|
+
"com.example.union#Response": {
|
|
24
|
+
type: "structure",
|
|
25
|
+
members: { value: { target: "com.example.union#Value" } },
|
|
26
|
+
},
|
|
27
|
+
"com.example.union#Mode": {
|
|
28
|
+
type: kind,
|
|
29
|
+
members: {
|
|
30
|
+
STOP: {
|
|
31
|
+
target: "smithy.api#Unit",
|
|
32
|
+
traits: { "smithy.api#enumValue": kind === "enum" ? "stop" : 1 },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"com.example.union#Value": {
|
|
37
|
+
type: "union",
|
|
38
|
+
members: {
|
|
39
|
+
mode: { target: "com.example.union#Mode" },
|
|
40
|
+
legacy: { target: "smithy.api#Boolean" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const spec: SdkSpec = {
|
|
47
|
+
unionStyle: "primitive",
|
|
48
|
+
operationDecl: {
|
|
49
|
+
contextType: "ExampleContext",
|
|
50
|
+
commonErrorType: "ExampleError",
|
|
51
|
+
commonErrorClasses: [],
|
|
52
|
+
protocol: "ExampleProtocol",
|
|
53
|
+
retry: "Retry.Retry",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("operation protocol overrides", () => {
|
|
58
|
+
for (const retry of [undefined, "Retry.Retry"]) {
|
|
59
|
+
for (const overridden of [false, true]) {
|
|
60
|
+
test(`${overridden ? "overridden" : "default"} protocol with ${retry ? "configured" : "disabled"} retries`, () => {
|
|
61
|
+
const { code } = generateService(primitiveUnionModel("enum", false), {
|
|
62
|
+
...spec,
|
|
63
|
+
operationDecl: {
|
|
64
|
+
contextType: "ExampleContext",
|
|
65
|
+
commonErrorType: "ExampleError",
|
|
66
|
+
commonErrorClasses: [],
|
|
67
|
+
protocol: "ExampleProtocol",
|
|
68
|
+
retry,
|
|
69
|
+
overrides: () =>
|
|
70
|
+
overridden
|
|
71
|
+
? { contextType: "PublicContext", protocol: "PublicProtocol" }
|
|
72
|
+
: undefined,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(code).toContain(
|
|
76
|
+
` protocol: ${overridden ? "PublicProtocol" : "ExampleProtocol"},`,
|
|
77
|
+
);
|
|
78
|
+
expect(code).toContain(
|
|
79
|
+
` ${overridden ? "PublicContext" : "ExampleContext"}\n>`,
|
|
80
|
+
);
|
|
81
|
+
if (retry) expect(code).toContain(` retry: ${retry},`);
|
|
82
|
+
else expect(code).not.toContain(" retry:");
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("primitive union generation", () => {
|
|
89
|
+
for (const kind of ["enum", "intEnum"] as const) {
|
|
90
|
+
for (const request of [false, true]) {
|
|
91
|
+
test(`${kind}/Boolean stays open when ${request ? "request-reachable" : "response-only"}`, () => {
|
|
92
|
+
const { code } = generateService(
|
|
93
|
+
primitiveUnionModel(kind, request),
|
|
94
|
+
spec,
|
|
95
|
+
);
|
|
96
|
+
const primitive = kind === "enum" ? "string" : "number";
|
|
97
|
+
expect(code).toContain(
|
|
98
|
+
`export type Value = Mode | (${primitive} & {}) | boolean;`,
|
|
99
|
+
);
|
|
100
|
+
expect(code).toContain(
|
|
101
|
+
"export const Value: S.Codec<Value> = /*@__PURE__*/ S.Union([Mode, S.Boolean]);",
|
|
102
|
+
);
|
|
103
|
+
expect(code).toContain(
|
|
104
|
+
kind === "enum"
|
|
105
|
+
? "export const Mode = S.String;"
|
|
106
|
+
: "export const Mode = S.Number;",
|
|
107
|
+
);
|
|
108
|
+
expect(code).not.toContain("export type Value = Mode | boolean;");
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test("opaque-cases retains its existing primitive union codec", () => {
|
|
114
|
+
const { code } = generateService(primitiveUnionModel("enum", false), {
|
|
115
|
+
...spec,
|
|
116
|
+
unionStyle: "opaque-cases",
|
|
117
|
+
});
|
|
118
|
+
expect(code).toContain("export type Value = Mode | boolean;");
|
|
119
|
+
expect(code).toContain(
|
|
120
|
+
"export const Value = /*@__PURE__*/ S.Unknown.pipe(T.UnionCases([[],[]]));",
|
|
121
|
+
);
|
|
122
|
+
expect(code).not.toContain("S.Union([Mode, S.Boolean])");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("primitive union emission requires an explicit style", () => {
|
|
126
|
+
expect(() =>
|
|
127
|
+
generateService(primitiveUnionModel("enum", false), {
|
|
128
|
+
...spec,
|
|
129
|
+
unionStyle: undefined,
|
|
130
|
+
}),
|
|
131
|
+
).toThrow("no union emission configured for shape com.example.union#Value");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("primitive style rejects object cases", () => {
|
|
135
|
+
const model = primitiveUnionModel("enum", false);
|
|
136
|
+
model.shapes["com.example.union#Value"].members.mode.target =
|
|
137
|
+
"com.example.union#Request";
|
|
138
|
+
expect(() => generateService(model, spec)).toThrow(
|
|
139
|
+
"no union emission configured for shape com.example.union#Value",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("custom union emission retains precedence", () => {
|
|
144
|
+
const { code } = generateService(primitiveUnionModel("enum", false), {
|
|
145
|
+
...spec,
|
|
146
|
+
union: ({ name }) => [`export type ${name} = CustomUnion;`],
|
|
147
|
+
});
|
|
148
|
+
expect(code).toContain("export type Value = CustomUnion;");
|
|
149
|
+
expect(code).not.toContain("S.Union([Mode, S.Boolean])");
|
|
150
|
+
});
|
|
151
|
+
});
|