From 6ac766b42d586f2714fb5e83e8d20c9784c076ed Mon Sep 17 00:00:00 2001 From: Prawn Date: Sat, 18 Sep 2021 21:34:42 +1200 Subject: [PATCH] Adding a REQUIRED option (#78) * Adding a REQUIRED option * Extra docs Co-authored-by: chris --- README.md | 9 ++++++++ internal/converter/converter.go | 21 ++++++++++-------- internal/converter/converter_test.go | 5 +++++ .../testdata/proto/Proto3Required.proto | 9 ++++++++ .../converter/testdata/proto3_required.go | 22 +++++++++++++++++++ internal/converter/types.go | 7 ++++++ options.proto | 1 + 7 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 internal/converter/testdata/proto/Proto3Required.proto create mode 100644 internal/converter/testdata/proto3_required.go diff --git a/README.md b/README.md index 91f3e62f..57cf217c 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,15 @@ protoc \ --proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto ``` +### Custom option to ignore specific fields + +Use the custom 'ignore' option on the fields you'd like to omit from generated schemas, then use the "exclude_ignored_fields" flag with your protoc command. + +### Custom option to mark fields as required + +Use the custom 'required' option on the fields you'd like to mark as required in generated schemas. + + ## Sample protos (for testing) * Proto with a simple (flat) structure: [samples.PayloadMessage](internal/converter/testdata/proto/PayloadMessage.proto) diff --git a/internal/converter/converter.go b/internal/converter/converter.go index e9decf61..cba9ce34 100644 --- a/internal/converter/converter.go +++ b/internal/converter/converter.go @@ -17,17 +17,19 @@ import ( ) const ( - ignoredFieldOption = "50505:1" - messageDelimiter = "+" + ignoredFieldOption = "50505:1" + messageDelimiter = "+" + requiredFieldOption = "50515:1" ) // Converter is everything you need to convert protos to JSONSchemas: type Converter struct { - Flags ConverterFlags - ignoredFieldOption string - logger *logrus.Logger - sourceInfo *sourceCodeInfo - messageTargets []string + Flags ConverterFlags + ignoredFieldOption string + logger *logrus.Logger + requiredFieldOption string + sourceInfo *sourceCodeInfo + messageTargets []string } // ConverterFlags control the behaviour of the converter: @@ -46,8 +48,9 @@ type ConverterFlags struct { // New returns a configured *Converter: func New(logger *logrus.Logger) *Converter { return &Converter{ - ignoredFieldOption: ignoredFieldOption, - logger: logger, + ignoredFieldOption: ignoredFieldOption, + logger: logger, + requiredFieldOption: requiredFieldOption, } } diff --git a/internal/converter/converter_test.go b/internal/converter/converter_test.go index 2392b0cc..96ecf74c 100644 --- a/internal/converter/converter_test.go +++ b/internal/converter/converter_test.go @@ -274,6 +274,11 @@ func configureSampleProtos() map[string]sampleProto { FilesToGenerate: []string{"options.proto", "HiddenFields.proto"}, ProtoFileName: "HiddenFields.proto", }, + "Proto3Required": { + ExpectedJSONSchema: []string{testdata.Proto3Required}, + FilesToGenerate: []string{"options.proto", "Proto3Required.proto"}, + ProtoFileName: "Proto3Required.proto", + }, } } diff --git a/internal/converter/testdata/proto/Proto3Required.proto b/internal/converter/testdata/proto/Proto3Required.proto new file mode 100644 index 00000000..ff5263f6 --- /dev/null +++ b/internal/converter/testdata/proto/Proto3Required.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; +package samples; +import "options.proto"; + +message Proto3Required { + string query = 1 [(protoc.gen.jsonschema.required) = true]; + int32 page_number = 2 [deprecated = true, (protoc.gen.jsonschema.required) = true]; + int32 result_per_page = 3; +} diff --git a/internal/converter/testdata/proto3_required.go b/internal/converter/testdata/proto3_required.go new file mode 100644 index 00000000..1ace9432 --- /dev/null +++ b/internal/converter/testdata/proto3_required.go @@ -0,0 +1,22 @@ +package testdata + +const Proto3Required = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "required": [ + "query", + "page_number" + ], + "properties": { + "query": { + "type": "string" + }, + "page_number": { + "type": "integer" + }, + "result_per_page": { + "type": "integer" + } + }, + "additionalProperties": true, + "type": "object" +}` diff --git a/internal/converter/types.go b/internal/converter/types.go index 6f757dd6..b25fad7f 100644 --- a/internal/converter/types.go +++ b/internal/converter/types.go @@ -502,6 +502,7 @@ func (c *Converter) recursiveConvertMessageType(curPkg *ProtoPackage, msg *descr continue } + // Convert the field into a JSONSchema type: recursedJSONSchemaType, err := c.convertField(curPkg, fieldDesc, msg, duplicatedMessages) if err != nil { c.logger.WithError(err).WithField("field_name", fieldDesc.GetName()).WithField("message_name", msg.GetName()).Error("Failed to convert field") @@ -529,6 +530,12 @@ func (c *Converter) recursiveConvertMessageType(curPkg *ProtoPackage, msg *descr if fieldDesc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REQUIRED && fieldDesc.OneofIndex == nil { jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetName()) } + + // Look for our custom proto3 "required" field-option (and hope that nobody else happens to be using our number): + if strings.Contains(fieldDesc.GetOptions().String(), c.requiredFieldOption) { + c.logger.WithField("field_name", fieldDesc.GetName()).WithField("message_name", msg.GetName()).Debug("Marking required field") + jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetName()) + } } // Remove empty properties to keep the final output as clean as possible: diff --git a/options.proto b/options.proto index 96e55d80..ebc391ea 100644 --- a/options.proto +++ b/options.proto @@ -4,4 +4,5 @@ import "google/protobuf/descriptor.proto"; extend google.protobuf.FieldOptions { bool ignore = 50505; + bool required = 50515; }