@@ -19,6 +19,16 @@ function pathWithExtension (input: string, extension: string, outputDir?: string
19
19
return path . join ( output , path . basename ( input ) . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' ) + extension )
20
20
}
21
21
22
+ export class CodeError extends Error {
23
+ public code : string
24
+
25
+ constructor ( message : string , code : string , options ?: ErrorOptions ) {
26
+ super ( message , options )
27
+
28
+ this . code = code
29
+ }
30
+ }
31
+
22
32
const types : Record < string , string > = {
23
33
bool : 'boolean' ,
24
34
bytes : 'Uint8Array' ,
@@ -287,6 +297,13 @@ interface FieldDef {
287
297
map : boolean
288
298
valueType : string
289
299
keyType : string
300
+
301
+ /**
302
+ * Support proto2 required field. This field means a value should always be
303
+ * in the serialized buffer, any message without it should be considered
304
+ * invalid. It was removed for proto3.
305
+ */
306
+ proto2Required : boolean
290
307
}
291
308
292
309
function defineFields ( fields : Record < string , FieldDef > , messageDef : MessageDef , moduleDef : ModuleDef ) : string [ ] {
@@ -299,10 +316,22 @@ function defineFields (fields: Record<string, FieldDef>, messageDef: MessageDef,
299
316
} )
300
317
}
301
318
302
- function compileMessage ( messageDef : MessageDef , moduleDef : ModuleDef ) : string {
319
+ function compileMessage ( messageDef : MessageDef , moduleDef : ModuleDef , flags ?: Flags ) : string {
303
320
if ( isEnumDef ( messageDef ) ) {
304
321
moduleDef . imports . add ( 'enumeration' )
305
322
323
+ // check that the enum def values start from 0
324
+ if ( Object . values ( messageDef . values ) [ 0 ] !== 0 ) {
325
+ const message = `enum ${ messageDef . name } does not contain a value that maps to zero as it's first element, this is required in proto3 - see https://protobuf.dev/programming-guides/proto3/#enum`
326
+
327
+ if ( flags ?. strict === true ) {
328
+ throw new CodeError ( message , 'ERR_PARSE_ERROR' )
329
+ } else {
330
+ // eslint-disable-next-line no-console
331
+ console . info ( `[WARN] ${ message } ` )
332
+ }
333
+ }
334
+
306
335
return `
307
336
export enum ${ messageDef . name } {
308
337
${
@@ -332,7 +361,7 @@ export namespace ${messageDef.name} {
332
361
if ( messageDef . nested != null ) {
333
362
nested = '\n'
334
363
nested += Object . values ( messageDef . nested )
335
- . map ( def => compileMessage ( def , moduleDef ) . trim ( ) )
364
+ . map ( def => compileMessage ( def , moduleDef , flags ) . trim ( ) )
336
365
. join ( '\n\n' )
337
366
. split ( '\n' )
338
367
. map ( line => line . trim ( ) === '' ? '' : ` ${ line } ` )
@@ -391,13 +420,25 @@ export interface ${messageDef.name} {
391
420
392
421
if ( fieldDef . map ) {
393
422
valueTest = `obj.${ name } != null && obj.${ name } .size !== 0`
394
- } else if ( ! fieldDef . optional && ! fieldDef . repeated ) {
423
+ } else if ( ! fieldDef . optional && ! fieldDef . repeated && ! fieldDef . proto2Required ) {
395
424
// proto3 singular fields should only be written out if they are not the default value
396
425
if ( defaultValueTestGenerators [ type ] != null ) {
397
426
valueTest = `${ defaultValueTestGenerators [ type ] ( `obj.${ name } ` ) } `
398
427
} else if ( type === 'enum' ) {
399
428
// handle enums
400
- valueTest = `obj.${ name } != null && __${ fieldDef . type } Values[obj.${ name } ] !== 0`
429
+ const def = findDef ( fieldDef . type , messageDef , moduleDef )
430
+
431
+ if ( ! isEnumDef ( def ) ) {
432
+ throw new Error ( `${ fieldDef . type } was not enum def` )
433
+ }
434
+
435
+ valueTest = `obj.${ name } != null`
436
+
437
+ // singular enums default to 0, but enums can be defined without a 0
438
+ // value which is against the proto3 spec but is tolerated
439
+ if ( Object . values ( def . values ) [ 0 ] === 0 ) {
440
+ valueTest += ` && __${ fieldDef . type } Values[obj.${ name } ] !== 0`
441
+ }
401
442
}
402
443
}
403
444
@@ -496,14 +537,16 @@ export interface ${messageDef.name} {
496
537
break
497
538
}`
498
539
} else if ( fieldDef . repeated ) {
499
- return `case ${ fieldDef . id } :
540
+ return `case ${ fieldDef . id } : {
500
541
obj.${ fieldName } .push(${ parseValue } )
501
- break`
542
+ break
543
+ }`
502
544
}
503
545
504
- return `case ${ fieldDef . id } :
546
+ return `case ${ fieldDef . id } : {
505
547
obj.${ fieldName } = ${ parseValue }
506
- break`
548
+ break
549
+ }`
507
550
}
508
551
509
552
return createReadField ( fieldName , fieldDef )
@@ -532,9 +575,10 @@ ${encodeFields === '' ? '' : `${encodeFields}\n`}
532
575
const tag = reader.uint32()
533
576
534
577
switch (tag >>> 3) {${ decodeFields === '' ? '' : `\n ${ decodeFields } ` }
535
- default:
578
+ default: {
536
579
reader.skipType(tag & 7)
537
580
break
581
+ }
538
582
}
539
583
}
540
584
@@ -570,7 +614,7 @@ interface ModuleDef {
570
614
globals : Record < string , ClassDef >
571
615
}
572
616
573
- function defineModule ( def : ClassDef ) : ModuleDef {
617
+ function defineModule ( def : ClassDef , flags : Flags ) : ModuleDef {
574
618
const moduleDef : ModuleDef = {
575
619
imports : new Set ( ) ,
576
620
importedTypes : new Set ( ) ,
@@ -582,10 +626,10 @@ function defineModule (def: ClassDef): ModuleDef {
582
626
const defs = def . nested
583
627
584
628
if ( defs == null ) {
585
- throw new Error ( 'No top-level messages found in protobuf' )
629
+ throw new CodeError ( 'No top-level messages found in protobuf' , 'ERR_NO_MESSAGES_FOUND ')
586
630
}
587
631
588
- function defineMessage ( defs : Record < string , ClassDef > , parent ?: ClassDef ) : void {
632
+ function defineMessage ( defs : Record < string , ClassDef > , parent ?: ClassDef , flags ?: Flags ) : void {
589
633
for ( const className of Object . keys ( defs ) ) {
590
634
const classDef = defs [ className ]
591
635
@@ -603,9 +647,19 @@ function defineModule (def: ClassDef): ModuleDef {
603
647
fieldDef . repeated = fieldDef . rule === 'repeated'
604
648
fieldDef . optional = ! fieldDef . repeated && fieldDef . options ?. proto3_optional === true
605
649
fieldDef . map = fieldDef . keyType != null
650
+ fieldDef . proto2Required = false
606
651
607
652
if ( fieldDef . rule === 'required' ) {
608
- throw new Error ( '"required" fields are not allowed in proto3 - please convert your proto2 definitions to proto3' )
653
+ const message = `field "${ name } " is required, this is not allowed in proto3. Please convert your proto2 definitions to proto3 - see https://github.com/ipfs/protons/wiki/Required-fields-and-protobuf-3`
654
+
655
+ if ( flags ?. strict === true ) {
656
+ throw new CodeError ( message , 'ERR_PARSE_ERROR' )
657
+ } else {
658
+ fieldDef . proto2Required = true
659
+
660
+ // eslint-disable-next-line no-console
661
+ console . info ( `[WARN] ${ message } ` )
662
+ }
609
663
}
610
664
}
611
665
}
@@ -644,22 +698,30 @@ function defineModule (def: ClassDef): ModuleDef {
644
698
}
645
699
}
646
700
647
- defineMessage ( defs )
701
+ defineMessage ( defs , undefined , flags )
648
702
649
703
// set enum/message fields now all messages have been defined
650
704
updateTypes ( defs )
651
705
652
706
for ( const className of Object . keys ( defs ) ) {
653
707
const classDef = defs [ className ]
654
708
655
- moduleDef . compiled . push ( compileMessage ( classDef , moduleDef ) )
709
+ moduleDef . compiled . push ( compileMessage ( classDef , moduleDef , flags ) )
656
710
}
657
711
658
712
return moduleDef
659
713
}
660
714
661
715
interface Flags {
716
+ /**
717
+ * Specifies an output directory
718
+ */
662
719
output ?: string
720
+
721
+ /**
722
+ * If true, warnings will be thrown as errors
723
+ */
724
+ strict ?: boolean
663
725
}
664
726
665
727
export async function generate ( source : string , flags : Flags ) : Promise < void > {
@@ -701,7 +763,7 @@ export async function generate (source: string, flags: Flags): Promise<void> {
701
763
}
702
764
}
703
765
704
- const moduleDef = defineModule ( def )
766
+ const moduleDef = defineModule ( def , flags )
705
767
706
768
const ignores = [
707
769
'/* eslint-disable import/export */' ,
0 commit comments