Skip to content
This repository was archived by the owner on Jun 20, 2024. It is now read-only.

Commit c8aad7b

Browse files
authored
This is a way to force consts for enum (#99)
* This is a way to force consts for enum * New enum options * ENUM const logic * Tests * Forcing version draft-06 * README update * Regenerated jsonschemas too * Switching title for description * Dynamically changing schema version when const is used * Updated unit tests Co-authored-by: Chrusty <>
1 parent 61fcfc3 commit c8aad7b

19 files changed

+521
-143
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ samples: build
3333
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfMessages.proto || echo "No messages found (ArrayOfMessages.proto)"
3434
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfObjects.proto || echo "No messages found (ArrayOfObjects.proto)"
3535
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfPrimitives.proto || echo "No messages found (ArrayOfPrimitives.proto)"
36-
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Enumception.proto || echo "No messages found (Enumception.proto)"
37-
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ImportedEnum.proto || echo "No messages found (ImportedEnum.proto)"
36+
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/Enumception.proto || echo "No messages found (Enumception.proto)"
37+
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_additional_properties:jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/ImportedEnum.proto || echo "No messages found (ImportedEnum.proto)"
3838
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/NestedMessage.proto || echo "No messages found (NestedMessage.proto)"
3939
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/NestedObject.proto || echo "No messages found (NestedObject.proto)"
4040
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/PayloadMessage.proto || echo "No messages found (PayloadMessage.proto)"
@@ -49,6 +49,7 @@ samples: build
4949
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Proto2Required.proto || echo "No messages found (Proto2Required.proto)"
5050
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Proto2NestedMessage.proto || echo "No messages found (Proto2NestedMessage.proto)"
5151
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleValue.proto || echo "No messages found (GoogleValue.proto)"
52+
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/OptionEnumsAsConstants.proto || echo "No messages found (OptionEnumsAsConstants.proto)"
5253
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/OptionFileExtension.proto || echo "No messages found (OptionFileExtension.proto)"
5354
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/OptionIgnoredField.proto || echo "No messages found (HiddenFields.proto)"
5455
@protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/OptionIgnoredFile.proto || echo "No messages found (IgnoredFile.proto)"

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ Custom Proto Options
9090

9191
If you don't want to use the configuration parameters (admittedly quite a nasty cli syntax) then some of the generator behaviour can be controlled using custom proto options. These are defined in [options.proto](options.proto), and your protoc command will need to include this file. See the [sample protos](internal/converter/testdata/proto) and generator commands in the [Makefile](Makefile).
9292

93+
### Enum Options
94+
95+
These apply to specifically marked enums, giving you more finely-grained control than with the CLI flags.
96+
97+
- [enums_as_constants](internal/converter/testdata/proto/ImportedEnum.proto): Encode ENUMs (and their annotations) as CONST
98+
9399
### Field Options
94100

95101
These apply to specifically marked fields, giving you more finely-grained control than with the CLI flags.
@@ -112,6 +118,7 @@ These options apply to a specific proto message.
112118
- [all_fields_required](internal/converter/testdata/proto/OptionRequiredMessage.proto): Mark all fields in a specific message as "required"
113119
- [allow_null_values](internal/converter/testdata/proto/OptionAllowNullValues.proto): Additionally allow null values for all fields in a message
114120
- [disallow_additional_properties](internal/converter/testdata/proto/OptionDisallowAdditionalProperties.proto): Only accept the specific properties, no extras
121+
- [enums_as_constants](internal/converter/testdata/proto/OptionEnumsAsConstants.proto): Encode ENUMs (and their annotations) as CONST
115122

116123

117124
Examples

internal/converter/converter.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/alecthomas/jsonschema"
1313
"github.com/chrusty/protoc-gen-jsonschema/internal/protos"
1414
"github.com/sirupsen/logrus"
15+
"github.com/xeipuuv/gojsonschema"
1516
"google.golang.org/protobuf/proto"
1617
descriptor "google.golang.org/protobuf/types/descriptorpb"
1718
plugin "google.golang.org/protobuf/types/pluginpb"
@@ -21,6 +22,8 @@ const (
2122
defaultFileExtension = "json"
2223
defaultRefPrefix = "#/definitions/"
2324
messageDelimiter = "+"
25+
versionDraft04 = "http://json-schema.org/draft-04/schema#"
26+
versionDraft06 = "http://json-schema.org/draft-06/schema#"
2427
)
2528

2629
// Converter is everything you need to convert protos to JSONSchemas:
@@ -31,6 +34,7 @@ type Converter struct {
3134
refPrefix string
3235
requiredFieldOption string
3336
schemaFileExtension string
37+
schemaVersion string
3438
sourceInfo *sourceCodeInfo
3539
messageTargets []string
3640
}
@@ -42,17 +46,19 @@ type ConverterFlags struct {
4246
DisallowAdditionalProperties bool
4347
DisallowBigIntsAsStrings bool
4448
EnforceOneOf bool
49+
EnumsAsConstants bool
4550
PrefixSchemaFilesWithPackage bool
4651
UseJSONFieldnamesOnly bool
4752
UseProtoAndJSONFieldNames bool
4853
}
4954

50-
// New returns a configured *Converter:
55+
// New returns a configured *Converter (defaulting to draft-04 version):
5156
func New(logger *logrus.Logger) *Converter {
5257
return &Converter{
5358
logger: logger,
5459
refPrefix: defaultRefPrefix,
5560
schemaFileExtension: defaultFileExtension,
61+
schemaVersion: versionDraft04,
5662
}
5763
}
5864

@@ -117,26 +123,59 @@ func (c *Converter) parseGeneratorParameters(parameters string) {
117123
}
118124

119125
// Converts a proto "ENUM" into a JSON-Schema:
120-
func (c *Converter) convertEnumType(enum *descriptor.EnumDescriptorProto) (jsonschema.Type, error) {
126+
func (c *Converter) convertEnumType(enum *descriptor.EnumDescriptorProto, converterFlags ConverterFlags) (jsonschema.Type, error) {
121127

122-
// Prepare a new jsonschema.Type for our eventual return value:
123-
jsonSchemaType := jsonschema.Type{
124-
Version: jsonschema.Version,
128+
// Set some per-enum flags from config and options:
129+
if opts := enum.GetOptions(); opts != nil && proto.HasExtension(opts, protos.E_EnumOptions) {
130+
if opt := proto.GetExtension(opts, protos.E_EnumOptions); opt != nil {
131+
if enumOptions, ok := opt.(*protos.EnumOptions); ok {
132+
133+
// ENUMs as constants:
134+
if enumOptions.GetEnumsAsConstants() {
135+
converterFlags.EnumsAsConstants = true
136+
}
137+
}
138+
}
125139
}
126140

141+
// Prepare a new jsonschema.Type for our eventual return value:
142+
jsonSchemaType := jsonschema.Type{}
143+
127144
// Generate a description from src comments (if available):
128145
if src := c.sourceInfo.GetEnum(enum); src != nil {
129146
jsonSchemaType.Description = formatDescription(src)
130147
}
131148

132-
// Allow both strings and integers:
133-
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "string"})
134-
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "integer"})
149+
// Use basic types if we're not opting to use constants for ENUMs:
150+
if !converterFlags.EnumsAsConstants {
151+
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_STRING})
152+
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_INTEGER})
153+
}
154+
155+
// Optionally allow NULL values:
156+
if converterFlags.AllowNullValues {
157+
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL})
158+
}
159+
160+
// We have found an enum, append its values:
161+
for _, value := range enum.Value {
162+
163+
// Each ENUM value can have comments too:
164+
var valueDescription string
165+
if src := c.sourceInfo.GetEnumValue(value); src != nil {
166+
valueDescription = formatDescription(src)
167+
}
168+
169+
// If we're using constants for ENUMs then add these here, along with their title:
170+
if converterFlags.EnumsAsConstants {
171+
c.schemaVersion = versionDraft06 // Const requires draft-06
172+
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Extras: map[string]interface{}{"const": value.GetName()}, Description: valueDescription})
173+
jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Extras: map[string]interface{}{"const": value.GetNumber()}, Description: valueDescription})
174+
}
135175

136-
// Add the allowed values:
137-
for _, enumValue := range enum.Value {
138-
jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Name)
139-
jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Number)
176+
// Add the values to the ENUM:
177+
jsonSchemaType.Enum = append(jsonSchemaType.Enum, value.Name)
178+
jsonSchemaType.Enum = append(jsonSchemaType.Enum, value.Number)
140179
}
141180

142181
return jsonSchemaType, nil
@@ -170,11 +209,12 @@ func (c *Converter) convertFile(file *descriptor.FileDescriptorProto, fileExtens
170209
c.logger.WithField("proto_filename", protoFileName).WithField("enum_name", enum.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for stand-alone ENUM")
171210

172211
// Convert the ENUM:
173-
enumJSONSchema, err := c.convertEnumType(enum)
212+
enumJSONSchema, err := c.convertEnumType(enum, ConverterFlags{})
174213
if err != nil {
175214
c.logger.WithError(err).WithField("proto_filename", protoFileName).Error("Failed to convert")
176215
return nil, err
177216
}
217+
enumJSONSchema.Version = c.schemaVersion
178218

179219
// Marshal the JSON-Schema into JSON:
180220
jsonSchemaJSON, err := json.MarshalIndent(enumJSONSchema, "", " ")
@@ -271,7 +311,7 @@ func (c *Converter) convert(request *plugin.CodeGeneratorRequest) (*plugin.CodeG
271311
// Start with the default / global file extension:
272312
fileExtension := c.schemaFileExtension
273313

274-
// Check for our custom field options:
314+
// Check for our custom file options:
275315
if opts := fileDesc.GetOptions(); opts != nil && proto.HasExtension(opts, protos.E_FileOptions) {
276316
if opt := proto.GetExtension(opts, protos.E_FileOptions); opt != nil {
277317
if fileOptions, ok := opt.(*protos.FileOptions); ok {

internal/converter/converter_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,6 @@ func configureSampleProtos() map[string]sampleProto {
202202
ObjectsToValidateFail: []string{testdata.EnumWithMessageFail},
203203
ObjectsToValidatePass: []string{testdata.EnumWithMessagePass},
204204
},
205-
"EnumImport": {
206-
ExpectedJSONSchema: []string{testdata.EnumImport},
207-
FilesToGenerate: []string{"ImportEnum.proto"},
208-
ProtoFileName: "ImportEnum.proto",
209-
ObjectsToValidateFail: []string{testdata.EnumImportFail},
210-
ObjectsToValidatePass: []string{testdata.EnumImportPass},
211-
},
212205
"EnumCeption": {
213206
ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.ImportedEnum, testdata.EnumCeption},
214207
FilesToGenerate: []string{"Enumception.proto", "PayloadMessage.proto", "ImportedEnum.proto"},
@@ -286,6 +279,13 @@ func configureSampleProtos() map[string]sampleProto {
286279
ObjectsToValidateFail: []string{testdata.OptionDisallowAdditionalPropertiesFail},
287280
ObjectsToValidatePass: []string{testdata.OptionDisallowAdditionalPropertiesPass},
288281
},
282+
"OptionEnumsAsConstants": {
283+
ExpectedJSONSchema: []string{testdata.OptionEnumsAsConstants},
284+
FilesToGenerate: []string{"OptionEnumsAsConstants.proto"},
285+
ProtoFileName: "OptionEnumsAsConstants.proto",
286+
ObjectsToValidateFail: []string{testdata.OptionEnumsAsConstantsFail},
287+
ObjectsToValidatePass: []string{testdata.OptionEnumsAsConstantsPass},
288+
},
289289
"OptionFileExtension": {
290290
ExpectedJSONSchema: []string{testdata.OptionFileExtension},
291291
ExpectedFileNames: []string{"OptionFileExtension.jsonschema"},

internal/converter/testdata/enum_ception.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package testdata
22

33
const EnumCeption = `{
4-
"$schema": "http://json-schema.org/draft-04/schema#",
4+
"$schema": "http://json-schema.org/draft-06/schema#",
55
"$ref": "#/definitions/Enumception",
66
"definitions": {
77
"Enumception": {
@@ -61,10 +61,36 @@ const EnumCeption = `{
6161
],
6262
"oneOf": [
6363
{
64-
"type": "string"
64+
"description": "Zero",
65+
"const": "VALUE_0"
6566
},
6667
{
67-
"type": "integer"
68+
"description": "Zero",
69+
"const": 0
70+
},
71+
{
72+
"description": "One",
73+
"const": "VALUE_1"
74+
},
75+
{
76+
"description": "One",
77+
"const": 1
78+
},
79+
{
80+
"description": "Two",
81+
"const": "VALUE_2"
82+
},
83+
{
84+
"description": "Two",
85+
"const": 2
86+
},
87+
{
88+
"description": "Three",
89+
"const": "VALUE_3"
90+
},
91+
{
92+
"description": "Three",
93+
"const": 3
6894
}
6995
],
7096
"description": "This is an enum"

internal/converter/testdata/enum_imported.go

Lines changed: 0 additions & 39 deletions
This file was deleted.

internal/converter/testdata/imported_enum.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package testdata
22

33
const ImportedEnum = `{
4-
"$schema": "http://json-schema.org/draft-04/schema#",
4+
"$schema": "http://json-schema.org/draft-06/schema#",
55
"enum": [
66
"VALUE_0",
77
0,
@@ -14,10 +14,36 @@ const ImportedEnum = `{
1414
],
1515
"oneOf": [
1616
{
17-
"type": "string"
17+
"description": "Zero",
18+
"const": "VALUE_0"
1819
},
1920
{
20-
"type": "integer"
21+
"description": "Zero",
22+
"const": 0
23+
},
24+
{
25+
"description": "One",
26+
"const": "VALUE_1"
27+
},
28+
{
29+
"description": "One",
30+
"const": 1
31+
},
32+
{
33+
"description": "Two",
34+
"const": "VALUE_2"
35+
},
36+
{
37+
"description": "Two",
38+
"const": 2
39+
},
40+
{
41+
"description": "Three",
42+
"const": "VALUE_3"
43+
},
44+
{
45+
"description": "Three",
46+
"const": 3
2147
}
2248
],
2349
"description": "This is an enum"

0 commit comments

Comments
 (0)