@zero-server/sdk 0.9.1 → 0.9.2
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 +21 -21
- package/README.md +460 -443
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +465 -465
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +137 -137
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +255 -255
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/grpc/proto.js
CHANGED
|
@@ -1,821 +1,821 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module grpc/proto
|
|
3
|
-
* @description Zero-dependency proto3 parser — reads `.proto` file text and produces
|
|
4
|
-
* message descriptors, enum definitions, and service/RPC declarations
|
|
5
|
-
* that the codec and server use at runtime.
|
|
6
|
-
*
|
|
7
|
-
* Supports:
|
|
8
|
-
* - `syntax = "proto3";`
|
|
9
|
-
* - `package`, `option`, `import` (recorded but not resolved)
|
|
10
|
-
* - Scalar types, enums, nested messages, `oneof`, `map`, `repeated`
|
|
11
|
-
* - Services with unary, server-streaming, client-streaming, and bidi RPCs
|
|
12
|
-
* - Comments (// and /* ... */)
|
|
13
|
-
* - Reserved fields and field options like `[deprecated = true]`
|
|
14
|
-
*
|
|
15
|
-
* @see https://protobuf.dev/programming-guides/proto3/
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* const { parseProto } = require('./proto');
|
|
19
|
-
* const schema = parseProto(fs.readFileSync('chat.proto', 'utf8'));
|
|
20
|
-
* // schema.messages — { MessageName: { fields: [...] } }
|
|
21
|
-
* // schema.enums — { EnumName: { values: { ... } } }
|
|
22
|
-
* // schema.services — { ServiceName: { methods: { ... } } }
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const fs = require('fs');
|
|
26
|
-
const path = require('path');
|
|
27
|
-
const log = require('../debug')('zero:grpc');
|
|
28
|
-
|
|
29
|
-
// -- Token Types -------------------------------------------
|
|
30
|
-
|
|
31
|
-
/** @private */
|
|
32
|
-
const TOK = {
|
|
33
|
-
IDENT: 'IDENT',
|
|
34
|
-
NUMBER: 'NUMBER',
|
|
35
|
-
STRING: 'STRING',
|
|
36
|
-
SYMBOL: 'SYMBOL',
|
|
37
|
-
EOF: 'EOF',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// -- Lexer -------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Tokenize proto3 source text.
|
|
44
|
-
* @private
|
|
45
|
-
* @param {string} source
|
|
46
|
-
* @returns {{ type: string, value: string, line: number }[]}
|
|
47
|
-
*/
|
|
48
|
-
function tokenize(source)
|
|
49
|
-
{
|
|
50
|
-
const tokens = [];
|
|
51
|
-
let i = 0;
|
|
52
|
-
let line = 1;
|
|
53
|
-
|
|
54
|
-
while (i < source.length)
|
|
55
|
-
{
|
|
56
|
-
const ch = source[i];
|
|
57
|
-
|
|
58
|
-
// Newlines
|
|
59
|
-
if (ch === '\n') { line++; i++; continue; }
|
|
60
|
-
if (ch === '\r') { i++; continue; }
|
|
61
|
-
|
|
62
|
-
// Whitespace
|
|
63
|
-
if (ch === ' ' || ch === '\t') { i++; continue; }
|
|
64
|
-
|
|
65
|
-
// Single-line comment
|
|
66
|
-
if (ch === '/' && source[i + 1] === '/')
|
|
67
|
-
{
|
|
68
|
-
while (i < source.length && source[i] !== '\n') i++;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Block comment
|
|
73
|
-
if (ch === '/' && source[i + 1] === '*')
|
|
74
|
-
{
|
|
75
|
-
i += 2;
|
|
76
|
-
while (i < source.length - 1)
|
|
77
|
-
{
|
|
78
|
-
if (source[i] === '\n') line++;
|
|
79
|
-
if (source[i] === '*' && source[i + 1] === '/')
|
|
80
|
-
{
|
|
81
|
-
i += 2;
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
i++;
|
|
85
|
-
}
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// String literal
|
|
90
|
-
if (ch === '"' || ch === "'")
|
|
91
|
-
{
|
|
92
|
-
const quote = ch;
|
|
93
|
-
let str = '';
|
|
94
|
-
i++;
|
|
95
|
-
while (i < source.length && source[i] !== quote)
|
|
96
|
-
{
|
|
97
|
-
if (source[i] === '\\' && i + 1 < source.length)
|
|
98
|
-
{
|
|
99
|
-
const esc = source[i + 1];
|
|
100
|
-
if (esc === 'n') str += '\n';
|
|
101
|
-
else if (esc === 't') str += '\t';
|
|
102
|
-
else if (esc === '\\') str += '\\';
|
|
103
|
-
else if (esc === quote) str += quote;
|
|
104
|
-
else str += esc;
|
|
105
|
-
i += 2;
|
|
106
|
-
}
|
|
107
|
-
else
|
|
108
|
-
{
|
|
109
|
-
str += source[i++];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
i++; // skip closing quote
|
|
113
|
-
tokens.push({ type: TOK.STRING, value: str, line });
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Number (integer or float, including negative)
|
|
118
|
-
if ((ch >= '0' && ch <= '9') || (ch === '-' && source[i + 1] >= '0' && source[i + 1] <= '9'))
|
|
119
|
-
{
|
|
120
|
-
let num = '';
|
|
121
|
-
if (ch === '-') { num = '-'; i++; }
|
|
122
|
-
|
|
123
|
-
// Hex
|
|
124
|
-
if (source[i] === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X'))
|
|
125
|
-
{
|
|
126
|
-
num += '0x'; i += 2;
|
|
127
|
-
while (i < source.length && /[0-9a-fA-F]/.test(source[i])) num += source[i++];
|
|
128
|
-
}
|
|
129
|
-
else
|
|
130
|
-
{
|
|
131
|
-
while (i < source.length && ((source[i] >= '0' && source[i] <= '9') || source[i] === '.' || source[i] === 'e' || source[i] === 'E' || source[i] === '+' || source[i] === '-'))
|
|
132
|
-
{
|
|
133
|
-
// Avoid consuming the next field's minus sign
|
|
134
|
-
if ((source[i] === '+' || source[i] === '-') && source[i - 1] !== 'e' && source[i - 1] !== 'E') break;
|
|
135
|
-
num += source[i++];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
tokens.push({ type: TOK.NUMBER, value: num, line });
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Identifier or keyword
|
|
143
|
-
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_')
|
|
144
|
-
{
|
|
145
|
-
let ident = '';
|
|
146
|
-
while (i < source.length && ((source[i] >= 'a' && source[i] <= 'z') || (source[i] >= 'A' && source[i] <= 'Z') || (source[i] >= '0' && source[i] <= '9') || source[i] === '_' || source[i] === '.'))
|
|
147
|
-
{
|
|
148
|
-
ident += source[i++];
|
|
149
|
-
}
|
|
150
|
-
tokens.push({ type: TOK.IDENT, value: ident, line });
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Symbols: { } ( ) ; = , < > [ ]
|
|
155
|
-
tokens.push({ type: TOK.SYMBOL, value: ch, line });
|
|
156
|
-
i++;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
tokens.push({ type: TOK.EOF, value: '', line });
|
|
160
|
-
return tokens;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// -- Parser ------------------------------------------------
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Parse a proto3 source string into a structured schema.
|
|
167
|
-
*
|
|
168
|
-
* @param {string} source - Proto3 source text.
|
|
169
|
-
* @param {object} [opts] - Parser options.
|
|
170
|
-
* @param {string} [opts.filename] - File name for error messages.
|
|
171
|
-
* @param {string} [opts.basePath] - Base directory for resolving imports.
|
|
172
|
-
* @param {boolean} [opts.resolveImports=false] - Whether to recursively parse imported files.
|
|
173
|
-
* @returns {ProtoSchema} Parsed schema with messages, enums, and services.
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* const schema = parseProto('syntax = "proto3"; message Ping { string msg = 1; }');
|
|
177
|
-
* schema.messages.Ping.fields[0].name; // 'msg'
|
|
178
|
-
* schema.messages.Ping.fields[0].type; // 'string'
|
|
179
|
-
*/
|
|
180
|
-
function parseProto(source, opts = {})
|
|
181
|
-
{
|
|
182
|
-
const tokens = tokenize(source);
|
|
183
|
-
let pos = 0;
|
|
184
|
-
const filename = opts.filename || '<inline>';
|
|
185
|
-
|
|
186
|
-
const schema = {
|
|
187
|
-
syntax: 'proto3',
|
|
188
|
-
package: '',
|
|
189
|
-
imports: [],
|
|
190
|
-
options: {},
|
|
191
|
-
messages: {},
|
|
192
|
-
enums: {},
|
|
193
|
-
services: {},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get current token.
|
|
198
|
-
* @private
|
|
199
|
-
*/
|
|
200
|
-
function peek()
|
|
201
|
-
{
|
|
202
|
-
return tokens[pos];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Consume and return the current token.
|
|
207
|
-
* @private
|
|
208
|
-
*/
|
|
209
|
-
function next()
|
|
210
|
-
{
|
|
211
|
-
return tokens[pos++];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Expect a specific token value or type.
|
|
216
|
-
* @private
|
|
217
|
-
*/
|
|
218
|
-
function expect(value, type)
|
|
219
|
-
{
|
|
220
|
-
const tok = next();
|
|
221
|
-
if (type && tok.type !== type)
|
|
222
|
-
throw new SyntaxError(`${filename}:${tok.line}: expected ${type} "${value}", got ${tok.type} "${tok.value}"`);
|
|
223
|
-
if (value && tok.value !== value)
|
|
224
|
-
throw new SyntaxError(`${filename}:${tok.line}: expected "${value}", got "${tok.value}"`);
|
|
225
|
-
return tok;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Check if current token matches value.
|
|
230
|
-
* @private
|
|
231
|
-
*/
|
|
232
|
-
function match(value)
|
|
233
|
-
{
|
|
234
|
-
return peek().value === value;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Consume if current token matches value.
|
|
239
|
-
* @private
|
|
240
|
-
*/
|
|
241
|
-
function eat(value)
|
|
242
|
-
{
|
|
243
|
-
if (match(value)) { next(); return true; }
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// -- Top-Level Parsing ---------------------------------
|
|
248
|
-
|
|
249
|
-
while (peek().type !== TOK.EOF)
|
|
250
|
-
{
|
|
251
|
-
const tok = peek();
|
|
252
|
-
|
|
253
|
-
if (tok.value === 'syntax')
|
|
254
|
-
{
|
|
255
|
-
next(); expect('='); schema.syntax = next().value; expect(';');
|
|
256
|
-
if (schema.syntax !== 'proto3')
|
|
257
|
-
log.warn('proto file uses syntax "%s" — only proto3 is fully supported', schema.syntax);
|
|
258
|
-
}
|
|
259
|
-
else if (tok.value === 'package')
|
|
260
|
-
{
|
|
261
|
-
next(); schema.package = next().value; expect(';');
|
|
262
|
-
}
|
|
263
|
-
else if (tok.value === 'import')
|
|
264
|
-
{
|
|
265
|
-
next();
|
|
266
|
-
const weak = eat('weak');
|
|
267
|
-
const pub = eat('public');
|
|
268
|
-
const importPath = next().value;
|
|
269
|
-
expect(';');
|
|
270
|
-
schema.imports.push({ path: importPath, weak, public: pub });
|
|
271
|
-
|
|
272
|
-
if (opts.resolveImports && opts.basePath)
|
|
273
|
-
{
|
|
274
|
-
try
|
|
275
|
-
{
|
|
276
|
-
const fullPath = path.resolve(opts.basePath, importPath);
|
|
277
|
-
const importSource = fs.readFileSync(fullPath, 'utf8');
|
|
278
|
-
const importSchema = parseProto(importSource, {
|
|
279
|
-
filename: importPath,
|
|
280
|
-
basePath: path.dirname(fullPath),
|
|
281
|
-
resolveImports: true,
|
|
282
|
-
});
|
|
283
|
-
// Merge imported definitions
|
|
284
|
-
Object.assign(schema.messages, importSchema.messages);
|
|
285
|
-
Object.assign(schema.enums, importSchema.enums);
|
|
286
|
-
Object.assign(schema.services, importSchema.services);
|
|
287
|
-
}
|
|
288
|
-
catch (err)
|
|
289
|
-
{
|
|
290
|
-
log.warn('failed to resolve import "%s": %s', importPath, err.message);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
else if (tok.value === 'option')
|
|
295
|
-
{
|
|
296
|
-
next(); _parseOption(schema.options);
|
|
297
|
-
}
|
|
298
|
-
else if (tok.value === 'message')
|
|
299
|
-
{
|
|
300
|
-
next();
|
|
301
|
-
const name = next().value;
|
|
302
|
-
schema.messages[name] = _parseMessage(name);
|
|
303
|
-
}
|
|
304
|
-
else if (tok.value === 'enum')
|
|
305
|
-
{
|
|
306
|
-
next();
|
|
307
|
-
const name = next().value;
|
|
308
|
-
schema.enums[name] = _parseEnum(name);
|
|
309
|
-
}
|
|
310
|
-
else if (tok.value === 'service')
|
|
311
|
-
{
|
|
312
|
-
next();
|
|
313
|
-
const name = next().value;
|
|
314
|
-
schema.services[name] = _parseService(name);
|
|
315
|
-
}
|
|
316
|
-
else if (tok.value === ';')
|
|
317
|
-
{
|
|
318
|
-
next(); // skip stray semicolons
|
|
319
|
-
}
|
|
320
|
-
else
|
|
321
|
-
{
|
|
322
|
-
throw new SyntaxError(`${filename}:${tok.line}: unexpected token "${tok.value}"`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Link enum definitions into message fields
|
|
327
|
-
_linkEnums(schema);
|
|
328
|
-
|
|
329
|
-
log.info('parsed %s: %d messages, %d enums, %d services',
|
|
330
|
-
filename,
|
|
331
|
-
Object.keys(schema.messages).length,
|
|
332
|
-
Object.keys(schema.enums).length,
|
|
333
|
-
Object.keys(schema.services).length,
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
return schema;
|
|
337
|
-
|
|
338
|
-
// -- Helper Functions (closures over peek/next/expect/match/eat) --
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Parse a message definition (including nested messages and enums).
|
|
342
|
-
* @param {string} msgName
|
|
343
|
-
* @returns {object}
|
|
344
|
-
*/
|
|
345
|
-
function _parseMessage(msgName)
|
|
346
|
-
{
|
|
347
|
-
const msg = { name: msgName, fields: [], oneofs: {}, nested: {}, nestedEnums: {}, options: {} };
|
|
348
|
-
|
|
349
|
-
expect('{');
|
|
350
|
-
|
|
351
|
-
while (!match('}'))
|
|
352
|
-
{
|
|
353
|
-
const tok = peek();
|
|
354
|
-
|
|
355
|
-
if (tok.value === 'message')
|
|
356
|
-
{
|
|
357
|
-
next();
|
|
358
|
-
const nestedName = next().value;
|
|
359
|
-
msg.nested[nestedName] = _parseMessage(nestedName);
|
|
360
|
-
}
|
|
361
|
-
else if (tok.value === 'enum')
|
|
362
|
-
{
|
|
363
|
-
next();
|
|
364
|
-
const enumName = next().value;
|
|
365
|
-
msg.nestedEnums[enumName] = _parseEnum(enumName);
|
|
366
|
-
}
|
|
367
|
-
else if (tok.value === 'oneof')
|
|
368
|
-
{
|
|
369
|
-
next();
|
|
370
|
-
const oneofName = next().value;
|
|
371
|
-
msg.oneofs[oneofName] = _parseOneof(oneofName, msg.fields);
|
|
372
|
-
}
|
|
373
|
-
else if (tok.value === 'map')
|
|
374
|
-
{
|
|
375
|
-
_parseMapField(msg.fields);
|
|
376
|
-
}
|
|
377
|
-
else if (tok.value === 'reserved')
|
|
378
|
-
{
|
|
379
|
-
_parseReserved();
|
|
380
|
-
}
|
|
381
|
-
else if (tok.value === 'option')
|
|
382
|
-
{
|
|
383
|
-
next(); _parseOption(msg.options);
|
|
384
|
-
}
|
|
385
|
-
else if (tok.value === ';')
|
|
386
|
-
{
|
|
387
|
-
next();
|
|
388
|
-
}
|
|
389
|
-
else
|
|
390
|
-
{
|
|
391
|
-
_parseField(msg.fields);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
expect('}');
|
|
396
|
-
if (peek().value === ';') next();
|
|
397
|
-
|
|
398
|
-
return msg;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Parse a field declaration.
|
|
403
|
-
* @param {object[]} fieldsArray
|
|
404
|
-
*/
|
|
405
|
-
function _parseField(fieldsArray)
|
|
406
|
-
{
|
|
407
|
-
let repeated = false;
|
|
408
|
-
let optional = false;
|
|
409
|
-
|
|
410
|
-
if (match('repeated')) { next(); repeated = true; }
|
|
411
|
-
else if (match('optional')) { next(); optional = true; }
|
|
412
|
-
|
|
413
|
-
const type = next().value;
|
|
414
|
-
const fName = next().value;
|
|
415
|
-
expect('=');
|
|
416
|
-
const number = parseInt(next().value, 10);
|
|
417
|
-
|
|
418
|
-
const fieldOpts = {};
|
|
419
|
-
if (match('['))
|
|
420
|
-
{
|
|
421
|
-
next();
|
|
422
|
-
while (!match(']'))
|
|
423
|
-
{
|
|
424
|
-
const optName = next().value;
|
|
425
|
-
expect('=');
|
|
426
|
-
const optVal = next().value;
|
|
427
|
-
fieldOpts[optName] = optVal;
|
|
428
|
-
eat(',');
|
|
429
|
-
}
|
|
430
|
-
expect(']');
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
expect(';');
|
|
434
|
-
|
|
435
|
-
fieldsArray.push({
|
|
436
|
-
name: fName,
|
|
437
|
-
type,
|
|
438
|
-
number,
|
|
439
|
-
repeated,
|
|
440
|
-
optional,
|
|
441
|
-
map: false,
|
|
442
|
-
options: fieldOpts,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Parse a map field: `map<KeyType, ValueType> name = N;`
|
|
448
|
-
* @param {object[]} fieldsArray
|
|
449
|
-
*/
|
|
450
|
-
function _parseMapField(fieldsArray)
|
|
451
|
-
{
|
|
452
|
-
expect('map');
|
|
453
|
-
expect('<');
|
|
454
|
-
const keyType = next().value;
|
|
455
|
-
expect(',');
|
|
456
|
-
const valueType = next().value;
|
|
457
|
-
expect('>');
|
|
458
|
-
const fName = next().value;
|
|
459
|
-
expect('=');
|
|
460
|
-
const number = parseInt(next().value, 10);
|
|
461
|
-
|
|
462
|
-
const fieldOpts = {};
|
|
463
|
-
if (match('['))
|
|
464
|
-
{
|
|
465
|
-
next();
|
|
466
|
-
while (!match(']'))
|
|
467
|
-
{
|
|
468
|
-
const optName = next().value;
|
|
469
|
-
expect('=');
|
|
470
|
-
const optVal = next().value;
|
|
471
|
-
fieldOpts[optName] = optVal;
|
|
472
|
-
eat(',');
|
|
473
|
-
}
|
|
474
|
-
expect(']');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
expect(';');
|
|
478
|
-
|
|
479
|
-
fieldsArray.push({
|
|
480
|
-
name: fName,
|
|
481
|
-
type: `map<${keyType},${valueType}>`,
|
|
482
|
-
keyType,
|
|
483
|
-
valueType,
|
|
484
|
-
number,
|
|
485
|
-
repeated: false,
|
|
486
|
-
optional: false,
|
|
487
|
-
map: true,
|
|
488
|
-
mapKeyType: keyType,
|
|
489
|
-
mapValueType: valueType,
|
|
490
|
-
options: fieldOpts,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Parse oneof block.
|
|
496
|
-
* @param {string} oneofName
|
|
497
|
-
* @param {object[]} fieldsArray
|
|
498
|
-
* @returns {string[]}
|
|
499
|
-
*/
|
|
500
|
-
function _parseOneof(oneofName, fieldsArray)
|
|
501
|
-
{
|
|
502
|
-
const fieldNames = [];
|
|
503
|
-
expect('{');
|
|
504
|
-
|
|
505
|
-
while (!match('}'))
|
|
506
|
-
{
|
|
507
|
-
if (match('option'))
|
|
508
|
-
{
|
|
509
|
-
next(); _parseOption({});
|
|
510
|
-
}
|
|
511
|
-
else if (match(';'))
|
|
512
|
-
{
|
|
513
|
-
next();
|
|
514
|
-
}
|
|
515
|
-
else
|
|
516
|
-
{
|
|
517
|
-
const type = next().value;
|
|
518
|
-
const fName = next().value;
|
|
519
|
-
expect('=');
|
|
520
|
-
const number = parseInt(next().value, 10);
|
|
521
|
-
|
|
522
|
-
const fieldOpts = {};
|
|
523
|
-
if (match('['))
|
|
524
|
-
{
|
|
525
|
-
next();
|
|
526
|
-
while (!match(']'))
|
|
527
|
-
{
|
|
528
|
-
const optName = next().value;
|
|
529
|
-
expect('=');
|
|
530
|
-
const optVal = next().value;
|
|
531
|
-
fieldOpts[optName] = optVal;
|
|
532
|
-
eat(',');
|
|
533
|
-
}
|
|
534
|
-
expect(']');
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
expect(';');
|
|
538
|
-
|
|
539
|
-
fieldsArray.push({
|
|
540
|
-
name: fName,
|
|
541
|
-
type,
|
|
542
|
-
number,
|
|
543
|
-
repeated: false,
|
|
544
|
-
optional: false,
|
|
545
|
-
map: false,
|
|
546
|
-
oneofName,
|
|
547
|
-
options: fieldOpts,
|
|
548
|
-
});
|
|
549
|
-
fieldNames.push(fName);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
expect('}');
|
|
554
|
-
return fieldNames;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Parse an enum definition.
|
|
559
|
-
* @param {string} eName
|
|
560
|
-
* @returns {object}
|
|
561
|
-
*/
|
|
562
|
-
function _parseEnum(eName)
|
|
563
|
-
{
|
|
564
|
-
const enumDef = { name: eName, values: {}, options: {} };
|
|
565
|
-
expect('{');
|
|
566
|
-
|
|
567
|
-
while (!match('}'))
|
|
568
|
-
{
|
|
569
|
-
if (match('option'))
|
|
570
|
-
{
|
|
571
|
-
next(); _parseOption(enumDef.options);
|
|
572
|
-
}
|
|
573
|
-
else if (match('reserved'))
|
|
574
|
-
{
|
|
575
|
-
_parseReserved();
|
|
576
|
-
}
|
|
577
|
-
else if (match(';'))
|
|
578
|
-
{
|
|
579
|
-
next();
|
|
580
|
-
}
|
|
581
|
-
else
|
|
582
|
-
{
|
|
583
|
-
const valueName = next().value;
|
|
584
|
-
expect('=');
|
|
585
|
-
const valueNumber = parseInt(next().value, 10);
|
|
586
|
-
|
|
587
|
-
if (match('['))
|
|
588
|
-
{
|
|
589
|
-
next();
|
|
590
|
-
while (!match(']'))
|
|
591
|
-
{
|
|
592
|
-
next(); eat('='); next(); eat(',');
|
|
593
|
-
}
|
|
594
|
-
expect(']');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
expect(';');
|
|
598
|
-
enumDef.values[valueName] = valueNumber;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
expect('}');
|
|
603
|
-
if (peek().value === ';') next();
|
|
604
|
-
|
|
605
|
-
return enumDef;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Parse a service definition with RPC methods.
|
|
610
|
-
* @param {string} sName
|
|
611
|
-
* @returns {object}
|
|
612
|
-
*/
|
|
613
|
-
function _parseService(sName)
|
|
614
|
-
{
|
|
615
|
-
const service = { name: sName, methods: {}, options: {} };
|
|
616
|
-
expect('{');
|
|
617
|
-
|
|
618
|
-
while (!match('}'))
|
|
619
|
-
{
|
|
620
|
-
if (match('rpc'))
|
|
621
|
-
{
|
|
622
|
-
next();
|
|
623
|
-
const methodName = next().value;
|
|
624
|
-
expect('(');
|
|
625
|
-
const clientStreaming = eat('stream');
|
|
626
|
-
const inputType = next().value;
|
|
627
|
-
expect(')');
|
|
628
|
-
expect('returns');
|
|
629
|
-
expect('(');
|
|
630
|
-
const serverStreaming = eat('stream');
|
|
631
|
-
const outputType = next().value;
|
|
632
|
-
expect(')');
|
|
633
|
-
|
|
634
|
-
const methodOpts = {};
|
|
635
|
-
if (match('{'))
|
|
636
|
-
{
|
|
637
|
-
next();
|
|
638
|
-
while (!match('}'))
|
|
639
|
-
{
|
|
640
|
-
if (match('option'))
|
|
641
|
-
{
|
|
642
|
-
next(); _parseOption(methodOpts);
|
|
643
|
-
}
|
|
644
|
-
else
|
|
645
|
-
{
|
|
646
|
-
next();
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
expect('}');
|
|
650
|
-
}
|
|
651
|
-
else
|
|
652
|
-
{
|
|
653
|
-
expect(';');
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
service.methods[methodName] = {
|
|
657
|
-
name: methodName,
|
|
658
|
-
inputType,
|
|
659
|
-
outputType,
|
|
660
|
-
clientStreaming: !!clientStreaming,
|
|
661
|
-
serverStreaming: !!serverStreaming,
|
|
662
|
-
options: methodOpts,
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
else if (match('option'))
|
|
666
|
-
{
|
|
667
|
-
next(); _parseOption(service.options);
|
|
668
|
-
}
|
|
669
|
-
else if (match(';'))
|
|
670
|
-
{
|
|
671
|
-
next();
|
|
672
|
-
}
|
|
673
|
-
else
|
|
674
|
-
{
|
|
675
|
-
next();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
expect('}');
|
|
680
|
-
if (peek().value === ';') next();
|
|
681
|
-
|
|
682
|
-
return service;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Parse an option statement: `option name = value;`
|
|
687
|
-
* @param {object} target
|
|
688
|
-
*/
|
|
689
|
-
function _parseOption(target)
|
|
690
|
-
{
|
|
691
|
-
let optName = '';
|
|
692
|
-
if (match('('))
|
|
693
|
-
{
|
|
694
|
-
next();
|
|
695
|
-
optName = '(' + next().value + ')';
|
|
696
|
-
expect(')');
|
|
697
|
-
}
|
|
698
|
-
else
|
|
699
|
-
{
|
|
700
|
-
optName = next().value;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
while (match('.'))
|
|
704
|
-
{
|
|
705
|
-
next();
|
|
706
|
-
optName += '.' + next().value;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
expect('=');
|
|
710
|
-
const value = next().value;
|
|
711
|
-
expect(';');
|
|
712
|
-
target[optName] = value;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Skip a `reserved` statement.
|
|
717
|
-
*/
|
|
718
|
-
function _parseReserved()
|
|
719
|
-
{
|
|
720
|
-
next(); // consume 'reserved'
|
|
721
|
-
while (peek().value !== ';' && peek().type !== TOK.EOF) next();
|
|
722
|
-
if (peek().value === ';') next();
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// -- Post-Processing --------------------------------------
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Link enum definitions into message fields so the codec knows how to encode/decode them.
|
|
730
|
-
* Also flattens nested messages and enums into the top-level maps for easy lookup.
|
|
731
|
-
* @private
|
|
732
|
-
* @param {object} schema
|
|
733
|
-
*/
|
|
734
|
-
function _linkEnums(schema)
|
|
735
|
-
{
|
|
736
|
-
// Flatten nested messages and enums
|
|
737
|
-
const flatMsgs = {};
|
|
738
|
-
const flatEnums = {};
|
|
739
|
-
|
|
740
|
-
function flatten(messages, enums, prefix)
|
|
741
|
-
{
|
|
742
|
-
for (const [name, msg] of Object.entries(messages))
|
|
743
|
-
{
|
|
744
|
-
const fullName = prefix ? `${prefix}.${name}` : name;
|
|
745
|
-
flatMsgs[fullName] = msg;
|
|
746
|
-
flatMsgs[name] = msg; // also store short name for convenience
|
|
747
|
-
|
|
748
|
-
if (msg.nested) flatten(msg.nested, {}, fullName);
|
|
749
|
-
if (msg.nestedEnums)
|
|
750
|
-
{
|
|
751
|
-
for (const [eName, eDef] of Object.entries(msg.nestedEnums))
|
|
752
|
-
{
|
|
753
|
-
const fullEnum = prefix ? `${prefix}.${name}.${eName}` : `${name}.${eName}`;
|
|
754
|
-
flatEnums[fullEnum] = eDef;
|
|
755
|
-
flatEnums[eName] = eDef; // short name
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
for (const [name, def] of Object.entries(enums))
|
|
761
|
-
{
|
|
762
|
-
const fullName = prefix ? `${prefix}.${name}` : name;
|
|
763
|
-
flatEnums[fullName] = def;
|
|
764
|
-
flatEnums[name] = def;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
flatten(schema.messages, schema.enums, schema.package);
|
|
769
|
-
|
|
770
|
-
// Merge flattened into schema
|
|
771
|
-
Object.assign(schema.messages, flatMsgs);
|
|
772
|
-
Object.assign(schema.enums, flatEnums);
|
|
773
|
-
|
|
774
|
-
// Link enum types into message fields
|
|
775
|
-
for (const msg of Object.values(schema.messages))
|
|
776
|
-
{
|
|
777
|
-
if (!msg.fields) continue;
|
|
778
|
-
for (const field of msg.fields)
|
|
779
|
-
{
|
|
780
|
-
if (!field.map && schema.enums[field.type])
|
|
781
|
-
{
|
|
782
|
-
field.enumDef = schema.enums[field.type];
|
|
783
|
-
}
|
|
784
|
-
// Map value enums
|
|
785
|
-
if (field.map && schema.enums[field.valueType])
|
|
786
|
-
{
|
|
787
|
-
field.enumDef = schema.enums[field.valueType];
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// -- File Loader -------------------------------------------
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Parse a `.proto` file from disk.
|
|
797
|
-
*
|
|
798
|
-
* @param {string} filePath - Path to the `.proto` file.
|
|
799
|
-
* @param {object} [opts] - Parser options.
|
|
800
|
-
* @param {boolean} [opts.resolveImports=false] - Whether to recursively resolve imports.
|
|
801
|
-
* @returns {ProtoSchema} Parsed schema.
|
|
802
|
-
*
|
|
803
|
-
* @example
|
|
804
|
-
* const schema = parseProtoFile('./protos/chat.proto');
|
|
805
|
-
*/
|
|
806
|
-
function parseProtoFile(filePath, opts = {})
|
|
807
|
-
{
|
|
808
|
-
const resolved = path.resolve(filePath);
|
|
809
|
-
const source = fs.readFileSync(resolved, 'utf8');
|
|
810
|
-
return parseProto(source, {
|
|
811
|
-
filename: path.basename(resolved),
|
|
812
|
-
basePath: path.dirname(resolved),
|
|
813
|
-
...opts,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
module.exports = {
|
|
818
|
-
parseProto,
|
|
819
|
-
parseProtoFile,
|
|
820
|
-
tokenize,
|
|
821
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/proto
|
|
3
|
+
* @description Zero-dependency proto3 parser — reads `.proto` file text and produces
|
|
4
|
+
* message descriptors, enum definitions, and service/RPC declarations
|
|
5
|
+
* that the codec and server use at runtime.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - `syntax = "proto3";`
|
|
9
|
+
* - `package`, `option`, `import` (recorded but not resolved)
|
|
10
|
+
* - Scalar types, enums, nested messages, `oneof`, `map`, `repeated`
|
|
11
|
+
* - Services with unary, server-streaming, client-streaming, and bidi RPCs
|
|
12
|
+
* - Comments (// and /* ... */)
|
|
13
|
+
* - Reserved fields and field options like `[deprecated = true]`
|
|
14
|
+
*
|
|
15
|
+
* @see https://protobuf.dev/programming-guides/proto3/
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { parseProto } = require('./proto');
|
|
19
|
+
* const schema = parseProto(fs.readFileSync('chat.proto', 'utf8'));
|
|
20
|
+
* // schema.messages — { MessageName: { fields: [...] } }
|
|
21
|
+
* // schema.enums — { EnumName: { values: { ... } } }
|
|
22
|
+
* // schema.services — { ServiceName: { methods: { ... } } }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const log = require('../debug')('zero:grpc');
|
|
28
|
+
|
|
29
|
+
// -- Token Types -------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** @private */
|
|
32
|
+
const TOK = {
|
|
33
|
+
IDENT: 'IDENT',
|
|
34
|
+
NUMBER: 'NUMBER',
|
|
35
|
+
STRING: 'STRING',
|
|
36
|
+
SYMBOL: 'SYMBOL',
|
|
37
|
+
EOF: 'EOF',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// -- Lexer -------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tokenize proto3 source text.
|
|
44
|
+
* @private
|
|
45
|
+
* @param {string} source
|
|
46
|
+
* @returns {{ type: string, value: string, line: number }[]}
|
|
47
|
+
*/
|
|
48
|
+
function tokenize(source)
|
|
49
|
+
{
|
|
50
|
+
const tokens = [];
|
|
51
|
+
let i = 0;
|
|
52
|
+
let line = 1;
|
|
53
|
+
|
|
54
|
+
while (i < source.length)
|
|
55
|
+
{
|
|
56
|
+
const ch = source[i];
|
|
57
|
+
|
|
58
|
+
// Newlines
|
|
59
|
+
if (ch === '\n') { line++; i++; continue; }
|
|
60
|
+
if (ch === '\r') { i++; continue; }
|
|
61
|
+
|
|
62
|
+
// Whitespace
|
|
63
|
+
if (ch === ' ' || ch === '\t') { i++; continue; }
|
|
64
|
+
|
|
65
|
+
// Single-line comment
|
|
66
|
+
if (ch === '/' && source[i + 1] === '/')
|
|
67
|
+
{
|
|
68
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Block comment
|
|
73
|
+
if (ch === '/' && source[i + 1] === '*')
|
|
74
|
+
{
|
|
75
|
+
i += 2;
|
|
76
|
+
while (i < source.length - 1)
|
|
77
|
+
{
|
|
78
|
+
if (source[i] === '\n') line++;
|
|
79
|
+
if (source[i] === '*' && source[i + 1] === '/')
|
|
80
|
+
{
|
|
81
|
+
i += 2;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// String literal
|
|
90
|
+
if (ch === '"' || ch === "'")
|
|
91
|
+
{
|
|
92
|
+
const quote = ch;
|
|
93
|
+
let str = '';
|
|
94
|
+
i++;
|
|
95
|
+
while (i < source.length && source[i] !== quote)
|
|
96
|
+
{
|
|
97
|
+
if (source[i] === '\\' && i + 1 < source.length)
|
|
98
|
+
{
|
|
99
|
+
const esc = source[i + 1];
|
|
100
|
+
if (esc === 'n') str += '\n';
|
|
101
|
+
else if (esc === 't') str += '\t';
|
|
102
|
+
else if (esc === '\\') str += '\\';
|
|
103
|
+
else if (esc === quote) str += quote;
|
|
104
|
+
else str += esc;
|
|
105
|
+
i += 2;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
{
|
|
109
|
+
str += source[i++];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
i++; // skip closing quote
|
|
113
|
+
tokens.push({ type: TOK.STRING, value: str, line });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Number (integer or float, including negative)
|
|
118
|
+
if ((ch >= '0' && ch <= '9') || (ch === '-' && source[i + 1] >= '0' && source[i + 1] <= '9'))
|
|
119
|
+
{
|
|
120
|
+
let num = '';
|
|
121
|
+
if (ch === '-') { num = '-'; i++; }
|
|
122
|
+
|
|
123
|
+
// Hex
|
|
124
|
+
if (source[i] === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X'))
|
|
125
|
+
{
|
|
126
|
+
num += '0x'; i += 2;
|
|
127
|
+
while (i < source.length && /[0-9a-fA-F]/.test(source[i])) num += source[i++];
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
{
|
|
131
|
+
while (i < source.length && ((source[i] >= '0' && source[i] <= '9') || source[i] === '.' || source[i] === 'e' || source[i] === 'E' || source[i] === '+' || source[i] === '-'))
|
|
132
|
+
{
|
|
133
|
+
// Avoid consuming the next field's minus sign
|
|
134
|
+
if ((source[i] === '+' || source[i] === '-') && source[i - 1] !== 'e' && source[i - 1] !== 'E') break;
|
|
135
|
+
num += source[i++];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
tokens.push({ type: TOK.NUMBER, value: num, line });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Identifier or keyword
|
|
143
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_')
|
|
144
|
+
{
|
|
145
|
+
let ident = '';
|
|
146
|
+
while (i < source.length && ((source[i] >= 'a' && source[i] <= 'z') || (source[i] >= 'A' && source[i] <= 'Z') || (source[i] >= '0' && source[i] <= '9') || source[i] === '_' || source[i] === '.'))
|
|
147
|
+
{
|
|
148
|
+
ident += source[i++];
|
|
149
|
+
}
|
|
150
|
+
tokens.push({ type: TOK.IDENT, value: ident, line });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Symbols: { } ( ) ; = , < > [ ]
|
|
155
|
+
tokens.push({ type: TOK.SYMBOL, value: ch, line });
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
tokens.push({ type: TOK.EOF, value: '', line });
|
|
160
|
+
return tokens;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// -- Parser ------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse a proto3 source string into a structured schema.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} source - Proto3 source text.
|
|
169
|
+
* @param {object} [opts] - Parser options.
|
|
170
|
+
* @param {string} [opts.filename] - File name for error messages.
|
|
171
|
+
* @param {string} [opts.basePath] - Base directory for resolving imports.
|
|
172
|
+
* @param {boolean} [opts.resolveImports=false] - Whether to recursively parse imported files.
|
|
173
|
+
* @returns {ProtoSchema} Parsed schema with messages, enums, and services.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const schema = parseProto('syntax = "proto3"; message Ping { string msg = 1; }');
|
|
177
|
+
* schema.messages.Ping.fields[0].name; // 'msg'
|
|
178
|
+
* schema.messages.Ping.fields[0].type; // 'string'
|
|
179
|
+
*/
|
|
180
|
+
function parseProto(source, opts = {})
|
|
181
|
+
{
|
|
182
|
+
const tokens = tokenize(source);
|
|
183
|
+
let pos = 0;
|
|
184
|
+
const filename = opts.filename || '<inline>';
|
|
185
|
+
|
|
186
|
+
const schema = {
|
|
187
|
+
syntax: 'proto3',
|
|
188
|
+
package: '',
|
|
189
|
+
imports: [],
|
|
190
|
+
options: {},
|
|
191
|
+
messages: {},
|
|
192
|
+
enums: {},
|
|
193
|
+
services: {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get current token.
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
function peek()
|
|
201
|
+
{
|
|
202
|
+
return tokens[pos];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Consume and return the current token.
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
function next()
|
|
210
|
+
{
|
|
211
|
+
return tokens[pos++];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Expect a specific token value or type.
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
function expect(value, type)
|
|
219
|
+
{
|
|
220
|
+
const tok = next();
|
|
221
|
+
if (type && tok.type !== type)
|
|
222
|
+
throw new SyntaxError(`${filename}:${tok.line}: expected ${type} "${value}", got ${tok.type} "${tok.value}"`);
|
|
223
|
+
if (value && tok.value !== value)
|
|
224
|
+
throw new SyntaxError(`${filename}:${tok.line}: expected "${value}", got "${tok.value}"`);
|
|
225
|
+
return tok;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if current token matches value.
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
function match(value)
|
|
233
|
+
{
|
|
234
|
+
return peek().value === value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Consume if current token matches value.
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
function eat(value)
|
|
242
|
+
{
|
|
243
|
+
if (match(value)) { next(); return true; }
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// -- Top-Level Parsing ---------------------------------
|
|
248
|
+
|
|
249
|
+
while (peek().type !== TOK.EOF)
|
|
250
|
+
{
|
|
251
|
+
const tok = peek();
|
|
252
|
+
|
|
253
|
+
if (tok.value === 'syntax')
|
|
254
|
+
{
|
|
255
|
+
next(); expect('='); schema.syntax = next().value; expect(';');
|
|
256
|
+
if (schema.syntax !== 'proto3')
|
|
257
|
+
log.warn('proto file uses syntax "%s" — only proto3 is fully supported', schema.syntax);
|
|
258
|
+
}
|
|
259
|
+
else if (tok.value === 'package')
|
|
260
|
+
{
|
|
261
|
+
next(); schema.package = next().value; expect(';');
|
|
262
|
+
}
|
|
263
|
+
else if (tok.value === 'import')
|
|
264
|
+
{
|
|
265
|
+
next();
|
|
266
|
+
const weak = eat('weak');
|
|
267
|
+
const pub = eat('public');
|
|
268
|
+
const importPath = next().value;
|
|
269
|
+
expect(';');
|
|
270
|
+
schema.imports.push({ path: importPath, weak, public: pub });
|
|
271
|
+
|
|
272
|
+
if (opts.resolveImports && opts.basePath)
|
|
273
|
+
{
|
|
274
|
+
try
|
|
275
|
+
{
|
|
276
|
+
const fullPath = path.resolve(opts.basePath, importPath);
|
|
277
|
+
const importSource = fs.readFileSync(fullPath, 'utf8');
|
|
278
|
+
const importSchema = parseProto(importSource, {
|
|
279
|
+
filename: importPath,
|
|
280
|
+
basePath: path.dirname(fullPath),
|
|
281
|
+
resolveImports: true,
|
|
282
|
+
});
|
|
283
|
+
// Merge imported definitions
|
|
284
|
+
Object.assign(schema.messages, importSchema.messages);
|
|
285
|
+
Object.assign(schema.enums, importSchema.enums);
|
|
286
|
+
Object.assign(schema.services, importSchema.services);
|
|
287
|
+
}
|
|
288
|
+
catch (err)
|
|
289
|
+
{
|
|
290
|
+
log.warn('failed to resolve import "%s": %s', importPath, err.message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (tok.value === 'option')
|
|
295
|
+
{
|
|
296
|
+
next(); _parseOption(schema.options);
|
|
297
|
+
}
|
|
298
|
+
else if (tok.value === 'message')
|
|
299
|
+
{
|
|
300
|
+
next();
|
|
301
|
+
const name = next().value;
|
|
302
|
+
schema.messages[name] = _parseMessage(name);
|
|
303
|
+
}
|
|
304
|
+
else if (tok.value === 'enum')
|
|
305
|
+
{
|
|
306
|
+
next();
|
|
307
|
+
const name = next().value;
|
|
308
|
+
schema.enums[name] = _parseEnum(name);
|
|
309
|
+
}
|
|
310
|
+
else if (tok.value === 'service')
|
|
311
|
+
{
|
|
312
|
+
next();
|
|
313
|
+
const name = next().value;
|
|
314
|
+
schema.services[name] = _parseService(name);
|
|
315
|
+
}
|
|
316
|
+
else if (tok.value === ';')
|
|
317
|
+
{
|
|
318
|
+
next(); // skip stray semicolons
|
|
319
|
+
}
|
|
320
|
+
else
|
|
321
|
+
{
|
|
322
|
+
throw new SyntaxError(`${filename}:${tok.line}: unexpected token "${tok.value}"`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Link enum definitions into message fields
|
|
327
|
+
_linkEnums(schema);
|
|
328
|
+
|
|
329
|
+
log.info('parsed %s: %d messages, %d enums, %d services',
|
|
330
|
+
filename,
|
|
331
|
+
Object.keys(schema.messages).length,
|
|
332
|
+
Object.keys(schema.enums).length,
|
|
333
|
+
Object.keys(schema.services).length,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return schema;
|
|
337
|
+
|
|
338
|
+
// -- Helper Functions (closures over peek/next/expect/match/eat) --
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Parse a message definition (including nested messages and enums).
|
|
342
|
+
* @param {string} msgName
|
|
343
|
+
* @returns {object}
|
|
344
|
+
*/
|
|
345
|
+
function _parseMessage(msgName)
|
|
346
|
+
{
|
|
347
|
+
const msg = { name: msgName, fields: [], oneofs: {}, nested: {}, nestedEnums: {}, options: {} };
|
|
348
|
+
|
|
349
|
+
expect('{');
|
|
350
|
+
|
|
351
|
+
while (!match('}'))
|
|
352
|
+
{
|
|
353
|
+
const tok = peek();
|
|
354
|
+
|
|
355
|
+
if (tok.value === 'message')
|
|
356
|
+
{
|
|
357
|
+
next();
|
|
358
|
+
const nestedName = next().value;
|
|
359
|
+
msg.nested[nestedName] = _parseMessage(nestedName);
|
|
360
|
+
}
|
|
361
|
+
else if (tok.value === 'enum')
|
|
362
|
+
{
|
|
363
|
+
next();
|
|
364
|
+
const enumName = next().value;
|
|
365
|
+
msg.nestedEnums[enumName] = _parseEnum(enumName);
|
|
366
|
+
}
|
|
367
|
+
else if (tok.value === 'oneof')
|
|
368
|
+
{
|
|
369
|
+
next();
|
|
370
|
+
const oneofName = next().value;
|
|
371
|
+
msg.oneofs[oneofName] = _parseOneof(oneofName, msg.fields);
|
|
372
|
+
}
|
|
373
|
+
else if (tok.value === 'map')
|
|
374
|
+
{
|
|
375
|
+
_parseMapField(msg.fields);
|
|
376
|
+
}
|
|
377
|
+
else if (tok.value === 'reserved')
|
|
378
|
+
{
|
|
379
|
+
_parseReserved();
|
|
380
|
+
}
|
|
381
|
+
else if (tok.value === 'option')
|
|
382
|
+
{
|
|
383
|
+
next(); _parseOption(msg.options);
|
|
384
|
+
}
|
|
385
|
+
else if (tok.value === ';')
|
|
386
|
+
{
|
|
387
|
+
next();
|
|
388
|
+
}
|
|
389
|
+
else
|
|
390
|
+
{
|
|
391
|
+
_parseField(msg.fields);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
expect('}');
|
|
396
|
+
if (peek().value === ';') next();
|
|
397
|
+
|
|
398
|
+
return msg;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Parse a field declaration.
|
|
403
|
+
* @param {object[]} fieldsArray
|
|
404
|
+
*/
|
|
405
|
+
function _parseField(fieldsArray)
|
|
406
|
+
{
|
|
407
|
+
let repeated = false;
|
|
408
|
+
let optional = false;
|
|
409
|
+
|
|
410
|
+
if (match('repeated')) { next(); repeated = true; }
|
|
411
|
+
else if (match('optional')) { next(); optional = true; }
|
|
412
|
+
|
|
413
|
+
const type = next().value;
|
|
414
|
+
const fName = next().value;
|
|
415
|
+
expect('=');
|
|
416
|
+
const number = parseInt(next().value, 10);
|
|
417
|
+
|
|
418
|
+
const fieldOpts = {};
|
|
419
|
+
if (match('['))
|
|
420
|
+
{
|
|
421
|
+
next();
|
|
422
|
+
while (!match(']'))
|
|
423
|
+
{
|
|
424
|
+
const optName = next().value;
|
|
425
|
+
expect('=');
|
|
426
|
+
const optVal = next().value;
|
|
427
|
+
fieldOpts[optName] = optVal;
|
|
428
|
+
eat(',');
|
|
429
|
+
}
|
|
430
|
+
expect(']');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
expect(';');
|
|
434
|
+
|
|
435
|
+
fieldsArray.push({
|
|
436
|
+
name: fName,
|
|
437
|
+
type,
|
|
438
|
+
number,
|
|
439
|
+
repeated,
|
|
440
|
+
optional,
|
|
441
|
+
map: false,
|
|
442
|
+
options: fieldOpts,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Parse a map field: `map<KeyType, ValueType> name = N;`
|
|
448
|
+
* @param {object[]} fieldsArray
|
|
449
|
+
*/
|
|
450
|
+
function _parseMapField(fieldsArray)
|
|
451
|
+
{
|
|
452
|
+
expect('map');
|
|
453
|
+
expect('<');
|
|
454
|
+
const keyType = next().value;
|
|
455
|
+
expect(',');
|
|
456
|
+
const valueType = next().value;
|
|
457
|
+
expect('>');
|
|
458
|
+
const fName = next().value;
|
|
459
|
+
expect('=');
|
|
460
|
+
const number = parseInt(next().value, 10);
|
|
461
|
+
|
|
462
|
+
const fieldOpts = {};
|
|
463
|
+
if (match('['))
|
|
464
|
+
{
|
|
465
|
+
next();
|
|
466
|
+
while (!match(']'))
|
|
467
|
+
{
|
|
468
|
+
const optName = next().value;
|
|
469
|
+
expect('=');
|
|
470
|
+
const optVal = next().value;
|
|
471
|
+
fieldOpts[optName] = optVal;
|
|
472
|
+
eat(',');
|
|
473
|
+
}
|
|
474
|
+
expect(']');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
expect(';');
|
|
478
|
+
|
|
479
|
+
fieldsArray.push({
|
|
480
|
+
name: fName,
|
|
481
|
+
type: `map<${keyType},${valueType}>`,
|
|
482
|
+
keyType,
|
|
483
|
+
valueType,
|
|
484
|
+
number,
|
|
485
|
+
repeated: false,
|
|
486
|
+
optional: false,
|
|
487
|
+
map: true,
|
|
488
|
+
mapKeyType: keyType,
|
|
489
|
+
mapValueType: valueType,
|
|
490
|
+
options: fieldOpts,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Parse oneof block.
|
|
496
|
+
* @param {string} oneofName
|
|
497
|
+
* @param {object[]} fieldsArray
|
|
498
|
+
* @returns {string[]}
|
|
499
|
+
*/
|
|
500
|
+
function _parseOneof(oneofName, fieldsArray)
|
|
501
|
+
{
|
|
502
|
+
const fieldNames = [];
|
|
503
|
+
expect('{');
|
|
504
|
+
|
|
505
|
+
while (!match('}'))
|
|
506
|
+
{
|
|
507
|
+
if (match('option'))
|
|
508
|
+
{
|
|
509
|
+
next(); _parseOption({});
|
|
510
|
+
}
|
|
511
|
+
else if (match(';'))
|
|
512
|
+
{
|
|
513
|
+
next();
|
|
514
|
+
}
|
|
515
|
+
else
|
|
516
|
+
{
|
|
517
|
+
const type = next().value;
|
|
518
|
+
const fName = next().value;
|
|
519
|
+
expect('=');
|
|
520
|
+
const number = parseInt(next().value, 10);
|
|
521
|
+
|
|
522
|
+
const fieldOpts = {};
|
|
523
|
+
if (match('['))
|
|
524
|
+
{
|
|
525
|
+
next();
|
|
526
|
+
while (!match(']'))
|
|
527
|
+
{
|
|
528
|
+
const optName = next().value;
|
|
529
|
+
expect('=');
|
|
530
|
+
const optVal = next().value;
|
|
531
|
+
fieldOpts[optName] = optVal;
|
|
532
|
+
eat(',');
|
|
533
|
+
}
|
|
534
|
+
expect(']');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
expect(';');
|
|
538
|
+
|
|
539
|
+
fieldsArray.push({
|
|
540
|
+
name: fName,
|
|
541
|
+
type,
|
|
542
|
+
number,
|
|
543
|
+
repeated: false,
|
|
544
|
+
optional: false,
|
|
545
|
+
map: false,
|
|
546
|
+
oneofName,
|
|
547
|
+
options: fieldOpts,
|
|
548
|
+
});
|
|
549
|
+
fieldNames.push(fName);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
expect('}');
|
|
554
|
+
return fieldNames;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Parse an enum definition.
|
|
559
|
+
* @param {string} eName
|
|
560
|
+
* @returns {object}
|
|
561
|
+
*/
|
|
562
|
+
function _parseEnum(eName)
|
|
563
|
+
{
|
|
564
|
+
const enumDef = { name: eName, values: {}, options: {} };
|
|
565
|
+
expect('{');
|
|
566
|
+
|
|
567
|
+
while (!match('}'))
|
|
568
|
+
{
|
|
569
|
+
if (match('option'))
|
|
570
|
+
{
|
|
571
|
+
next(); _parseOption(enumDef.options);
|
|
572
|
+
}
|
|
573
|
+
else if (match('reserved'))
|
|
574
|
+
{
|
|
575
|
+
_parseReserved();
|
|
576
|
+
}
|
|
577
|
+
else if (match(';'))
|
|
578
|
+
{
|
|
579
|
+
next();
|
|
580
|
+
}
|
|
581
|
+
else
|
|
582
|
+
{
|
|
583
|
+
const valueName = next().value;
|
|
584
|
+
expect('=');
|
|
585
|
+
const valueNumber = parseInt(next().value, 10);
|
|
586
|
+
|
|
587
|
+
if (match('['))
|
|
588
|
+
{
|
|
589
|
+
next();
|
|
590
|
+
while (!match(']'))
|
|
591
|
+
{
|
|
592
|
+
next(); eat('='); next(); eat(',');
|
|
593
|
+
}
|
|
594
|
+
expect(']');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
expect(';');
|
|
598
|
+
enumDef.values[valueName] = valueNumber;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
expect('}');
|
|
603
|
+
if (peek().value === ';') next();
|
|
604
|
+
|
|
605
|
+
return enumDef;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Parse a service definition with RPC methods.
|
|
610
|
+
* @param {string} sName
|
|
611
|
+
* @returns {object}
|
|
612
|
+
*/
|
|
613
|
+
function _parseService(sName)
|
|
614
|
+
{
|
|
615
|
+
const service = { name: sName, methods: {}, options: {} };
|
|
616
|
+
expect('{');
|
|
617
|
+
|
|
618
|
+
while (!match('}'))
|
|
619
|
+
{
|
|
620
|
+
if (match('rpc'))
|
|
621
|
+
{
|
|
622
|
+
next();
|
|
623
|
+
const methodName = next().value;
|
|
624
|
+
expect('(');
|
|
625
|
+
const clientStreaming = eat('stream');
|
|
626
|
+
const inputType = next().value;
|
|
627
|
+
expect(')');
|
|
628
|
+
expect('returns');
|
|
629
|
+
expect('(');
|
|
630
|
+
const serverStreaming = eat('stream');
|
|
631
|
+
const outputType = next().value;
|
|
632
|
+
expect(')');
|
|
633
|
+
|
|
634
|
+
const methodOpts = {};
|
|
635
|
+
if (match('{'))
|
|
636
|
+
{
|
|
637
|
+
next();
|
|
638
|
+
while (!match('}'))
|
|
639
|
+
{
|
|
640
|
+
if (match('option'))
|
|
641
|
+
{
|
|
642
|
+
next(); _parseOption(methodOpts);
|
|
643
|
+
}
|
|
644
|
+
else
|
|
645
|
+
{
|
|
646
|
+
next();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
expect('}');
|
|
650
|
+
}
|
|
651
|
+
else
|
|
652
|
+
{
|
|
653
|
+
expect(';');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
service.methods[methodName] = {
|
|
657
|
+
name: methodName,
|
|
658
|
+
inputType,
|
|
659
|
+
outputType,
|
|
660
|
+
clientStreaming: !!clientStreaming,
|
|
661
|
+
serverStreaming: !!serverStreaming,
|
|
662
|
+
options: methodOpts,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
else if (match('option'))
|
|
666
|
+
{
|
|
667
|
+
next(); _parseOption(service.options);
|
|
668
|
+
}
|
|
669
|
+
else if (match(';'))
|
|
670
|
+
{
|
|
671
|
+
next();
|
|
672
|
+
}
|
|
673
|
+
else
|
|
674
|
+
{
|
|
675
|
+
next();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
expect('}');
|
|
680
|
+
if (peek().value === ';') next();
|
|
681
|
+
|
|
682
|
+
return service;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Parse an option statement: `option name = value;`
|
|
687
|
+
* @param {object} target
|
|
688
|
+
*/
|
|
689
|
+
function _parseOption(target)
|
|
690
|
+
{
|
|
691
|
+
let optName = '';
|
|
692
|
+
if (match('('))
|
|
693
|
+
{
|
|
694
|
+
next();
|
|
695
|
+
optName = '(' + next().value + ')';
|
|
696
|
+
expect(')');
|
|
697
|
+
}
|
|
698
|
+
else
|
|
699
|
+
{
|
|
700
|
+
optName = next().value;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
while (match('.'))
|
|
704
|
+
{
|
|
705
|
+
next();
|
|
706
|
+
optName += '.' + next().value;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
expect('=');
|
|
710
|
+
const value = next().value;
|
|
711
|
+
expect(';');
|
|
712
|
+
target[optName] = value;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Skip a `reserved` statement.
|
|
717
|
+
*/
|
|
718
|
+
function _parseReserved()
|
|
719
|
+
{
|
|
720
|
+
next(); // consume 'reserved'
|
|
721
|
+
while (peek().value !== ';' && peek().type !== TOK.EOF) next();
|
|
722
|
+
if (peek().value === ';') next();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// -- Post-Processing --------------------------------------
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Link enum definitions into message fields so the codec knows how to encode/decode them.
|
|
730
|
+
* Also flattens nested messages and enums into the top-level maps for easy lookup.
|
|
731
|
+
* @private
|
|
732
|
+
* @param {object} schema
|
|
733
|
+
*/
|
|
734
|
+
function _linkEnums(schema)
|
|
735
|
+
{
|
|
736
|
+
// Flatten nested messages and enums
|
|
737
|
+
const flatMsgs = {};
|
|
738
|
+
const flatEnums = {};
|
|
739
|
+
|
|
740
|
+
function flatten(messages, enums, prefix)
|
|
741
|
+
{
|
|
742
|
+
for (const [name, msg] of Object.entries(messages))
|
|
743
|
+
{
|
|
744
|
+
const fullName = prefix ? `${prefix}.${name}` : name;
|
|
745
|
+
flatMsgs[fullName] = msg;
|
|
746
|
+
flatMsgs[name] = msg; // also store short name for convenience
|
|
747
|
+
|
|
748
|
+
if (msg.nested) flatten(msg.nested, {}, fullName);
|
|
749
|
+
if (msg.nestedEnums)
|
|
750
|
+
{
|
|
751
|
+
for (const [eName, eDef] of Object.entries(msg.nestedEnums))
|
|
752
|
+
{
|
|
753
|
+
const fullEnum = prefix ? `${prefix}.${name}.${eName}` : `${name}.${eName}`;
|
|
754
|
+
flatEnums[fullEnum] = eDef;
|
|
755
|
+
flatEnums[eName] = eDef; // short name
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
for (const [name, def] of Object.entries(enums))
|
|
761
|
+
{
|
|
762
|
+
const fullName = prefix ? `${prefix}.${name}` : name;
|
|
763
|
+
flatEnums[fullName] = def;
|
|
764
|
+
flatEnums[name] = def;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
flatten(schema.messages, schema.enums, schema.package);
|
|
769
|
+
|
|
770
|
+
// Merge flattened into schema
|
|
771
|
+
Object.assign(schema.messages, flatMsgs);
|
|
772
|
+
Object.assign(schema.enums, flatEnums);
|
|
773
|
+
|
|
774
|
+
// Link enum types into message fields
|
|
775
|
+
for (const msg of Object.values(schema.messages))
|
|
776
|
+
{
|
|
777
|
+
if (!msg.fields) continue;
|
|
778
|
+
for (const field of msg.fields)
|
|
779
|
+
{
|
|
780
|
+
if (!field.map && schema.enums[field.type])
|
|
781
|
+
{
|
|
782
|
+
field.enumDef = schema.enums[field.type];
|
|
783
|
+
}
|
|
784
|
+
// Map value enums
|
|
785
|
+
if (field.map && schema.enums[field.valueType])
|
|
786
|
+
{
|
|
787
|
+
field.enumDef = schema.enums[field.valueType];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// -- File Loader -------------------------------------------
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Parse a `.proto` file from disk.
|
|
797
|
+
*
|
|
798
|
+
* @param {string} filePath - Path to the `.proto` file.
|
|
799
|
+
* @param {object} [opts] - Parser options.
|
|
800
|
+
* @param {boolean} [opts.resolveImports=false] - Whether to recursively resolve imports.
|
|
801
|
+
* @returns {ProtoSchema} Parsed schema.
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* const schema = parseProtoFile('./protos/chat.proto');
|
|
805
|
+
*/
|
|
806
|
+
function parseProtoFile(filePath, opts = {})
|
|
807
|
+
{
|
|
808
|
+
const resolved = path.resolve(filePath);
|
|
809
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
810
|
+
return parseProto(source, {
|
|
811
|
+
filename: path.basename(resolved),
|
|
812
|
+
basePath: path.dirname(resolved),
|
|
813
|
+
...opts,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
module.exports = {
|
|
818
|
+
parseProto,
|
|
819
|
+
parseProtoFile,
|
|
820
|
+
tokenize,
|
|
821
|
+
};
|