@zero-server/grpc 0.9.1 → 0.9.3

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.
@@ -0,0 +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
+ };