@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,1128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proto3 → Smithy 2.0 JSON converter (dev-time only, provider-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Turns one or more `.proto` files (a package of messages, enums, and gRPC
|
|
5
|
+
* services) into the same Smithy JSON model shape the other spec converters
|
|
6
|
+
* produce (`{ smithy: "2.0", metadata, shapes }`), so a proto-sourced
|
|
7
|
+
* provider flows through the shared `generateService` compiler.
|
|
8
|
+
*
|
|
9
|
+
* Mapping (proto3 JSON encoding on the wire):
|
|
10
|
+
*
|
|
11
|
+
* • each selected unary `rpc` → a Smithy operation, `POST /<package>.<Service>/<Method>`
|
|
12
|
+
* • request/response messages → structures; members are camelCase (proto3 JSON)
|
|
13
|
+
* • `repeated` → list, `map<k,v>` → map (JSON object keys are strings)
|
|
14
|
+
* • `oneof` fields flatten onto the parent (proto3 JSON is untagged)
|
|
15
|
+
* • enums → string enums (proto3 JSON emits the name)
|
|
16
|
+
* • 64-bit integers → `smithy.api#String` (proto3 JSON encodes them as strings)
|
|
17
|
+
* • `bytes` → `smithy.api#Blob` (base64 on the JSON wire)
|
|
18
|
+
* • `google.protobuf.Empty` I/O → `smithy.api#Unit`
|
|
19
|
+
* • streaming RPCs are skipped by default (they are not request/response POSTs)
|
|
20
|
+
*
|
|
21
|
+
* The converter does not speak binary protobuf. Generated SDKs send proto3
|
|
22
|
+
* JSON at the gRPC method path — Connect-JSON / grpc-JSON transcoding shape.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { SENSITIVE_FIELD_PATTERNS } from "./openapi.ts";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Prelude
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export const PRELUDE = {
|
|
32
|
+
String: "smithy.api#String",
|
|
33
|
+
Boolean: "smithy.api#Boolean",
|
|
34
|
+
Integer: "smithy.api#Integer",
|
|
35
|
+
Long: "smithy.api#Long",
|
|
36
|
+
Float: "smithy.api#Float",
|
|
37
|
+
Double: "smithy.api#Double",
|
|
38
|
+
Timestamp: "smithy.api#Timestamp",
|
|
39
|
+
Blob: "smithy.api#Blob",
|
|
40
|
+
Document: "smithy.api#Document",
|
|
41
|
+
Unit: "smithy.api#Unit",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
const SCALAR_TARGETS: Record<string, string> = {
|
|
45
|
+
double: PRELUDE.Double,
|
|
46
|
+
float: PRELUDE.Float,
|
|
47
|
+
int32: PRELUDE.Integer,
|
|
48
|
+
int64: PRELUDE.String,
|
|
49
|
+
uint32: PRELUDE.Integer,
|
|
50
|
+
uint64: PRELUDE.String,
|
|
51
|
+
sint32: PRELUDE.Integer,
|
|
52
|
+
sint64: PRELUDE.String,
|
|
53
|
+
fixed32: PRELUDE.Integer,
|
|
54
|
+
fixed64: PRELUDE.String,
|
|
55
|
+
sfixed32: PRELUDE.Integer,
|
|
56
|
+
sfixed64: PRELUDE.String,
|
|
57
|
+
bool: PRELUDE.Boolean,
|
|
58
|
+
string: PRELUDE.String,
|
|
59
|
+
bytes: PRELUDE.Blob,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const WKT_TARGETS: Record<string, string> = {
|
|
63
|
+
"google.protobuf.Timestamp": PRELUDE.Timestamp,
|
|
64
|
+
"google.protobuf.Duration": PRELUDE.String,
|
|
65
|
+
"google.protobuf.Any": PRELUDE.Document,
|
|
66
|
+
"google.protobuf.Struct": PRELUDE.Document,
|
|
67
|
+
"google.protobuf.Value": PRELUDE.Document,
|
|
68
|
+
"google.protobuf.ListValue": PRELUDE.Document,
|
|
69
|
+
"google.protobuf.BoolValue": PRELUDE.Boolean,
|
|
70
|
+
"google.protobuf.StringValue": PRELUDE.String,
|
|
71
|
+
"google.protobuf.BytesValue": PRELUDE.Blob,
|
|
72
|
+
"google.protobuf.Int32Value": PRELUDE.Integer,
|
|
73
|
+
"google.protobuf.UInt32Value": PRELUDE.Integer,
|
|
74
|
+
"google.protobuf.Int64Value": PRELUDE.String,
|
|
75
|
+
"google.protobuf.UInt64Value": PRELUDE.String,
|
|
76
|
+
"google.protobuf.FloatValue": PRELUDE.Float,
|
|
77
|
+
"google.protobuf.DoubleValue": PRELUDE.Double,
|
|
78
|
+
"google.protobuf.FieldMask": PRELUDE.String,
|
|
79
|
+
"google.protobuf.Empty": PRELUDE.Unit,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// AST
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export interface ProtoField {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly type: string;
|
|
89
|
+
readonly repeated: boolean;
|
|
90
|
+
readonly optional: boolean;
|
|
91
|
+
readonly mapKey?: string;
|
|
92
|
+
readonly mapValue?: string;
|
|
93
|
+
readonly jsonName?: string;
|
|
94
|
+
readonly deprecated: boolean;
|
|
95
|
+
readonly documentation?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ProtoEnumValue {
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly number: number;
|
|
101
|
+
readonly deprecated: boolean;
|
|
102
|
+
readonly documentation?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ProtoEnum {
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly fullName: string;
|
|
108
|
+
readonly values: ProtoEnumValue[];
|
|
109
|
+
readonly documentation?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ProtoMessage {
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly fullName: string;
|
|
115
|
+
readonly fields: ProtoField[];
|
|
116
|
+
readonly messages: ProtoMessage[];
|
|
117
|
+
readonly enums: ProtoEnum[];
|
|
118
|
+
readonly documentation?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ProtoRpc {
|
|
122
|
+
readonly name: string;
|
|
123
|
+
readonly requestType: string;
|
|
124
|
+
readonly responseType: string;
|
|
125
|
+
readonly requestStream: boolean;
|
|
126
|
+
readonly responseStream: boolean;
|
|
127
|
+
readonly deprecated: boolean;
|
|
128
|
+
readonly documentation?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ProtoService {
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly fullName: string;
|
|
134
|
+
readonly rpcs: ProtoRpc[];
|
|
135
|
+
readonly documentation?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ProtoFile {
|
|
139
|
+
readonly filename: string;
|
|
140
|
+
readonly package: string;
|
|
141
|
+
readonly syntax: string;
|
|
142
|
+
readonly imports: string[];
|
|
143
|
+
readonly messages: ProtoMessage[];
|
|
144
|
+
readonly enums: ProtoEnum[];
|
|
145
|
+
readonly services: ProtoService[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ProtoConvertOptions {
|
|
149
|
+
readonly files: readonly ProtoFile[];
|
|
150
|
+
readonly namespace: string;
|
|
151
|
+
readonly serviceName: string;
|
|
152
|
+
readonly serviceTitle?: string;
|
|
153
|
+
readonly serviceDocumentation?: string;
|
|
154
|
+
readonly serviceVersion?: string;
|
|
155
|
+
/** Fully-qualified proto service (`modal.client.ModalClient`). */
|
|
156
|
+
readonly protoService: string;
|
|
157
|
+
/** Keep only these RPC names. Default: every unary RPC. */
|
|
158
|
+
readonly rpcNames?: ReadonlySet<string>;
|
|
159
|
+
readonly skipStreaming?: boolean;
|
|
160
|
+
readonly skipDeprecated?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ProtoConvertResult {
|
|
164
|
+
readonly model: {
|
|
165
|
+
smithy: "2.0";
|
|
166
|
+
metadata: Record<string, unknown>;
|
|
167
|
+
shapes: Record<string, any>;
|
|
168
|
+
};
|
|
169
|
+
readonly converted: number;
|
|
170
|
+
readonly skippedStreaming: number;
|
|
171
|
+
readonly skippedDeprecated: number;
|
|
172
|
+
readonly shapeCount: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Lexer / parser
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
class ProtoParseError extends Error {
|
|
180
|
+
constructor(filename: string, line: number, col: number, message: string) {
|
|
181
|
+
super(`${filename}:${line}:${col}: ${message}`);
|
|
182
|
+
this.name = "ProtoParseError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class Parser {
|
|
187
|
+
readonly filename: string;
|
|
188
|
+
readonly src: string;
|
|
189
|
+
i = 0;
|
|
190
|
+
line = 1;
|
|
191
|
+
col = 1;
|
|
192
|
+
lastComment = "";
|
|
193
|
+
|
|
194
|
+
constructor(src: string, filename: string) {
|
|
195
|
+
this.src = src;
|
|
196
|
+
this.filename = filename;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fail(message: string): never {
|
|
200
|
+
throw new ProtoParseError(this.filename, this.line, this.col, message);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
peekChar(): string {
|
|
204
|
+
return this.src[this.i] ?? "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
bump(): string {
|
|
208
|
+
const ch = this.src[this.i] ?? "";
|
|
209
|
+
this.i++;
|
|
210
|
+
if (ch === "\n") {
|
|
211
|
+
this.line++;
|
|
212
|
+
this.col = 1;
|
|
213
|
+
} else {
|
|
214
|
+
this.col++;
|
|
215
|
+
}
|
|
216
|
+
return ch;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
skipTrivia(): void {
|
|
220
|
+
const comments: string[] = [];
|
|
221
|
+
while (this.i < this.src.length) {
|
|
222
|
+
const ch = this.peekChar();
|
|
223
|
+
if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
|
|
224
|
+
this.bump();
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === "/" && this.src[this.i + 1] === "/") {
|
|
228
|
+
this.bump();
|
|
229
|
+
this.bump();
|
|
230
|
+
let text = "";
|
|
231
|
+
while (this.i < this.src.length && this.peekChar() !== "\n") {
|
|
232
|
+
text += this.bump();
|
|
233
|
+
}
|
|
234
|
+
comments.push(text.trim());
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (ch === "/" && this.src[this.i + 1] === "*") {
|
|
238
|
+
this.bump();
|
|
239
|
+
this.bump();
|
|
240
|
+
let text = "";
|
|
241
|
+
while (this.i < this.src.length) {
|
|
242
|
+
if (this.peekChar() === "*" && this.src[this.i + 1] === "/") {
|
|
243
|
+
this.bump();
|
|
244
|
+
this.bump();
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
text += this.bump();
|
|
248
|
+
}
|
|
249
|
+
for (const line of text.split("\n")) {
|
|
250
|
+
const t = line.replace(/^\s*\*?\s?/, "").trim();
|
|
251
|
+
if (t) comments.push(t);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (comments.length > 0) this.lastComment = comments.join(" ");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
takeDoc(): string | undefined {
|
|
261
|
+
const d = this.lastComment.trim();
|
|
262
|
+
this.lastComment = "";
|
|
263
|
+
return d || undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
peekIdent(): boolean {
|
|
267
|
+
this.skipTrivia();
|
|
268
|
+
const ch = this.peekChar();
|
|
269
|
+
return /[A-Za-z_]/.test(ch);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
ident(): string {
|
|
273
|
+
this.skipTrivia();
|
|
274
|
+
const ch = this.peekChar();
|
|
275
|
+
if (!/[A-Za-z_]/.test(ch))
|
|
276
|
+
this.fail(`expected identifier, got ${JSON.stringify(ch)}`);
|
|
277
|
+
let out = this.bump();
|
|
278
|
+
while (/[A-Za-z0-9_]/.test(this.peekChar())) out += this.bump();
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
tryIdent(want: string): boolean {
|
|
283
|
+
this.skipTrivia();
|
|
284
|
+
const start = this.i;
|
|
285
|
+
const line = this.line;
|
|
286
|
+
const col = this.col;
|
|
287
|
+
const comment = this.lastComment;
|
|
288
|
+
if (!this.peekIdent()) return false;
|
|
289
|
+
const got = this.ident();
|
|
290
|
+
if (got === want) return true;
|
|
291
|
+
this.i = start;
|
|
292
|
+
this.line = line;
|
|
293
|
+
this.col = col;
|
|
294
|
+
this.lastComment = comment;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
dotted(): string {
|
|
299
|
+
let name = this.ident();
|
|
300
|
+
while (this.tryChar(".")) name += `.${this.ident()}`;
|
|
301
|
+
return name;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
tryChar(ch: string): boolean {
|
|
305
|
+
this.skipTrivia();
|
|
306
|
+
if (this.peekChar() === ch) {
|
|
307
|
+
this.bump();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
expect(ch: string): void {
|
|
314
|
+
this.skipTrivia();
|
|
315
|
+
if (this.peekChar() !== ch) {
|
|
316
|
+
this.fail(
|
|
317
|
+
`expected ${JSON.stringify(ch)}, got ${JSON.stringify(this.peekChar())}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
this.bump();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
stringLit(): string {
|
|
324
|
+
this.skipTrivia();
|
|
325
|
+
const q = this.peekChar();
|
|
326
|
+
if (q !== '"' && q !== "'") this.fail("expected string literal");
|
|
327
|
+
this.bump();
|
|
328
|
+
let out = "";
|
|
329
|
+
while (this.i < this.src.length && this.peekChar() !== q) {
|
|
330
|
+
const ch = this.bump();
|
|
331
|
+
if (ch === "\\" && this.i < this.src.length) {
|
|
332
|
+
const n = this.bump();
|
|
333
|
+
const esc: Record<string, string> = {
|
|
334
|
+
n: "\n",
|
|
335
|
+
t: "\t",
|
|
336
|
+
r: "\r",
|
|
337
|
+
'"': '"',
|
|
338
|
+
"'": "'",
|
|
339
|
+
"\\": "\\",
|
|
340
|
+
};
|
|
341
|
+
out += esc[n] ?? n;
|
|
342
|
+
} else {
|
|
343
|
+
out += ch;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (this.peekChar() !== q) this.fail("unterminated string");
|
|
347
|
+
this.bump();
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
number(): number {
|
|
352
|
+
this.skipTrivia();
|
|
353
|
+
let s = "";
|
|
354
|
+
if (this.peekChar() === "-") s += this.bump();
|
|
355
|
+
if (!/[0-9]/.test(this.peekChar())) this.fail("expected number");
|
|
356
|
+
while (/[0-9]/.test(this.peekChar())) s += this.bump();
|
|
357
|
+
return Number(s);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
skipUntil(ch: string): void {
|
|
361
|
+
this.skipTrivia();
|
|
362
|
+
while (this.i < this.src.length && this.peekChar() !== ch) {
|
|
363
|
+
if (this.peekChar() === '"' || this.peekChar() === "'") this.stringLit();
|
|
364
|
+
else if (this.peekChar() === "{") {
|
|
365
|
+
this.bump();
|
|
366
|
+
this.skipUntil("}");
|
|
367
|
+
this.expect("}");
|
|
368
|
+
} else {
|
|
369
|
+
this.bump();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
parseOptions(): { jsonName?: string; deprecated: boolean } {
|
|
375
|
+
let jsonName: string | undefined;
|
|
376
|
+
let deprecated = false;
|
|
377
|
+
if (!this.tryChar("[")) return { deprecated };
|
|
378
|
+
while (true) {
|
|
379
|
+
this.skipTrivia();
|
|
380
|
+
if (this.peekChar() === "]") break;
|
|
381
|
+
const key = this.tryChar("(") ? `(${this.dotted()})` : this.dotted();
|
|
382
|
+
this.expect("=");
|
|
383
|
+
this.skipTrivia();
|
|
384
|
+
let value: unknown;
|
|
385
|
+
if (this.peekChar() === '"' || this.peekChar() === "'")
|
|
386
|
+
value = this.stringLit();
|
|
387
|
+
else if (this.tryIdent("true")) value = true;
|
|
388
|
+
else if (this.tryIdent("false")) value = false;
|
|
389
|
+
else if (/[0-9-]/.test(this.peekChar())) value = this.number();
|
|
390
|
+
else value = this.ident();
|
|
391
|
+
if (key === "json_name" && typeof value === "string") jsonName = value;
|
|
392
|
+
if (key === "deprecated" && value === true) deprecated = true;
|
|
393
|
+
this.tryChar(",");
|
|
394
|
+
}
|
|
395
|
+
this.expect("]");
|
|
396
|
+
return { jsonName, deprecated };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
parseField(): ProtoField {
|
|
400
|
+
const documentation = this.takeDoc();
|
|
401
|
+
let repeated = false;
|
|
402
|
+
let optional = false;
|
|
403
|
+
if (this.tryIdent("repeated")) repeated = true;
|
|
404
|
+
else if (this.tryIdent("optional")) optional = true;
|
|
405
|
+
let mapKey: string | undefined;
|
|
406
|
+
let mapValue: string | undefined;
|
|
407
|
+
let type: string;
|
|
408
|
+
if (this.tryIdent("map")) {
|
|
409
|
+
this.expect("<");
|
|
410
|
+
mapKey = this.dotted();
|
|
411
|
+
this.expect(",");
|
|
412
|
+
mapValue = this.dotted();
|
|
413
|
+
this.expect(">");
|
|
414
|
+
type = `map<${mapKey},${mapValue}>`;
|
|
415
|
+
} else {
|
|
416
|
+
type = this.dotted();
|
|
417
|
+
}
|
|
418
|
+
const name = this.ident();
|
|
419
|
+
this.expect("=");
|
|
420
|
+
this.number();
|
|
421
|
+
const opts = this.parseOptions();
|
|
422
|
+
this.expect(";");
|
|
423
|
+
return {
|
|
424
|
+
name,
|
|
425
|
+
type,
|
|
426
|
+
repeated,
|
|
427
|
+
optional,
|
|
428
|
+
mapKey,
|
|
429
|
+
mapValue,
|
|
430
|
+
jsonName: opts.jsonName,
|
|
431
|
+
deprecated: opts.deprecated,
|
|
432
|
+
documentation,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
parseEnum(pkg: string, prefix: string): ProtoEnum {
|
|
437
|
+
const documentation = this.takeDoc();
|
|
438
|
+
const name = this.ident();
|
|
439
|
+
const fullName = prefix ? `${prefix}.${name}` : `${pkg}.${name}`;
|
|
440
|
+
this.expect("{");
|
|
441
|
+
const values: ProtoEnumValue[] = [];
|
|
442
|
+
while (true) {
|
|
443
|
+
this.skipTrivia();
|
|
444
|
+
if (this.peekChar() === "}") break;
|
|
445
|
+
if (this.tryIdent("option") || this.tryIdent("reserved")) {
|
|
446
|
+
this.skipUntil(";");
|
|
447
|
+
this.expect(";");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const vdoc = this.takeDoc();
|
|
451
|
+
const vname = this.ident();
|
|
452
|
+
this.expect("=");
|
|
453
|
+
const number = this.number();
|
|
454
|
+
const opts = this.parseOptions();
|
|
455
|
+
this.expect(";");
|
|
456
|
+
values.push({
|
|
457
|
+
name: vname,
|
|
458
|
+
number,
|
|
459
|
+
deprecated: opts.deprecated,
|
|
460
|
+
documentation: vdoc,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
this.expect("}");
|
|
464
|
+
this.tryChar(";");
|
|
465
|
+
return { name, fullName, values, documentation };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
parseMessage(pkg: string, prefix: string): ProtoMessage {
|
|
469
|
+
const documentation = this.takeDoc();
|
|
470
|
+
const name = this.ident();
|
|
471
|
+
const fullName = prefix ? `${prefix}.${name}` : `${pkg}.${name}`;
|
|
472
|
+
this.expect("{");
|
|
473
|
+
const fields: ProtoField[] = [];
|
|
474
|
+
const messages: ProtoMessage[] = [];
|
|
475
|
+
const enums: ProtoEnum[] = [];
|
|
476
|
+
while (true) {
|
|
477
|
+
this.skipTrivia();
|
|
478
|
+
if (this.peekChar() === "}") break;
|
|
479
|
+
if (
|
|
480
|
+
this.tryIdent("option") ||
|
|
481
|
+
this.tryIdent("reserved") ||
|
|
482
|
+
this.tryIdent("extensions")
|
|
483
|
+
) {
|
|
484
|
+
this.skipUntil(";");
|
|
485
|
+
this.expect(";");
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (this.tryIdent("message")) {
|
|
489
|
+
messages.push(this.parseMessage(pkg, fullName));
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (this.tryIdent("enum")) {
|
|
493
|
+
enums.push(this.parseEnum(pkg, fullName));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (this.tryIdent("oneof")) {
|
|
497
|
+
this.ident();
|
|
498
|
+
this.expect("{");
|
|
499
|
+
while (true) {
|
|
500
|
+
this.skipTrivia();
|
|
501
|
+
if (this.peekChar() === "}") break;
|
|
502
|
+
if (this.tryIdent("option")) {
|
|
503
|
+
this.skipUntil(";");
|
|
504
|
+
this.expect(";");
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
fields.push({ ...this.parseField(), optional: true });
|
|
508
|
+
}
|
|
509
|
+
this.expect("}");
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (this.peekChar() === ";") {
|
|
513
|
+
this.bump();
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
fields.push(this.parseField());
|
|
517
|
+
}
|
|
518
|
+
this.expect("}");
|
|
519
|
+
this.tryChar(";");
|
|
520
|
+
return { name, fullName, fields, messages, enums, documentation };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
parseRpc(): ProtoRpc {
|
|
524
|
+
const documentation = this.takeDoc();
|
|
525
|
+
const name = this.ident();
|
|
526
|
+
this.expect("(");
|
|
527
|
+
const requestStream = this.tryIdent("stream");
|
|
528
|
+
const requestType = this.dotted();
|
|
529
|
+
this.expect(")");
|
|
530
|
+
if (!this.tryIdent("returns")) this.fail("expected `returns`");
|
|
531
|
+
this.expect("(");
|
|
532
|
+
const responseStream = this.tryIdent("stream");
|
|
533
|
+
const responseType = this.dotted();
|
|
534
|
+
this.expect(")");
|
|
535
|
+
let deprecated = false;
|
|
536
|
+
if (this.tryChar("{")) {
|
|
537
|
+
while (true) {
|
|
538
|
+
this.skipTrivia();
|
|
539
|
+
if (this.peekChar() === "}") break;
|
|
540
|
+
if (this.tryIdent("option")) {
|
|
541
|
+
const start = this.i;
|
|
542
|
+
this.skipUntil(";");
|
|
543
|
+
const body = this.src.slice(start, this.i);
|
|
544
|
+
if (/deprecated\s*=\s*true/.test(body)) deprecated = true;
|
|
545
|
+
this.expect(";");
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
this.skipUntil("}");
|
|
549
|
+
}
|
|
550
|
+
this.expect("}");
|
|
551
|
+
} else {
|
|
552
|
+
this.expect(";");
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
name,
|
|
556
|
+
requestType,
|
|
557
|
+
responseType,
|
|
558
|
+
requestStream,
|
|
559
|
+
responseStream,
|
|
560
|
+
deprecated,
|
|
561
|
+
documentation,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
parseService(pkg: string): ProtoService {
|
|
566
|
+
const documentation = this.takeDoc();
|
|
567
|
+
const name = this.ident();
|
|
568
|
+
this.expect("{");
|
|
569
|
+
const rpcs: ProtoRpc[] = [];
|
|
570
|
+
while (true) {
|
|
571
|
+
this.skipTrivia();
|
|
572
|
+
if (this.peekChar() === "}") break;
|
|
573
|
+
if (this.tryIdent("option") || this.tryIdent("reserved")) {
|
|
574
|
+
this.skipUntil(";");
|
|
575
|
+
this.expect(";");
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (this.tryIdent("rpc")) {
|
|
579
|
+
rpcs.push(this.parseRpc());
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
this.fail(`unexpected token in service ${name}`);
|
|
583
|
+
}
|
|
584
|
+
this.expect("}");
|
|
585
|
+
this.tryChar(";");
|
|
586
|
+
return { name, fullName: `${pkg}.${name}`, rpcs, documentation };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
parseFile(): ProtoFile {
|
|
590
|
+
let syntax = "proto2";
|
|
591
|
+
let pkg = "";
|
|
592
|
+
const imports: string[] = [];
|
|
593
|
+
const messages: ProtoMessage[] = [];
|
|
594
|
+
const enums: ProtoEnum[] = [];
|
|
595
|
+
const services: ProtoService[] = [];
|
|
596
|
+
this.skipTrivia();
|
|
597
|
+
while (this.i < this.src.length) {
|
|
598
|
+
this.skipTrivia();
|
|
599
|
+
if (this.i >= this.src.length) break;
|
|
600
|
+
if (this.tryIdent("syntax")) {
|
|
601
|
+
this.expect("=");
|
|
602
|
+
syntax = this.stringLit();
|
|
603
|
+
this.expect(";");
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (this.tryIdent("package")) {
|
|
607
|
+
pkg = this.dotted();
|
|
608
|
+
this.expect(";");
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (this.tryIdent("import")) {
|
|
612
|
+
this.tryIdent("public");
|
|
613
|
+
this.tryIdent("weak");
|
|
614
|
+
imports.push(this.stringLit());
|
|
615
|
+
this.expect(";");
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (this.tryIdent("option")) {
|
|
619
|
+
this.skipUntil(";");
|
|
620
|
+
this.expect(";");
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (this.tryIdent("message")) {
|
|
624
|
+
messages.push(this.parseMessage(pkg, ""));
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (this.tryIdent("enum")) {
|
|
628
|
+
enums.push(this.parseEnum(pkg, ""));
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (this.tryIdent("service")) {
|
|
632
|
+
services.push(this.parseService(pkg));
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
this.fail(`unexpected token ${JSON.stringify(this.peekChar())}`);
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
filename: this.filename,
|
|
639
|
+
package: pkg,
|
|
640
|
+
syntax,
|
|
641
|
+
imports,
|
|
642
|
+
messages,
|
|
643
|
+
enums,
|
|
644
|
+
services,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Parse a proto3 source file into an AST. */
|
|
650
|
+
export const parseProto = (
|
|
651
|
+
text: string,
|
|
652
|
+
filename = "input.proto",
|
|
653
|
+
): ProtoFile => {
|
|
654
|
+
const file = new Parser(text, filename).parseFile();
|
|
655
|
+
if (file.syntax !== "proto3") {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`${filename}: expected proto3, got ${JSON.stringify(file.syntax)}`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return file;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// ============================================================================
|
|
664
|
+
// Type universe
|
|
665
|
+
// ============================================================================
|
|
666
|
+
|
|
667
|
+
interface TypeIndex {
|
|
668
|
+
messages: Map<string, ProtoMessage>;
|
|
669
|
+
enums: Map<string, ProtoEnum>;
|
|
670
|
+
services: ProtoService[];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const walkMessage = (msg: ProtoMessage, index: TypeIndex): void => {
|
|
674
|
+
index.messages.set(msg.fullName, msg);
|
|
675
|
+
for (const nested of msg.messages) walkMessage(nested, index);
|
|
676
|
+
for (const en of msg.enums) index.enums.set(en.fullName, en);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const indexFiles = (files: readonly ProtoFile[]): TypeIndex => {
|
|
680
|
+
const index: TypeIndex = {
|
|
681
|
+
messages: new Map(),
|
|
682
|
+
enums: new Map(),
|
|
683
|
+
services: [],
|
|
684
|
+
};
|
|
685
|
+
for (const file of files) {
|
|
686
|
+
for (const msg of file.messages) walkMessage(msg, index);
|
|
687
|
+
for (const en of file.enums)
|
|
688
|
+
index.enums.set(en.fullName, index.enums.get(en.fullName) ?? en);
|
|
689
|
+
index.services.push(...file.services);
|
|
690
|
+
}
|
|
691
|
+
return index;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const resolveName = (
|
|
695
|
+
name: string,
|
|
696
|
+
pkg: string,
|
|
697
|
+
scope: string | undefined,
|
|
698
|
+
index: TypeIndex,
|
|
699
|
+
): string => {
|
|
700
|
+
const stripped = name.startsWith(".") ? name.slice(1) : name;
|
|
701
|
+
if (SCALAR_TARGETS[stripped]) return stripped;
|
|
702
|
+
if (WKT_TARGETS[stripped] || stripped.startsWith("google.protobuf."))
|
|
703
|
+
return stripped;
|
|
704
|
+
const candidates: string[] = [];
|
|
705
|
+
if (name.startsWith(".")) {
|
|
706
|
+
candidates.push(stripped);
|
|
707
|
+
} else {
|
|
708
|
+
if (scope) {
|
|
709
|
+
const parts = scope.split(".");
|
|
710
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
711
|
+
const prefix = parts.slice(0, i).join(".");
|
|
712
|
+
candidates.push(prefix ? `${prefix}.${stripped}` : stripped);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (pkg) candidates.push(`${pkg}.${stripped}`);
|
|
716
|
+
candidates.push(stripped);
|
|
717
|
+
}
|
|
718
|
+
for (const c of candidates) {
|
|
719
|
+
if (index.messages.has(c) || index.enums.has(c)) return c;
|
|
720
|
+
}
|
|
721
|
+
throw new Error(
|
|
722
|
+
`unresolved proto type ${JSON.stringify(name)} (package ${pkg}, scope ${scope})`,
|
|
723
|
+
);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// ============================================================================
|
|
727
|
+
// Smithy emission
|
|
728
|
+
// ============================================================================
|
|
729
|
+
|
|
730
|
+
interface Bag {
|
|
731
|
+
shapes: Record<string, any>;
|
|
732
|
+
names: Set<string>;
|
|
733
|
+
namespace: string;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const capitalize = (s: string): string =>
|
|
737
|
+
s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
738
|
+
|
|
739
|
+
const ident = (s: string): string => {
|
|
740
|
+
let out = s.replace(/[^A-Za-z0-9_]/g, "_");
|
|
741
|
+
if (/^[0-9]/.test(out)) out = `_${out}`;
|
|
742
|
+
return out || "_";
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const camel = (s: string): string =>
|
|
746
|
+
s.replace(/[_-]+([A-Za-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
747
|
+
|
|
748
|
+
const shapeNameFor = (fullName: string): string => {
|
|
749
|
+
const parts = fullName.split(".").filter(Boolean);
|
|
750
|
+
// Drop the proto package (everything before the first PascalCase type).
|
|
751
|
+
const start = parts.findIndex((p) => /^[A-Z]/.test(p));
|
|
752
|
+
const typeParts = start >= 0 ? parts.slice(start) : parts.slice(-1);
|
|
753
|
+
return ident(typeParts.join(""));
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const addExact = (bag: Bag, name: string, def: any): string => {
|
|
757
|
+
const id = `${bag.namespace}#${name}`;
|
|
758
|
+
if (bag.shapes[id] !== undefined) {
|
|
759
|
+
throw new Error(`smithy shape ${id} already exists`);
|
|
760
|
+
}
|
|
761
|
+
bag.shapes[id] = def;
|
|
762
|
+
bag.names.add(name);
|
|
763
|
+
return id;
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const addShape = (bag: Bag, base: string, def: any): string => {
|
|
767
|
+
let want = ident(capitalize(base));
|
|
768
|
+
let name = want;
|
|
769
|
+
let n = 2;
|
|
770
|
+
while (bag.names.has(name)) name = `${want}${n++}`;
|
|
771
|
+
return addExact(bag, name, def);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const oneLineDoc = (s: string | undefined): string | undefined => {
|
|
775
|
+
const t = s?.replace(/\s+/g, " ").trim();
|
|
776
|
+
return t ? t : undefined;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const isSensitive = (name: string): boolean =>
|
|
780
|
+
SENSITIVE_FIELD_PATTERNS.some((re) => re.test(name));
|
|
781
|
+
|
|
782
|
+
const EMPTY_STRUCT_ID = "GoogleProtobufEmpty";
|
|
783
|
+
|
|
784
|
+
type EmitCtx = {
|
|
785
|
+
bag: Bag;
|
|
786
|
+
index: TypeIndex;
|
|
787
|
+
pkg: string;
|
|
788
|
+
memo: Map<string, string>;
|
|
789
|
+
listMemo: Map<string, string>;
|
|
790
|
+
mapMemo: Map<string, string>;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const emitEnum = (ctx: EmitCtx, en: ProtoEnum): string => {
|
|
794
|
+
const cached = ctx.memo.get(en.fullName);
|
|
795
|
+
if (cached) return cached;
|
|
796
|
+
const members: Record<string, any> = {};
|
|
797
|
+
const used = new Set<string>();
|
|
798
|
+
for (const v of en.values) {
|
|
799
|
+
let mn = ident(v.name);
|
|
800
|
+
let k = 2;
|
|
801
|
+
while (used.has(mn)) mn = `${ident(v.name)}_${k++}`;
|
|
802
|
+
used.add(mn);
|
|
803
|
+
const traits: Record<string, any> = { "smithy.api#enumValue": v.name };
|
|
804
|
+
const doc = oneLineDoc(v.documentation);
|
|
805
|
+
if (doc) traits["smithy.api#documentation"] = doc;
|
|
806
|
+
if (v.deprecated) traits["smithy.api#deprecated"] = {};
|
|
807
|
+
members[mn] = { target: "smithy.api#Unit", traits };
|
|
808
|
+
}
|
|
809
|
+
const def: any = { type: "enum", members };
|
|
810
|
+
const doc = oneLineDoc(en.documentation);
|
|
811
|
+
if (doc) def.traits = { "smithy.api#documentation": doc };
|
|
812
|
+
const id = addShape(ctx.bag, shapeNameFor(en.fullName), def);
|
|
813
|
+
ctx.memo.set(en.fullName, id);
|
|
814
|
+
return id;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const emitEmptyStruct = (ctx: EmitCtx): string => {
|
|
818
|
+
const cached = ctx.memo.get("google.protobuf.Empty.struct");
|
|
819
|
+
if (cached) return cached;
|
|
820
|
+
const id = addShape(ctx.bag, EMPTY_STRUCT_ID, {
|
|
821
|
+
type: "structure",
|
|
822
|
+
members: {},
|
|
823
|
+
traits: {
|
|
824
|
+
"smithy.api#documentation": "google.protobuf.Empty as a nested field.",
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
ctx.memo.set("google.protobuf.Empty.struct", id);
|
|
828
|
+
return id;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const emitType = (
|
|
832
|
+
ctx: EmitCtx,
|
|
833
|
+
raw: string,
|
|
834
|
+
scope: string | undefined,
|
|
835
|
+
asField: boolean,
|
|
836
|
+
): string => {
|
|
837
|
+
if (SCALAR_TARGETS[raw]) return SCALAR_TARGETS[raw]!;
|
|
838
|
+
const resolved = resolveName(raw, ctx.pkg, scope, ctx.index);
|
|
839
|
+
if (SCALAR_TARGETS[resolved]) return SCALAR_TARGETS[resolved]!;
|
|
840
|
+
if (resolved === "google.protobuf.Empty") {
|
|
841
|
+
return asField ? emitEmptyStruct(ctx) : PRELUDE.Unit;
|
|
842
|
+
}
|
|
843
|
+
if (WKT_TARGETS[resolved]) return WKT_TARGETS[resolved]!;
|
|
844
|
+
const cached = ctx.memo.get(resolved);
|
|
845
|
+
if (cached) return cached;
|
|
846
|
+
const en = ctx.index.enums.get(resolved);
|
|
847
|
+
if (en) return emitEnum(ctx, en);
|
|
848
|
+
const msg = ctx.index.messages.get(resolved);
|
|
849
|
+
if (!msg) throw new Error(`no message or enum for ${resolved}`);
|
|
850
|
+
return emitMessage(ctx, msg);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const listOf = (ctx: EmitCtx, elem: string): string => {
|
|
854
|
+
const cached = ctx.listMemo.get(elem);
|
|
855
|
+
if (cached) return cached;
|
|
856
|
+
const local = elem.split("#")[1] ?? "Value";
|
|
857
|
+
const id = addShape(ctx.bag, `${local}List`, {
|
|
858
|
+
type: "list",
|
|
859
|
+
member: { target: elem },
|
|
860
|
+
});
|
|
861
|
+
ctx.listMemo.set(elem, id);
|
|
862
|
+
return id;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const mapOf = (ctx: EmitCtx, value: string): string => {
|
|
866
|
+
const cached = ctx.mapMemo.get(value);
|
|
867
|
+
if (cached) return cached;
|
|
868
|
+
const local = value.split("#")[1] ?? "Value";
|
|
869
|
+
const id = addShape(ctx.bag, `${local}Map`, {
|
|
870
|
+
type: "map",
|
|
871
|
+
key: { target: PRELUDE.String },
|
|
872
|
+
value: { target: value },
|
|
873
|
+
});
|
|
874
|
+
ctx.mapMemo.set(value, id);
|
|
875
|
+
return id;
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const fieldTarget = (
|
|
879
|
+
ctx: EmitCtx,
|
|
880
|
+
field: ProtoField,
|
|
881
|
+
scope: string,
|
|
882
|
+
): string => {
|
|
883
|
+
if (field.mapKey && field.mapValue) {
|
|
884
|
+
const value = emitType(ctx, field.mapValue, scope, true);
|
|
885
|
+
return mapOf(ctx, value);
|
|
886
|
+
}
|
|
887
|
+
const inner = emitType(ctx, field.type, scope, true);
|
|
888
|
+
return field.repeated ? listOf(ctx, inner) : inner;
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const memberTraits = (
|
|
892
|
+
field: ProtoField,
|
|
893
|
+
tsName: string,
|
|
894
|
+
): Record<string, any> => {
|
|
895
|
+
const traits: Record<string, any> = {};
|
|
896
|
+
const doc = oneLineDoc(field.documentation);
|
|
897
|
+
if (doc) traits["smithy.api#documentation"] = doc;
|
|
898
|
+
const wire = field.jsonName ?? camel(field.name);
|
|
899
|
+
if (wire !== tsName) traits["smithy.api#jsonName"] = wire;
|
|
900
|
+
if (isSensitive(field.name) || isSensitive(tsName)) {
|
|
901
|
+
traits["smithy.api#sensitive"] = {};
|
|
902
|
+
}
|
|
903
|
+
if (field.deprecated) traits["smithy.api#deprecated"] = {};
|
|
904
|
+
return traits;
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const emitMembers = (
|
|
908
|
+
ctx: EmitCtx,
|
|
909
|
+
fields: readonly ProtoField[],
|
|
910
|
+
scope: string,
|
|
911
|
+
): Record<string, any> => {
|
|
912
|
+
const members: Record<string, any> = {};
|
|
913
|
+
const used = new Set<string>();
|
|
914
|
+
for (const field of fields) {
|
|
915
|
+
let tsName = ident(camel(field.name));
|
|
916
|
+
let k = 2;
|
|
917
|
+
while (used.has(tsName)) tsName = `${ident(camel(field.name))}${k++}`;
|
|
918
|
+
used.add(tsName);
|
|
919
|
+
const traits = memberTraits(field, tsName);
|
|
920
|
+
members[tsName] = {
|
|
921
|
+
target: fieldTarget(ctx, field, scope),
|
|
922
|
+
...(Object.keys(traits).length ? { traits } : {}),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return members;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const emitMessage = (ctx: EmitCtx, msg: ProtoMessage): string => {
|
|
929
|
+
const cached = ctx.memo.get(msg.fullName);
|
|
930
|
+
if (cached) return cached;
|
|
931
|
+
const def: any = { type: "structure", members: {} };
|
|
932
|
+
const doc = oneLineDoc(msg.documentation);
|
|
933
|
+
if (doc) def.traits = { "smithy.api#documentation": doc };
|
|
934
|
+
const id = addShape(ctx.bag, shapeNameFor(msg.fullName), def);
|
|
935
|
+
ctx.memo.set(msg.fullName, id);
|
|
936
|
+
def.members = emitMembers(ctx, msg.fields, msg.fullName);
|
|
937
|
+
return id;
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const copyIo = (
|
|
941
|
+
ctx: EmitCtx,
|
|
942
|
+
rpcName: string,
|
|
943
|
+
side: "Request" | "Response",
|
|
944
|
+
typeName: string,
|
|
945
|
+
): string => {
|
|
946
|
+
if (SCALAR_TARGETS[typeName]) {
|
|
947
|
+
return addShape(ctx.bag, `${rpcName}${side}`, {
|
|
948
|
+
type: "structure",
|
|
949
|
+
members: {
|
|
950
|
+
value: {
|
|
951
|
+
target: SCALAR_TARGETS[typeName],
|
|
952
|
+
traits: { "smithy.api#required": {} },
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
traits: {
|
|
956
|
+
[side === "Request" ? "smithy.api#input" : "smithy.api#output"]: {},
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
const resolved = resolveName(typeName, ctx.pkg, undefined, ctx.index);
|
|
961
|
+
if (resolved === "google.protobuf.Empty") return PRELUDE.Unit;
|
|
962
|
+
if (WKT_TARGETS[resolved] && resolved !== "google.protobuf.Empty") {
|
|
963
|
+
return addShape(ctx.bag, `${rpcName}${side}`, {
|
|
964
|
+
type: "structure",
|
|
965
|
+
members: {
|
|
966
|
+
value: {
|
|
967
|
+
target: WKT_TARGETS[resolved],
|
|
968
|
+
traits: { "smithy.api#required": {} },
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
traits: {
|
|
972
|
+
[side === "Request" ? "smithy.api#input" : "smithy.api#output"]: {},
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
const en = ctx.index.enums.get(resolved);
|
|
977
|
+
if (en) {
|
|
978
|
+
return addShape(ctx.bag, `${rpcName}${side}`, {
|
|
979
|
+
type: "structure",
|
|
980
|
+
members: {
|
|
981
|
+
value: {
|
|
982
|
+
target: emitEnum(ctx, en),
|
|
983
|
+
traits: { "smithy.api#required": {} },
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
traits: {
|
|
987
|
+
[side === "Request" ? "smithy.api#input" : "smithy.api#output"]: {},
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
const msg = ctx.index.messages.get(resolved);
|
|
992
|
+
if (!msg)
|
|
993
|
+
throw new Error(
|
|
994
|
+
`rpc ${rpcName}: ${side} type ${typeName} is not a message`,
|
|
995
|
+
);
|
|
996
|
+
return addShape(ctx.bag, `${rpcName}${side}`, {
|
|
997
|
+
type: "structure",
|
|
998
|
+
members: emitMembers(ctx, msg.fields, msg.fullName),
|
|
999
|
+
traits: {
|
|
1000
|
+
[side === "Request" ? "smithy.api#input" : "smithy.api#output"]: {},
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/** Leading PascalCase token of an RPC name (`AppCreate` → `App`). */
|
|
1006
|
+
export const rpcGroupName = (rpcName: string): string => {
|
|
1007
|
+
const m = rpcName.match(/^([A-Z][a-z]+|[A-Z]+(?![a-z]))/);
|
|
1008
|
+
return m?.[1] ?? rpcName;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
export const convertProtoToSmithy = (
|
|
1012
|
+
options: ProtoConvertOptions,
|
|
1013
|
+
): ProtoConvertResult => {
|
|
1014
|
+
const index = indexFiles(options.files);
|
|
1015
|
+
const service = index.services.find(
|
|
1016
|
+
(s) => s.fullName === options.protoService,
|
|
1017
|
+
);
|
|
1018
|
+
if (!service) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`proto service ${options.protoService} not found (have ${index.services
|
|
1021
|
+
.map((s) => s.fullName)
|
|
1022
|
+
.join(", ")})`,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const file = options.files.find((f) =>
|
|
1026
|
+
f.services.some((s) => s.fullName === service.fullName),
|
|
1027
|
+
);
|
|
1028
|
+
const pkg =
|
|
1029
|
+
file?.package ?? service.fullName.split(".").slice(0, -1).join(".");
|
|
1030
|
+
|
|
1031
|
+
const bag: Bag = {
|
|
1032
|
+
shapes: {},
|
|
1033
|
+
names: new Set(),
|
|
1034
|
+
namespace: options.namespace,
|
|
1035
|
+
};
|
|
1036
|
+
const ctx: EmitCtx = {
|
|
1037
|
+
bag,
|
|
1038
|
+
index,
|
|
1039
|
+
pkg,
|
|
1040
|
+
memo: new Map(),
|
|
1041
|
+
listMemo: new Map(),
|
|
1042
|
+
mapMemo: new Map(),
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const skipStreaming = options.skipStreaming ?? true;
|
|
1046
|
+
const skipDeprecated = options.skipDeprecated ?? true;
|
|
1047
|
+
let converted = 0;
|
|
1048
|
+
let skippedStreaming = 0;
|
|
1049
|
+
let skippedDeprecated = 0;
|
|
1050
|
+
const serviceOps: Array<{ target: string }> = [];
|
|
1051
|
+
|
|
1052
|
+
// Reserve RPC local names so message/enum shapes that share an RPC's
|
|
1053
|
+
// PascalCase name (`AppDeploymentHistory`, `DomainList`) uniquify instead
|
|
1054
|
+
// of being overwritten when the operation is addExact'd. The Smithy
|
|
1055
|
+
// service shape is `${serviceName}Service` for the same reason (`Image`
|
|
1056
|
+
// the message vs Image the RPC group).
|
|
1057
|
+
const serviceShapeName = options.serviceName.endsWith("Service")
|
|
1058
|
+
? options.serviceName
|
|
1059
|
+
: `${options.serviceName}Service`;
|
|
1060
|
+
bag.names.add(serviceShapeName);
|
|
1061
|
+
for (const rpc of service.rpcs) {
|
|
1062
|
+
if (options.rpcNames && !options.rpcNames.has(rpc.name)) continue;
|
|
1063
|
+
bag.names.add(rpc.name);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for (const rpc of service.rpcs) {
|
|
1067
|
+
if (options.rpcNames && !options.rpcNames.has(rpc.name)) continue;
|
|
1068
|
+
if (rpc.requestStream || rpc.responseStream) {
|
|
1069
|
+
if (skipStreaming) {
|
|
1070
|
+
skippedStreaming++;
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (rpc.deprecated && skipDeprecated) {
|
|
1075
|
+
skippedDeprecated++;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const input = copyIo(ctx, rpc.name, "Request", rpc.requestType);
|
|
1080
|
+
const output = copyIo(ctx, rpc.name, "Response", rpc.responseType);
|
|
1081
|
+
const httpUri = `/${service.fullName}/${rpc.name}`;
|
|
1082
|
+
const traits: Record<string, any> = {
|
|
1083
|
+
"smithy.api#http": { method: "POST", uri: httpUri, code: 200 },
|
|
1084
|
+
};
|
|
1085
|
+
const doc = oneLineDoc(rpc.documentation);
|
|
1086
|
+
if (doc) traits["smithy.api#documentation"] = doc;
|
|
1087
|
+
if (rpc.deprecated) traits["smithy.api#deprecated"] = {};
|
|
1088
|
+
|
|
1089
|
+
const opId = addExact(bag, rpc.name, {
|
|
1090
|
+
type: "operation",
|
|
1091
|
+
input: { target: input },
|
|
1092
|
+
output: { target: output },
|
|
1093
|
+
traits,
|
|
1094
|
+
});
|
|
1095
|
+
serviceOps.push({ target: opId });
|
|
1096
|
+
converted++;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
addExact(bag, serviceShapeName, {
|
|
1100
|
+
type: "service",
|
|
1101
|
+
version: options.serviceVersion ?? "1.0",
|
|
1102
|
+
operations: serviceOps,
|
|
1103
|
+
traits: {
|
|
1104
|
+
"smithy.api#title": options.serviceTitle ?? options.serviceName,
|
|
1105
|
+
...(options.serviceDocumentation
|
|
1106
|
+
? { "smithy.api#documentation": options.serviceDocumentation }
|
|
1107
|
+
: {}),
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
return {
|
|
1112
|
+
model: {
|
|
1113
|
+
smithy: "2.0",
|
|
1114
|
+
metadata: {
|
|
1115
|
+
suppressions: [
|
|
1116
|
+
{ id: "HttpUriConflict", namespace: "*" },
|
|
1117
|
+
{ id: "HttpMethodSemantics", namespace: "*" },
|
|
1118
|
+
{ id: "UnreferencedShape", namespace: "*" },
|
|
1119
|
+
],
|
|
1120
|
+
},
|
|
1121
|
+
shapes: bag.shapes,
|
|
1122
|
+
},
|
|
1123
|
+
converted,
|
|
1124
|
+
skippedStreaming,
|
|
1125
|
+
skippedDeprecated,
|
|
1126
|
+
shapeCount: Object.keys(bag.shapes).length,
|
|
1127
|
+
};
|
|
1128
|
+
};
|