From 3633499d2cc6e8a12c35a86c39f14e84ea5b6ee5 Mon Sep 17 00:00:00 2001 From: Cassondra Foesch Date: Fri, 13 Sep 2024 13:59:54 +0000 Subject: [PATCH] PIE-176 integrate recent changes --- .gitignore | 1 + Makefile | 71 ++- README.md | 86 ++- cmd/protoc-gen-jsonschema/main.go | 38 +- go.mod | 33 +- go.sum | 51 +- internal/converter/converter.go | 292 +++++++-- internal/converter/converter_test.go | 500 ++++++++++----- internal/converter/proto_package.go | 112 +++- internal/converter/sourcecodeinfo.go | 82 ++- internal/converter/sourcecodeinfo_test.go | 42 +- internal/converter/testdata/array_of_enums.go | 61 +- .../converter/testdata/array_of_messages.go | 128 ++-- .../converter/testdata/array_of_objects.go | 232 ++++--- .../converter/testdata/array_of_primitives.go | 359 +++++------ internal/converter/testdata/bytes_payload.go | 25 + .../converter/testdata/cyclical_reference.go | 216 +++++++ internal/converter/testdata/enum_ception.go | 149 ++--- .../testdata/enum_nested_reference.go | 44 ++ .../converter/testdata/enum_with_message.go | 38 ++ internal/converter/testdata/file_options.go | 27 + internal/converter/testdata/first_enum.go | 7 +- internal/converter/testdata/first_message.go | 46 +- .../converter/testdata/google_int64_value.go | 23 + .../testdata/google_int64_value_allow_null.go | 38 ++ .../google_int64_value_disallow_string.go | 23 + ..._int64_value_disallow_string_allow_null.go | 38 ++ internal/converter/testdata/google_value.go | 49 ++ internal/converter/testdata/imported_enum.go | 40 +- internal/converter/testdata/json_fields.go | 40 ++ internal/converter/testdata/maps.go | 140 +++-- .../converter/testdata/message_kind_10.go | 30 + .../converter/testdata/message_kind_11.go | 136 ++++ .../converter/testdata/message_kind_12.go | 237 +++++++ .../converter/testdata/message_options.go | 35 ++ .../testdata/message_with_comments.go | 27 +- internal/converter/testdata/nested_message.go | 44 +- internal/converter/testdata/nested_object.go | 40 +- internal/converter/testdata/oneof.go | 98 +++ .../converter/testdata/payload_message.go | 87 +-- .../converter/testdata/payload_message_2.go | 76 +++ .../testdata/proto/ArrayOfPrimitives.proto | 2 +- .../testdata/proto/BytesPayload.proto | 7 + .../testdata/proto/CyclicalReference.proto | 21 + .../testdata/proto/EnumNestedReference.proto | 8 + .../testdata/proto/EnumWithMessage.proto | 12 + .../testdata/proto/Enumception.proto | 4 +- .../testdata/proto/GoogleInt64Value.proto | 8 + .../proto/GoogleInt64ValueAllowNull.proto | 8 + .../GoogleInt64ValueDisallowString.proto | 8 + ...gleInt64ValueDisallowStringAllowNull.proto | 8 + .../testdata/proto/GoogleValue.proto | 10 + .../testdata/proto/ImportedEnum.proto | 9 - .../converter/testdata/proto/JSONFields.proto | 13 + .../testdata/proto/MessageWithComments.proto | 5 + .../testdata/proto/NestedMessage.proto | 2 +- .../converter/testdata/proto/NoPackage.proto | 9 + internal/converter/testdata/proto/OneOf.proto | 20 + .../testdata/proto/PayloadMessage2.proto | 24 + .../testdata/proto/Proto2NestedMessage.proto | 25 + .../testdata/proto/Proto2NestedObject.proto | 25 + .../testdata/proto/Proto2Required.proto | 8 + .../testdata/proto/SelfReference.proto | 7 + .../converter/testdata/proto/Timestamp.proto | 8 + .../testdata/proto/TwelveMessages.proto | 109 ++++ .../converter/testdata/proto/WellKnown.proto | 5 + .../testdata/proto2_nested_message.go | 90 +++ .../testdata/proto2_nested_object.go | 99 +++ .../testdata/proto2_payload_message.go | 69 +++ .../converter/testdata/proto2_required.go | 36 ++ internal/converter/testdata/second_enum.go | 7 +- internal/converter/testdata/second_message.go | 46 +- internal/converter/testdata/self_reference.go | 45 ++ internal/converter/testdata/timestamp.go | 23 + .../testdata/type_names_with_no_package.go | 85 +++ internal/converter/testdata/wellknown.go | 83 +-- internal/converter/types.go | 584 +++++++++++++----- jsonschemas/Enumception.jsonschema | 12 +- 78 files changed, 4196 insertions(+), 1189 deletions(-) create mode 100644 internal/converter/testdata/bytes_payload.go create mode 100644 internal/converter/testdata/cyclical_reference.go create mode 100644 internal/converter/testdata/enum_nested_reference.go create mode 100644 internal/converter/testdata/enum_with_message.go create mode 100644 internal/converter/testdata/file_options.go create mode 100644 internal/converter/testdata/google_int64_value.go create mode 100644 internal/converter/testdata/google_int64_value_allow_null.go create mode 100644 internal/converter/testdata/google_int64_value_disallow_string.go create mode 100644 internal/converter/testdata/google_int64_value_disallow_string_allow_null.go create mode 100644 internal/converter/testdata/google_value.go create mode 100644 internal/converter/testdata/json_fields.go create mode 100644 internal/converter/testdata/message_kind_10.go create mode 100644 internal/converter/testdata/message_kind_11.go create mode 100644 internal/converter/testdata/message_kind_12.go create mode 100644 internal/converter/testdata/message_options.go create mode 100644 internal/converter/testdata/oneof.go create mode 100644 internal/converter/testdata/payload_message_2.go create mode 100644 internal/converter/testdata/proto/BytesPayload.proto create mode 100644 internal/converter/testdata/proto/CyclicalReference.proto create mode 100644 internal/converter/testdata/proto/EnumNestedReference.proto create mode 100644 internal/converter/testdata/proto/EnumWithMessage.proto create mode 100644 internal/converter/testdata/proto/GoogleInt64Value.proto create mode 100644 internal/converter/testdata/proto/GoogleInt64ValueAllowNull.proto create mode 100644 internal/converter/testdata/proto/GoogleInt64ValueDisallowString.proto create mode 100644 internal/converter/testdata/proto/GoogleInt64ValueDisallowStringAllowNull.proto create mode 100644 internal/converter/testdata/proto/GoogleValue.proto delete mode 100644 internal/converter/testdata/proto/ImportedEnum.proto create mode 100644 internal/converter/testdata/proto/JSONFields.proto create mode 100644 internal/converter/testdata/proto/NoPackage.proto create mode 100644 internal/converter/testdata/proto/OneOf.proto create mode 100644 internal/converter/testdata/proto/PayloadMessage2.proto create mode 100644 internal/converter/testdata/proto/Proto2NestedMessage.proto create mode 100644 internal/converter/testdata/proto/Proto2NestedObject.proto create mode 100644 internal/converter/testdata/proto/Proto2Required.proto create mode 100644 internal/converter/testdata/proto/SelfReference.proto create mode 100644 internal/converter/testdata/proto/Timestamp.proto create mode 100644 internal/converter/testdata/proto/TwelveMessages.proto create mode 100644 internal/converter/testdata/proto2_nested_message.go create mode 100644 internal/converter/testdata/proto2_nested_object.go create mode 100644 internal/converter/testdata/proto2_payload_message.go create mode 100644 internal/converter/testdata/proto2_required.go create mode 100644 internal/converter/testdata/self_reference.go create mode 100644 internal/converter/testdata/timestamp.go create mode 100644 internal/converter/testdata/type_names_with_no_package.go diff --git a/.gitignore b/.gitignore index a09c56df..98eba867 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.idea +bin/ diff --git a/Makefile b/Makefile index 33759a7e..7b1af59f 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,56 @@ +PROTO_PATH ?= "internal/converter/testdata/proto" + default: build +.PHONY: build build: - @echo "Generating binary (protoc-gen-jsonschema) ..." - @mkdir -p bin - @go build -o bin/protoc-gen-jsonschema cmd/protoc-gen-jsonschema/main.go + mkdir -p bin + go build -o bin/protoc-gen-jsonschema cmd/protoc-gen-jsonschema/main.go +.PHONY: fmt +fmt: + gofmt -s -w . + goimports -w -local github.com/chrusty/protoc-gen-jsonschema . + +.PHONY: install install: - @GO111MODULE=on go get -u github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema && go install github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema + go install github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema +.PHONY: build_linux build_linux: - @echo "Generating Linux-amd64 binary (protoc-gen-jsonschema.linux-amd64) ..." - @GOOS=linux GOARCH=amd64 go build -o protoc-gen-jsonschema.linux-amd64 + GOOS=linux GOARCH=amd64 go build -o protoc-gen-jsonschema.linux-amd64 -PROTO_PATH ?= "internal/converter/testdata/proto" -samples: - @echo "Generating sample JSON-Schemas ..." - @mkdir -p jsonschemas - @PATH=./bin:$$PATH; protoc --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfMessages.proto 2>/dev/null || echo "No messages found (ArrayOfMessages.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfObjects.proto 2>/dev/null || echo "No messages found (ArrayOfObjects.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfPrimitives.proto 2>/dev/null || echo "No messages found (ArrayOfPrimitives.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Enumception.proto 2>/dev/null || echo "No messages found (Enumception.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ImportedEnum.proto 2>/dev/null || echo "No messages found (ImportedEnum.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_additional_properties:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/NestedMessage.proto 2>/dev/null || echo "No messages found (NestedMessage.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/NestedObject.proto 2>/dev/null || echo "No messages found (NestedObject.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/PayloadMessage.proto 2>/dev/null || echo "No messages found (PayloadMessage.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/SeveralEnums.proto 2>/dev/null || echo "No messages found (SeveralEnums.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/SeveralMessages.proto 2>/dev/null || echo "No messages found (SeveralMessages.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfEnums.proto 2>/dev/null || echo "No messages found (SeveralMessages.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Maps.proto 2>/dev/null || echo "No messages found (Maps.proto)" - @PATH=./bin:$$PATH; protoc --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/MessageWithComments.proto 2>/dev/null || echo "No messages found (MessageWithComments.proto)" - @PATH=./bin:$$PATH; protoc -I /usr/include --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/WellKnown.proto +.PHONY: samples +samples: build + mkdir -p jsonschemas + 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)" + 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)" + 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)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/Enumception.proto || echo "No messages found (Enumception.proto)" + 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)" + 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)" + 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)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/SeveralEnums.proto || echo "No messages found (SeveralEnums.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/SeveralMessages.proto || echo "No messages found (SeveralMessages.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Timestamp.proto || echo "No messages found (Timestamp.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=all_fields_required:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/PayloadMessage2.proto || echo "No messages found (PayloadMessage2.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=json_fieldnames:jsonschemas -I. --proto_path=${PROTO_PATH} ${PROTO_PATH}/JSONFields.proto || echo "No messages found (JSONFields.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/ArrayOfEnums.proto || echo "No messages found (SeveralMessages.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Maps.proto || echo "No messages found (Maps.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/MessageWithComments.proto || echo "No messages found (MessageWithComments.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Proto2Required.proto || echo "No messages found (Proto2Required.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Proto2NestedMessage.proto || echo "No messages found (Proto2NestedMessage.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleValue.proto || echo "No messages found (GoogleValue.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleInt64Value.proto || echo "No messages found (GoogleInt64Value.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleInt64ValueDisallowString.proto || echo "No messages found (GoogleInt64ValueDisallowString.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleInt64ValueAllowNull.proto || echo "No messages found (GoogleInt64ValueAllowNull.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=disallow_bigints_as_strings,allow_null_values:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/GoogleInt64ValueDisallowStringAllowNull.proto || echo "No messages found (GoogleInt64ValueDisallowStringAllowNull.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=enforce_oneof:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/OneOf.proto || echo "No messages found (OneOf.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=all_fields_required:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/Proto2NestedObject.proto || echo "No messages found (Proto2NestedObject.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/WellKnown.proto || echo "No messages found (WellKnown.proto)" + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/NoPackage.proto + protoc --plugin=bin/protoc-gen-jsonschema --jsonschema_out=messages=[MessageKind10+MessageKind11+MessageKind12]:jsonschemas --proto_path=${PROTO_PATH} ${PROTO_PATH}/TwelveMessages.proto || echo "No messages found (TwelveMessages.proto)" +.PHONY: test test: - @go test ./... -cover + go test ./... -cover -v diff --git a/README.md b/README.md index ef97355e..f832c6bf 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,80 @@ Protobuf to JSON-Schema compiler ================================ + This takes protobuf definitions and converts them into JSONSchemas, which can be used to dynamically validate JSON messages. -This will hopefully be useful for people who define their data using ProtoBuf, but use JSON for the "wire" format. +Useful for people who define their data using ProtoBuf, but use JSON for the "wire" format. "Heavily influenced" by [Google's protobuf-to-BigQuery-schema compiler](https://github.com/GoogleCloudPlatform/protoc-gen-bq-schema). +Generated Schemas +----------------- + +- One JSONSchema file is generated for each root-level proto message and ENUM. These are intended to be stand alone self-contained schemas which can be used to validate a payload derived from their source proto message +- Nested message schemas become [referenced "definitions"](https://cswr.github.io/JsonSchema/spec/definitions_references/). This means that you know the name of the proto message they came from, and their schema is not duplicated (within the context of one JSONSchema file at least) + + +Logic +----- + +- For each proto file provided + - Generates schema for each ENUM + - JSONSchema filename deried from ENUM name + - Generates schema for each Message + - Builds a list of every nested message and converts them to JSONSchema + - Recursively converts attributes and nested messages within the root message + - Special handling for "OneOf" + - Special handling for arrays + - Special handling for maps + - Injects references to nested messages + - JSONSchema filename derived from Message name + - Bundles these into a protoc generator response + + Installation ------------ -`GO111MODULE=on go get -u github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema && go install github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema` -Links ------ -* [About JSON Schema](http://json-schema.org/) -* [Popular GoLang JSON-Schema validation library](https://github.com/xeipuuv/gojsonschema) -* [Another GoLang JSON-Schema validation library](https://github.com/lestrrat/go-jsschema) +> Note: This tool requires Go 1.11+ to be installed. + +Install this plugin using Go: + +```sh +go install github.com/chrusty/protoc-gen-jsonschema/cmd/protoc-gen-jsonschema@latest +``` Usage ----- -* Allow NULL values (by default, JSONSchemas will reject NULL values unless we explicitly allow them): - `protoc --jsonschema_out=allow_null_values:. --proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto` -* Disallow additional properties (JSONSchemas won't validate JSON containing extra parameters): - `protoc --jsonschema_out=disallow_additional_properties:. --proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto` -* Disallow permissive validation of big-integers as strings (eg scientific notation): - `protoc --jsonschema_out=disallow_bigints_as_strings:. --proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto` -* Enable debug logging: - `protoc --jsonschema_out=debug:. --proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto` +> Note: This plugin requires the [`protoc`](https://github.com/protocolbuffers/protobuf) CLI to be installed. + +**protoc-gen-jsonschema** is designed to run like any other proto generator. + +```sh +protoc \ # The protobuf compiler +--proto_path=testdata/proto testdata/proto/ArrayOfPrimitives.proto # proto input directories and folders +``` Sample protos (for testing) --------------------------- -* Proto with a simple (flat) structure: [samples.PayloadMessage](testdata/proto/PayloadMessage.proto) -* Proto containing a nested object (defined internally): [samples.NestedObject](testdata/proto/NestedObject.proto) -* Proto containing a nested message (defined in a different proto file): [samples.NestedMessage](testdata/proto/NestedMessage.proto) -* Proto containing an array of a primitive types (string, int): [samples.ArrayOfPrimitives](testdata/proto/ArrayOfPrimitives.proto) -* Proto containing an array of objects (internally defined): [samples.ArrayOfObjects](testdata/proto/ArrayOfObjects.proto) -* Proto containing an array of messages (defined in a different proto file): [samples.ArrayOfMessage](testdata/proto/ArrayOfMessage.proto) -* Proto containing multi-level enums (flat and nested and arrays): [samples.Enumception](testdata/proto/Enumception.proto) -* Proto containing a stand-alone enum: [samples.ImportedEnum](testdata/proto/ImportedEnum.proto) -* Proto containing 2 stand-alone enums: [samples.FirstEnum, samples.SecondEnum](testdata/proto/SeveralEnums.proto) -* Proto containing 2 messages: [samples.FirstMessage, samples.SecondMessage](testdata/proto/SeveralMessages.proto) + +* Proto with a simple (flat) structure: [samples.PayloadMessage](internal/converter/testdata/proto/PayloadMessage.proto) +* Proto containing a nested object (defined internally): [samples.NestedObject](internal/converter/testdata/proto/NestedObject.proto) +* Proto containing a nested message (defined in a different proto file): [samples.NestedMessage](internal/converter/testdata/proto/NestedMessage.proto) +* Proto containing an array of a primitive types (string, int): [samples.ArrayOfPrimitives](internal/converter/testdata/proto/ArrayOfPrimitives.proto) +* Proto containing an array of objects (internally defined): [samples.ArrayOfObjects](internal/converter/testdata/proto/ArrayOfObjects.proto) +* Proto containing an array of messages (defined in a different proto file): [samples.ArrayOfMessage](internal/converter/testdata/proto/ArrayOfMessage.proto) +* Proto containing multi-level enums (flat and nested and arrays): [samples.Enumception](internal/converter/testdata/proto/Enumception.proto) +* Proto containing a stand-alone enum: [samples.ImportedEnum](internal/converter/testdata/proto/ImportedEnum.proto) +* Proto containing 2 stand-alone enums: [samples.FirstEnum, samples.SecondEnum](internal/converter/testdata/proto/SeveralEnums.proto) +* Proto containing 2 messages: [samples.FirstMessage, samples.SecondMessage](internal/converter/testdata/proto/SeveralMessages.proto) +* Proto containing 12 messages: [samples.MessageKind1 - samples.MessageKind12](internal/converter/testdata/proto/TwelveMessages.proto) + + +Links +----- + +* [About JSON Schema](http://json-schema.org/) +* [Popular GoLang JSON-Schema validation library](https://github.com/xeipuuv/gojsonschema) +* [Another GoLang JSON-Schema validation library](https://github.com/lestrrat/go-jsschema) diff --git a/cmd/protoc-gen-jsonschema/main.go b/cmd/protoc-gen-jsonschema/main.go index be84cbff..a38adacc 100644 --- a/cmd/protoc-gen-jsonschema/main.go +++ b/cmd/protoc-gen-jsonschema/main.go @@ -1,22 +1,35 @@ // protoc plugin which converts .proto to JSON schema -// It is spawned by protoc and generates JSON-schema files. +// It is called by protoc and generates JSON-schema files. // "Heavily influenced" by Google's "protog-gen-bq-schema" // // usage: -// $ bin/protoc --jsonschema_out=path/to/outdir foo.proto // +// $ bin/protoc --jsonschema_out=path/to/outdir foo.proto package main import ( + "flag" "fmt" "os" - "github.com/golang/protobuf/proto" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" "github.com/sirupsen/logrus" - "github.com/sixt/protoc-gen-jsonschema/internal/converter" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/pluginpb" + + "github.com/chrusty/protoc-gen-jsonschema/internal/converter" ) +const version = "v1.4.0" + +func init() { + versionFlag := flag.Bool("version", false, "prints current version") + flag.Parse() + if *versionFlag { + fmt.Println(version) + os.Exit(0) + } +} + func main() { // Make a Logrus logger (default to INFO): @@ -28,15 +41,14 @@ func main() { protoConverter := converter.New(logger) // Convert the generator request: - var ok = true + var failed bool logger.Debug("Processing code generator request") res, err := protoConverter.ConvertFrom(os.Stdin) if err != nil { - ok = false + failed = true if res == nil { - message := fmt.Sprintf("Failed to read input: %v", err) - res = &plugin.CodeGeneratorResponse{ - Error: &message, + res = &pluginpb.CodeGeneratorResponse{ + Error: proto.String(fmt.Sprintf("Failed to read input: %v", err)), } } } @@ -51,10 +63,10 @@ func main() { logger.WithError(err).Fatal("Failed to write response") } - if ok { - logger.Debug("Succeeded to process code generator request") - } else { + if failed { logger.Warn("Failed to process code generator but successfully sent the error to protoc") os.Exit(1) } + + logger.Debug("Succeeded to process code generator request") } diff --git a/go.mod b/go.mod index d3a7a308..95c54901 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,24 @@ -module github.com/sixt/protoc-gen-jsonschema +module github.com/chrusty/protoc-gen-jsonschema + +go 1.23 require ( - github.com/alecthomas/jsonschema v0.0.0-20200127222324-dd4542c1f589 - github.com/golang/protobuf v1.3.2 - github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 - github.com/sirupsen/logrus v1.4.2 - github.com/stretchr/testify v1.4.0 - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.1.0 + github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b + github.com/fatih/camelcase v1.0.0 + github.com/google/go-cmp v0.5.5 + github.com/iancoleman/orderedmap v0.3.0 + github.com/iancoleman/strcase v0.3.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.9.0 + github.com/xeipuuv/gojsonschema v1.2.0 + google.golang.org/protobuf v1.34.2 ) -replace ( - github.com/alecthomas/jsonschema => github.com/alecthomas/jsonschema v0.0.0-20200127222324-dd4542c1f589 - github.com/iancoleman/orderedmap => github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) - -go 1.13 diff --git a/go.sum b/go.sum index 8f066858..00fb7780 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,41 @@ -github.com/alecthomas/jsonschema v0.0.0-20200127222324-dd4542c1f589 h1:Ev3H/smEOziBuJdLm7J4JI6baieXE8RVMNBejT/hq3Q= -github.com/alecthomas/jsonschema v0.0.0-20200127222324-dd4542c1f589/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b h1:doCpXjVwui6HUN+xgNsNS3SZ0/jUZ68Eb+mJRNOZfog= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= -github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/converter/converter.go b/internal/converter/converter.go index cf5905b3..aae027d5 100644 --- a/internal/converter/converter.go +++ b/internal/converter/converter.go @@ -6,34 +6,73 @@ import ( "io" "io/ioutil" "path" + "regexp" "strings" "github.com/alecthomas/jsonschema" - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" + "github.com/iancoleman/strcase" "github.com/sirupsen/logrus" + "github.com/xeipuuv/gojsonschema" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" +) + +const ( + defaultCommentDelimiter = " " + defaultExcludeCommentToken = "@exclude" + defaultFileExtension = "json" + defaultPackageName = "package" + defaultRefPrefix = "#/definitions/" + messageDelimiter = "+" + versionDraft04 = "http://json-schema.org/draft-04/schema#" + versionDraft06 = "http://json-schema.org/draft-06/schema#" ) // Converter is everything you need to convert protos to JSONSchemas: type Converter struct { + Flags ConverterFlags + commentDelimiter string + excludeCommentToken string + logger *logrus.Logger + refPrefix string + schemaFileExtension string + schemaVersion string + sourceInfo *sourceCodeInfo + messageTargets []string +} + +// ConverterFlags control the behaviour of the converter: +type ConverterFlags struct { + AllFieldsRequired bool AllowNullValues bool DisallowAdditionalProperties bool DisallowBigIntsAsStrings bool - UseProtoAndJSONFieldnames bool - logger *logrus.Logger - sourceInfo *sourceCodeInfo + EnforceOneOf bool + EnumsAsConstants bool + EnumsAsStringsOnly bool + EnumsTrimPrefix bool + KeepNewLinesInDescription bool + PrefixSchemaFilesWithPackage bool + UseJSONFieldnamesOnly bool + UseProtoAndJSONFieldNames bool + TypeNamesWithNoPackage bool } -// New returns a configured *Converter: +// New returns a configured *Converter (defaulting to draft-04 version): func New(logger *logrus.Logger) *Converter { return &Converter{ - logger: logger, + commentDelimiter: defaultCommentDelimiter, + excludeCommentToken: defaultExcludeCommentToken, + logger: logger, + refPrefix: defaultRefPrefix, + schemaFileExtension: defaultFileExtension, + schemaVersion: versionDraft04, } } // ConvertFrom tells the convert to work on the given input: -func (c *Converter) ConvertFrom(rd io.Reader) (*plugin.CodeGeneratorResponse, error) { +func (c *Converter) ConvertFrom(rd io.Reader) (*pluginpb.CodeGeneratorResponse, error) { c.logger.Debug("Reading code generation request") input, err := ioutil.ReadAll(rd) if err != nil { @@ -41,92 +80,168 @@ func (c *Converter) ConvertFrom(rd io.Reader) (*plugin.CodeGeneratorResponse, er return nil, err } - req := &plugin.CodeGeneratorRequest{} + req := &pluginpb.CodeGeneratorRequest{} err = proto.Unmarshal(input, req) if err != nil { c.logger.WithError(err).Error("Can't unmarshal input") return nil, err } - c.parseGeneratorParameters(req.GetParameter()) - c.logger.Debug("Converting input") return c.convert(req) - // return c.debugger(req) } func (c *Converter) parseGeneratorParameters(parameters string) { for _, parameter := range strings.Split(parameters, ",") { switch parameter { + case "all_fields_required": + c.Flags.AllFieldsRequired = true case "allow_null_values": - c.AllowNullValues = true + c.Flags.AllowNullValues = true case "debug": c.logger.SetLevel(logrus.DebugLevel) case "disallow_additional_properties": - c.DisallowAdditionalProperties = true + c.Flags.DisallowAdditionalProperties = true case "disallow_bigints_as_strings": - c.DisallowBigIntsAsStrings = true + c.Flags.DisallowBigIntsAsStrings = true + case "enforce_oneof": + c.Flags.EnforceOneOf = true + case "enums_as_strings_only": + c.Flags.EnumsAsStringsOnly = true + case "enums_trim_prefix": + c.Flags.EnumsTrimPrefix = true + case "json_fieldnames": + c.Flags.UseJSONFieldnamesOnly = true + case "prefix_schema_files_with_package": + c.Flags.PrefixSchemaFilesWithPackage = true case "proto_and_json_fieldnames": - c.UseProtoAndJSONFieldnames = true + c.Flags.UseProtoAndJSONFieldNames = true + case "type_names_with_no_package": + c.Flags.TypeNamesWithNoPackage = true + } + + // look for specific message targets + // message types are separated by messageDelimiter "+" + // examples: + // messages=[foo+bar] + // messages=[foo] + rx := regexp.MustCompile(`messages=\[([^\]]+)\]`) + if matches := rx.FindStringSubmatch(parameter); len(matches) == 2 { + c.messageTargets = strings.Split(matches[1], messageDelimiter) + } + + // Configure custom file extension: + if parameterParts := strings.Split(parameter, "file_extension="); len(parameterParts) == 2 { + c.schemaFileExtension = parameterParts[1] } } } // Converts a proto "ENUM" into a JSON-Schema: -func (c *Converter) convertEnumType(enum *descriptor.EnumDescriptorProto) (jsonschema.Type, error) { +func (c *Converter) convertEnumType(enum *descriptorpb.EnumDescriptorProto, converterFlags ConverterFlags) (jsonschema.Type, error) { // Prepare a new jsonschema.Type for our eventual return value: - jsonSchemaType := jsonschema.Type{ - Version: jsonschema.Version, - } + jsonSchemaType := jsonschema.Type{} + + // Inherit the CLI converterFlags: + converterFlags.EnumsAsStringsOnly = c.Flags.EnumsAsStringsOnly - // Generate a description from src comments (if available) + // Generate a description from src comments (if available): if src := c.sourceInfo.GetEnum(enum); src != nil { - jsonSchemaType.Description = formatDescription(src) + jsonSchemaType.Title, jsonSchemaType.Description = c.formatTitleAndDescription(enum.GetName(), src) + } + + // Use basic types if we're not opting to use constants for ENUMs: + if !converterFlags.EnumsAsConstants { + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) + if !converterFlags.EnumsAsStringsOnly { + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_INTEGER}) + } } - // Allow both strings and integers: - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "string"}) - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "integer"}) + // Optionally allow NULL values: + if converterFlags.AllowNullValues { + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL}) + } + + // If we end up with just one option in OneOf, unwrap it + if len(jsonSchemaType.OneOf) == 1 { + jsonSchemaType.Type = jsonSchemaType.OneOf[0].Type + jsonSchemaType.OneOf = nil + } + + // If we need to trim prefix from enum value + enumNamePrefix := fmt.Sprintf("%s_", strcase.ToScreamingSnake(*enum.Name)) + + // We have found an enum, append its values: + for _, value := range enum.Value { + + // Each ENUM value can have comments too: + var valueDescription string + if src := c.sourceInfo.GetEnumValue(value); src != nil { + _, valueDescription = c.formatTitleAndDescription("", src) + } - // Add the allowed values: - for _, enumValue := range enum.Value { - jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Name) - jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Number) + valueName := value.GetName() + + // If enum name prefix should be removed from enum value name: + if converterFlags.EnumsTrimPrefix { + valueName = strings.TrimPrefix(valueName, enumNamePrefix) + } + + // If we're using constants for ENUMs then add these here, along with their title: + if converterFlags.EnumsAsConstants { + c.schemaVersion = versionDraft06 // Const requires draft-06 + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Extras: map[string]interface{}{"const": valueName}, Description: valueDescription}) + if !converterFlags.EnumsAsStringsOnly { + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Extras: map[string]interface{}{"const": value.GetNumber()}, Description: valueDescription}) + } + } + + // Add the values to the ENUM: + jsonSchemaType.Enum = append(jsonSchemaType.Enum, valueName) + if !converterFlags.EnumsAsStringsOnly { + jsonSchemaType.Enum = append(jsonSchemaType.Enum, value.Number) + } } return jsonSchemaType, nil } // Converts a proto file into a JSON-Schema: -func (c *Converter) convertFile(file *descriptor.FileDescriptorProto) ([]*plugin.CodeGeneratorResponse_File, error) { +func (c *Converter) convertFile(file *descriptorpb.FileDescriptorProto, fileExtension string) ([]*pluginpb.CodeGeneratorResponse_File, error) { // Input filename: protoFileName := path.Base(file.GetName()) // Prepare a list of responses: - response := []*plugin.CodeGeneratorResponse_File{} + var response []*pluginpb.CodeGeneratorResponse_File + + // user wants specific messages + genSpecificMessages := len(c.messageTargets) > 0 // Warn about multiple messages / enums in files: - if len(file.GetMessageType()) > 1 { - c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Warn("protoc-gen-jsonschema will create multiple MESSAGE schemas from one proto file") + if !genSpecificMessages && len(file.GetMessageType()) > 1 { + c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Debug("protoc-gen-jsonschema will create multiple MESSAGE schemas from one proto file") } + if len(file.GetEnumType()) > 1 { - c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Warn("protoc-gen-jsonschema will create multiple ENUM schemas from one proto file") + c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Debug("protoc-gen-jsonschema will create multiple ENUM schemas from one proto file") } // Generate standalone ENUMs: if len(file.GetMessageType()) == 0 { for _, enum := range file.GetEnumType() { - jsonSchemaFileName := fmt.Sprintf("%s.jsonschema", enum.GetName()) + jsonSchemaFileName := c.generateSchemaFilename(file, fileExtension, enum.GetName()) c.logger.WithField("proto_filename", protoFileName).WithField("enum_name", enum.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for stand-alone ENUM") // Convert the ENUM: - enumJSONSchema, err := c.convertEnumType(enum) + enumJSONSchema, err := c.convertEnumType(enum, ConverterFlags{}) if err != nil { c.logger.WithError(err).WithField("proto_filename", protoFileName).Error("Failed to convert") return nil, err } + enumJSONSchema.Version = c.schemaVersion // Marshal the JSON-Schema into JSON: jsonSchemaJSON, err := json.MarshalIndent(enumJSONSchema, "", " ") @@ -136,7 +251,7 @@ func (c *Converter) convertFile(file *descriptor.FileDescriptorProto) ([]*plugin } // Add a response: - resFile := &plugin.CodeGeneratorResponse_File{ + resFile := &pluginpb.CodeGeneratorResponse_File{ Name: proto.String(jsonSchemaFileName), Content: proto.String(string(jsonSchemaJSON)), } @@ -148,17 +263,26 @@ func (c *Converter) convertFile(file *descriptor.FileDescriptorProto) ([]*plugin if !ok { return nil, fmt.Errorf("no such package found: %s", file.GetPackage()) } - for _, msg := range file.GetMessageType() { - jsonSchemaFileName := fmt.Sprintf("%s.jsonschema", msg.GetName()) - c.logger.WithField("proto_filename", protoFileName).WithField("msg_name", msg.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for MESSAGE") + + // Go through all of the messages in this file: + for _, msgDesc := range file.GetMessageType() { + + // skip if we are only generating schema for specific messages + if genSpecificMessages && !contains(c.messageTargets, msgDesc.GetName()) { + continue + } // Convert the message: - messageJSONSchema, err := c.convertMessageType(pkg, msg, "") + messageJSONSchema, err := c.convertMessageType(pkg, msgDesc) if err != nil { c.logger.WithError(err).WithField("proto_filename", protoFileName).Error("Failed to convert") return nil, err } + // Generate a schema filename: + jsonSchemaFileName := c.generateSchemaFilename(file, fileExtension, msgDesc.GetName()) + c.logger.WithField("proto_filename", protoFileName).WithField("msg_name", msgDesc.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for MESSAGE") + // Marshal the JSON-Schema into JSON: jsonSchemaJSON, err := json.MarshalIndent(messageJSONSchema, "", " ") if err != nil { @@ -167,7 +291,7 @@ func (c *Converter) convertFile(file *descriptor.FileDescriptorProto) ([]*plugin } // Add a response: - resFile := &plugin.CodeGeneratorResponse_File{ + resFile := &pluginpb.CodeGeneratorResponse_File{ Name: proto.String(jsonSchemaFileName), Content: proto.String(string(jsonSchemaJSON)), } @@ -178,30 +302,78 @@ func (c *Converter) convertFile(file *descriptor.FileDescriptorProto) ([]*plugin return response, nil } -func (c *Converter) convert(req *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) { +// convert processes a protoc CodeGeneratorRequest: +func (c *Converter) convert(request *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) { + response := &pluginpb.CodeGeneratorResponse{} + + // Parse the various generator parameter flags: + c.parseGeneratorParameters(request.GetParameter()) + + // Prepare a list of target files: generateTargets := make(map[string]bool) - for _, file := range req.GetFileToGenerate() { + for _, file := range request.GetFileToGenerate() { generateTargets[file] = true } - c.sourceInfo = newSourceCodeInfo(req.GetProtoFile()) - res := &plugin.CodeGeneratorResponse{} - for _, file := range req.GetProtoFile() { - for _, msg := range file.GetMessageType() { - c.logger.WithField("msg_name", msg.GetName()).WithField("package_name", file.GetPackage()).Debug("Loading a message") - c.registerType(file.Package, msg) + // Get the source-code info (we use this to map any code comments to JSONSchema descriptions): + c.sourceInfo = newSourceCodeInfo(request.GetProtoFile()) + + // Go through the list of proto files provided by protoc: + for _, fileDesc := range request.GetProtoFile() { + + // Start with the default / global file extension: + fileExtension := c.schemaFileExtension + + // Check that this file has a proto package, and give it one if not: + if fileDesc.GetPackage() == "" { + c.logger.WithField("filename", fileDesc.GetName()).WithField("default_package_name", defaultPackageName).Debug("Proto file doesn't specify a package - assuming the default") + fileDesc.Package = proto.String(defaultPackageName) } - } - for _, file := range req.GetProtoFile() { - if _, ok := generateTargets[file.GetName()]; ok { - c.logger.WithField("filename", file.GetName()).Debug("Converting file") - converted, err := c.convertFile(file) + + // Build a list of any messages specified by this file: + for _, msgDesc := range fileDesc.GetMessageType() { + c.logger.WithField("msg_name", msgDesc.GetName()).WithField("package_name", fileDesc.GetPackage()).Debug("Loading a message") + c.registerType(fileDesc.GetPackage(), msgDesc) + } + + // Build a list of any enums specified by this file: + for _, en := range fileDesc.GetEnumType() { + c.logger.WithField("enum_name", en.GetName()).WithField("package_name", fileDesc.GetPackage()).Debug("Loading an enum") + c.registerEnum(fileDesc.GetPackage(), en) + } + + // Generate schemas for this file: + if _, ok := generateTargets[fileDesc.GetName()]; ok { + c.logger.WithField("filename", fileDesc.GetName()).Debug("Converting file") + converted, err := c.convertFile(fileDesc, fileExtension) if err != nil { - res.Error = proto.String(fmt.Sprintf("Failed to convert %s: %v", file.GetName(), err)) - return res, err + response.Error = proto.String(fmt.Sprintf("Failed to convert %s: %v", fileDesc.GetName(), err)) + return response, err } - res.File = append(res.File, converted...) + response.File = append(response.File, converted...) + } + } + + // This is required in order to "support" optional proto3 fields: + // https://chromium.googlesource.com/external/github.com/protocolbuffers/protobuf/+/refs/heads/master/docs/implementing_proto3_presence.md + response.SupportedFeatures = proto.Uint64(uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)) + + return response, nil +} + +func (c *Converter) generateSchemaFilename(file *descriptorpb.FileDescriptorProto, fileExtension, protoName string) string { + if c.Flags.PrefixSchemaFilesWithPackage { + return fmt.Sprintf("%s/%s.%s", file.GetPackage(), protoName, fileExtension) + } + return fmt.Sprintf("%s.%s", protoName, fileExtension) +} + +func contains(haystack []string, needle string) bool { + for i := 0; i < len(haystack); i++ { + if haystack[i] == needle { + return true } } - return res, nil + + return false } diff --git a/internal/converter/converter_test.go b/internal/converter/converter_test.go index 5361344d..38954f31 100644 --- a/internal/converter/converter_test.go +++ b/internal/converter/converter_test.go @@ -8,51 +8,48 @@ import ( "strings" "testing" - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" + "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" - "github.com/sixt/protoc-gen-jsonschema/internal/converter/testdata" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" + + "github.com/chrusty/protoc-gen-jsonschema/internal/converter/testdata" ) -var ( +const ( sampleProtoDirectory = "testdata/proto" - sampleProtos = make(map[string]sampleProto) ) type sampleProto struct { - AllowNullValues bool - ExpectedJSONSchema []string - FilesToGenerate []string - ProtoFileName string - UseProtoAndJSONFieldNames bool + Flags ConverterFlags + ExpectedFileNames []string + ExpectedJSONSchema []string + FilesToGenerate []string + ObjectsToValidateFail []string + ObjectsToValidatePass []string + ProtoFileName string + TargetedMessages []string } func TestGenerateJsonSchema(t *testing.T) { // Configure the list of sample protos to test, and their expected JSON-Schemas: - configureSampleProtos() + sampleProtos := configureSampleProtos() // Convert the protos, compare the results against the expected JSON-Schemas: - testConvertSampleProto(t, sampleProtos["Comments"]) - testConvertSampleProto(t, sampleProtos["ArrayOfMessages"]) - testConvertSampleProto(t, sampleProtos["ArrayOfObjects"]) - testConvertSampleProto(t, sampleProtos["ArrayOfPrimitives"]) - testConvertSampleProto(t, sampleProtos["ArrayOfPrimitivesDouble"]) - testConvertSampleProto(t, sampleProtos["EnumCeption"]) - testConvertSampleProto(t, sampleProtos["ImportedEnum"]) - testConvertSampleProto(t, sampleProtos["NestedMessage"]) - testConvertSampleProto(t, sampleProtos["NestedObject"]) - testConvertSampleProto(t, sampleProtos["PayloadMessage"]) - testConvertSampleProto(t, sampleProtos["SeveralEnums"]) - testConvertSampleProto(t, sampleProtos["SeveralMessages"]) - testConvertSampleProto(t, sampleProtos["ArrayOfEnums"]) - testConvertSampleProto(t, sampleProtos["Maps"]) - testConvertSampleProto(t, sampleProtos["WellKnown"]) + for name, sampleProto := range sampleProtos { + t.Run(name, func(t *testing.T) { + testConvertSampleProto(t, sampleProto) + }) + } } func testConvertSampleProto(t *testing.T, sampleProto sampleProto) { + t.Helper() // Make a Logrus logger: logger := logrus.New() @@ -61,167 +58,346 @@ func testConvertSampleProto(t *testing.T, sampleProto sampleProto) { // Use the logger to make a Converter: protoConverter := New(logger) - protoConverter.AllowNullValues = sampleProto.AllowNullValues - protoConverter.UseProtoAndJSONFieldnames = sampleProto.UseProtoAndJSONFieldNames + protoConverter.Flags = sampleProto.Flags // Open the sample proto file: sampleProtoFileName := fmt.Sprintf("%v/%v", sampleProtoDirectory, sampleProto.ProtoFileName) fileDescriptorSet := mustReadProtoFiles(t, sampleProtoDirectory, sampleProto.ProtoFileName) // Prepare a request: - codeGeneratorRequest := plugin.CodeGeneratorRequest{ + codeGeneratorRequest := pluginpb.CodeGeneratorRequest{ FileToGenerate: sampleProto.FilesToGenerate, ProtoFile: fileDescriptorSet.GetFile(), } - // Perform the conversion: - response, err := protoConverter.convert(&codeGeneratorRequest) - assert.NoError(t, err, "Unable to convert sample proto file (%v)", sampleProtoFileName) - assert.Equal(t, len(sampleProto.ExpectedJSONSchema), len(response.File), "Incorrect number of JSON-Schema files returned for sample proto file (%v)", sampleProtoFileName) - if len(sampleProto.ExpectedJSONSchema) != len(response.File) { - t.Fail() - } else { - for responseFileIndex, responseFile := range response.File { - assert.Equal(t, sampleProto.ExpectedJSONSchema[responseFileIndex], *responseFile.Content, "Incorrect JSON-Schema returned for sample proto file (%v)", sampleProtoFileName) - } - } - -} - -func configureSampleProtos() { - // ArrayOfMessages: - sampleProtos["ArrayOfMessages"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.ArrayOfMessages}, - FilesToGenerate: []string{"ArrayOfMessages.proto", "PayloadMessage.proto"}, - ProtoFileName: "ArrayOfMessages.proto", - } - - // ArrayOfObjects: - sampleProtos["ArrayOfObjects"] = sampleProto{ - AllowNullValues: true, - ExpectedJSONSchema: []string{testdata.ArrayOfObjects}, - FilesToGenerate: []string{"ArrayOfObjects.proto"}, - ProtoFileName: "ArrayOfObjects.proto", - } - - // ArrayOfPrimitives: - sampleProtos["ArrayOfPrimitives"] = sampleProto{ - AllowNullValues: true, - ExpectedJSONSchema: []string{testdata.ArrayOfPrimitives}, - FilesToGenerate: []string{"ArrayOfPrimitives.proto"}, - ProtoFileName: "ArrayOfPrimitives.proto", - } - - // ArrayOfPrimitives: - sampleProtos["ArrayOfPrimitivesDouble"] = sampleProto{ - AllowNullValues: true, - ExpectedJSONSchema: []string{testdata.ArrayOfPrimitivesDouble}, - FilesToGenerate: []string{"ArrayOfPrimitives.proto"}, - ProtoFileName: "ArrayOfPrimitives.proto", - UseProtoAndJSONFieldNames: true, - } - - // EnumCeption: - sampleProtos["EnumCeption"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.ImportedEnum, testdata.EnumCeption}, - FilesToGenerate: []string{"Enumception.proto", "PayloadMessage.proto", "ImportedEnum.proto"}, - ProtoFileName: "Enumception.proto", - } - - // ImportedEnum: - sampleProtos["ImportedEnum"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.ImportedEnum}, - FilesToGenerate: []string{"ImportedEnum.proto"}, - ProtoFileName: "ImportedEnum.proto", + // Test the TargetedMessages feature: + if len(sampleProto.TargetedMessages) > 0 { + arg := fmt.Sprintf("messages=[%s]", strings.Join(sampleProto.TargetedMessages, messageDelimiter)) + codeGeneratorRequest.Parameter = &arg } - // NestedMessage: - sampleProtos["NestedMessage"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.NestedMessage}, - FilesToGenerate: []string{"NestedMessage.proto", "PayloadMessage.proto"}, - ProtoFileName: "NestedMessage.proto", - } + // Perform the conversion: + response, err := protoConverter.convert(&codeGeneratorRequest) + require.NoError(t, err, "Unable to convert sample proto file (%v)", sampleProtoFileName) + require.Equal(t, len(sampleProto.ExpectedJSONSchema), len(response.File), "Incorrect number of JSON-Schema files returned for sample proto file (%v)", sampleProtoFileName) - // NestedObject: - sampleProtos["NestedObject"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.NestedObject}, - FilesToGenerate: []string{"NestedObject.proto"}, - ProtoFileName: "NestedObject.proto", - } + for responseFileIndex, responseFile := range response.File { + expected := strings.TrimSpace(sampleProto.ExpectedJSONSchema[responseFileIndex]) + got := responseFile.GetContent() - // PayloadMessage: - sampleProtos["PayloadMessage"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.PayloadMessage}, - FilesToGenerate: []string{"PayloadMessage.proto"}, - ProtoFileName: "PayloadMessage.proto", - } + if diff := cmp.Diff(expected, got); diff != "" { + t.Error(diff) + } - // SeveralEnums: - sampleProtos["SeveralEnums"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.FirstEnum, testdata.SecondEnum}, - FilesToGenerate: []string{"SeveralEnums.proto"}, - ProtoFileName: "SeveralEnums.proto", - } + // Validate the generated filenames: + if len(sampleProto.ExpectedFileNames) > 0 { + assert.Equal(t, sampleProto.ExpectedFileNames[responseFileIndex], responseFile.GetName()) + } - // SeveralMessages: - sampleProtos["SeveralMessages"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.FirstMessage, testdata.SecondMessage}, - FilesToGenerate: []string{"SeveralMessages.proto"}, - ProtoFileName: "SeveralMessages.proto", - } + // Validate any intended-to-fail data against the new schema: + if len(sampleProto.ObjectsToValidateFail) >= responseFileIndex+1 { + valid, err := validateSchema(got, sampleProto.ObjectsToValidateFail[responseFileIndex]) + assert.NoError(t, err) + assert.False(t, valid, "Expected canned data to fail validation)") + } - // ArrayOfEnums: - sampleProtos["ArrayOfEnums"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.ArrayOfEnums}, - FilesToGenerate: []string{"ArrayOfEnums.proto"}, - ProtoFileName: "ArrayOfEnums.proto", + // Validate any intended-to-pass data against the new schema: + if len(sampleProto.ObjectsToValidatePass) >= responseFileIndex+1 { + valid, err := validateSchema(got, sampleProto.ObjectsToValidatePass[responseFileIndex]) + assert.NoError(t, err, "Error validating canned data with generated schema") + assert.True(t, valid, "Expected canned data validate)") + } } - // Maps: - sampleProtos["Maps"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.Maps}, - FilesToGenerate: []string{"Maps.proto"}, - ProtoFileName: "Maps.proto", + // Return now if we have no files + if len(response.File) == 0 { + return } - // Comments: - sampleProtos["Comments"] = sampleProto{ - AllowNullValues: false, - ExpectedJSONSchema: []string{testdata.MessageWithComments}, - FilesToGenerate: []string{"MessageWithComments.proto"}, - ProtoFileName: "MessageWithComments.proto", + // Check for the correct prefix: + if protoConverter.Flags.PrefixSchemaFilesWithPackage { + assert.Contains(t, response.File[0].GetName(), "samples") + } else { + assert.NotContains(t, response.File[0].GetName(), "samples") } +} - sampleProtos["WellKnown"] = sampleProto{ - ExpectedJSONSchema: []string{testdata.WellKnown}, - FilesToGenerate: []string{"WellKnown.proto"}, - ProtoFileName: "WellKnown.proto", +func configureSampleProtos() map[string]sampleProto { + return map[string]sampleProto{ + "AllRequired": { + Flags: ConverterFlags{AllFieldsRequired: true}, + ExpectedJSONSchema: []string{testdata.PayloadMessage2}, + FilesToGenerate: []string{"PayloadMessage2.proto"}, + ProtoFileName: "PayloadMessage2.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessage2Fail}, + ObjectsToValidatePass: []string{testdata.PayloadMessage2Pass}, + }, + "ArrayOfEnums": { + ExpectedJSONSchema: []string{testdata.ArrayOfEnums}, + FilesToGenerate: []string{"ArrayOfEnums.proto"}, + ProtoFileName: "ArrayOfEnums.proto", + ObjectsToValidateFail: []string{testdata.ArrayOfEnumsFail}, + ObjectsToValidatePass: []string{testdata.ArrayOfEnumsPass}, + }, + "ArrayOfMessages": { + ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.ArrayOfMessages}, + FilesToGenerate: []string{"ArrayOfMessages.proto", "PayloadMessage.proto"}, + ProtoFileName: "ArrayOfMessages.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessageFail, testdata.ArrayOfMessagesFail}, + ObjectsToValidatePass: []string{testdata.PayloadMessagePass, testdata.ArrayOfMessagesPass}, + }, + "TypeNamesWithNoPackage": { + Flags: ConverterFlags{TypeNamesWithNoPackage: true}, + ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.TypeNamesWithNoPackage}, + FilesToGenerate: []string{"ArrayOfMessages.proto", "PayloadMessage.proto"}, + ProtoFileName: "ArrayOfMessages.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessageFail, testdata.TypeNamesWithNoPackageFail}, + ObjectsToValidatePass: []string{testdata.PayloadMessagePass, testdata.TypeNamesWithNoPackagePass}, + }, + "ArrayOfObjects": { + Flags: ConverterFlags{AllowNullValues: true}, + ExpectedJSONSchema: []string{testdata.ArrayOfObjects}, + FilesToGenerate: []string{"ArrayOfObjects.proto"}, + ProtoFileName: "ArrayOfObjects.proto", + ObjectsToValidateFail: []string{testdata.ArrayOfObjectsFail}, + ObjectsToValidatePass: []string{testdata.ArrayOfObjectsPass}, + }, + "ArrayOfPrimitives": { + Flags: ConverterFlags{AllowNullValues: true}, + ExpectedJSONSchema: []string{testdata.ArrayOfPrimitives}, + FilesToGenerate: []string{"ArrayOfPrimitives.proto"}, + ProtoFileName: "ArrayOfPrimitives.proto", + ObjectsToValidateFail: []string{testdata.ArrayOfPrimitivesFail}, + ObjectsToValidatePass: []string{testdata.ArrayOfPrimitivesPass}, + }, + "ArrayOfPrimitivesDouble": { + Flags: ConverterFlags{ + AllowNullValues: true, + UseProtoAndJSONFieldNames: true, + }, + ExpectedJSONSchema: []string{testdata.ArrayOfPrimitivesDouble}, + FilesToGenerate: []string{"ArrayOfPrimitives.proto"}, + ProtoFileName: "ArrayOfPrimitives.proto", + ObjectsToValidateFail: []string{testdata.ArrayOfPrimitivesDoubleFail}, + ObjectsToValidatePass: []string{testdata.ArrayOfPrimitivesDoublePass}, + }, + "BytesPayload": { + ExpectedJSONSchema: []string{testdata.BytesPayload}, + FilesToGenerate: []string{"BytesPayload.proto"}, + ProtoFileName: "BytesPayload.proto", + ObjectsToValidateFail: []string{testdata.BytesPayloadFail}, + }, + "Comments": { + ExpectedJSONSchema: []string{testdata.MessageWithComments}, + FilesToGenerate: []string{"MessageWithComments.proto"}, + ProtoFileName: "MessageWithComments.proto", + ObjectsToValidateFail: []string{testdata.MessageWithCommentsFail}, + }, + "CyclicalReference": { + ExpectedJSONSchema: []string{testdata.CyclicalReferenceMessageM, testdata.CyclicalReferenceMessageFoo, testdata.CyclicalReferenceMessageBar, testdata.CyclicalReferenceMessageBaz}, + FilesToGenerate: []string{"CyclicalReference.proto"}, + ProtoFileName: "CyclicalReference.proto", + }, + "EnumNestedReference": { + ExpectedJSONSchema: []string{testdata.EnumNestedReference}, + FilesToGenerate: []string{"EnumNestedReference.proto"}, + ProtoFileName: "EnumNestedReference.proto", + ObjectsToValidateFail: []string{testdata.EnumNestedReferenceFail}, + ObjectsToValidatePass: []string{testdata.EnumNestedReferencePass}, + }, + "EnumWithMessage": { + ExpectedJSONSchema: []string{testdata.EnumWithMessage}, + FilesToGenerate: []string{"EnumWithMessage.proto"}, + ProtoFileName: "EnumWithMessage.proto", + ObjectsToValidateFail: []string{testdata.EnumWithMessageFail}, + ObjectsToValidatePass: []string{testdata.EnumWithMessagePass}, + }, + "EnumCeption": { + ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.EnumCeption}, + FilesToGenerate: []string{"Enumception.proto", "PayloadMessage.proto"}, + ProtoFileName: "Enumception.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessageFail, testdata.EnumCeptionFail}, + ObjectsToValidatePass: []string{testdata.PayloadMessagePass, testdata.EnumCeptionPass}, + }, + "GoogleValue": { + Flags: ConverterFlags{DisallowAdditionalProperties: true}, + ExpectedJSONSchema: []string{testdata.GoogleValue}, + FilesToGenerate: []string{"GoogleValue.proto"}, + ProtoFileName: "GoogleValue.proto", + ObjectsToValidateFail: []string{testdata.GoogleValueFail}, + ObjectsToValidatePass: []string{testdata.GoogleValuePass}, + }, + "GoogleInt64Value": { + ExpectedJSONSchema: []string{testdata.GoogleInt64Value}, + FilesToGenerate: []string{"GoogleInt64Value.proto"}, + ProtoFileName: "GoogleInt64Value.proto", + ObjectsToValidateFail: []string{testdata.GoogleInt64ValueFail}, + ObjectsToValidatePass: []string{testdata.GoogleInt64ValuePass}, + }, + "GoogleInt64ValueAllowNull": { + Flags: ConverterFlags{AllowNullValues: true}, + ExpectedJSONSchema: []string{testdata.GoogleInt64ValueAllowNull}, + FilesToGenerate: []string{"GoogleInt64ValueAllowNull.proto"}, + ProtoFileName: "GoogleInt64ValueAllowNull.proto", + ObjectsToValidateFail: []string{testdata.GoogleInt64ValueAllowNullFail}, + ObjectsToValidatePass: []string{testdata.GoogleInt64ValueAllowNullPass}, + }, + "GoogleInt64ValueDisallowString": { + Flags: ConverterFlags{DisallowBigIntsAsStrings: true}, + ExpectedJSONSchema: []string{testdata.GoogleInt64ValueDisallowString}, + FilesToGenerate: []string{"GoogleInt64ValueDisallowString.proto"}, + ProtoFileName: "GoogleInt64ValueDisallowString.proto", + ObjectsToValidateFail: []string{testdata.GoogleInt64ValueDisallowStringFail}, + ObjectsToValidatePass: []string{testdata.GoogleInt64ValueDisallowStringPass}, + }, + "GoogleInt64ValueDisallowStringAllowNull": { + Flags: ConverterFlags{ + DisallowBigIntsAsStrings: true, + AllowNullValues: true, + }, + ExpectedJSONSchema: []string{testdata.GoogleInt64ValueDisallowStringAllowNull}, + FilesToGenerate: []string{"GoogleInt64ValueDisallowStringAllowNull.proto"}, + ProtoFileName: "GoogleInt64ValueDisallowStringAllowNull.proto", + ObjectsToValidateFail: []string{testdata.GoogleInt64ValueDisallowStringAllowNullFail}, + ObjectsToValidatePass: []string{testdata.GoogleInt64ValueDisallowStringAllowNullPass}, + }, + "JSONFields": { + Flags: ConverterFlags{UseJSONFieldnamesOnly: true}, + ExpectedJSONSchema: []string{testdata.JSONFields}, + FilesToGenerate: []string{"JSONFields.proto"}, + ProtoFileName: "JSONFields.proto", + ObjectsToValidateFail: []string{testdata.JSONFieldsFail}, + ObjectsToValidatePass: []string{testdata.JSONFieldsPass}, + }, + "Maps": { + ExpectedJSONSchema: []string{testdata.Maps}, + FilesToGenerate: []string{"Maps.proto"}, + ProtoFileName: "Maps.proto", + ObjectsToValidateFail: []string{testdata.MapsFail}, + ObjectsToValidatePass: []string{testdata.MapsPass}, + }, + "NestedMessage": { + ExpectedJSONSchema: []string{testdata.PayloadMessage, testdata.NestedMessage}, + FilesToGenerate: []string{"NestedMessage.proto", "PayloadMessage.proto"}, + ProtoFileName: "NestedMessage.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessageFail, testdata.NestedMessageFail}, + ObjectsToValidatePass: []string{testdata.PayloadMessagePass, testdata.NestedMessagePass}, + }, + "NestedObject": { + ExpectedJSONSchema: []string{testdata.NestedObject}, + FilesToGenerate: []string{"NestedObject.proto"}, + ProtoFileName: "NestedObject.proto", + ObjectsToValidateFail: []string{testdata.NestedObjectFail}, + ObjectsToValidatePass: []string{testdata.NestedObjectPass}, + }, + "NoPackage": { + ExpectedJSONSchema: []string{}, + FilesToGenerate: []string{}, + ProtoFileName: "NoPackage.proto", + }, + "OneOf": { + Flags: ConverterFlags{AllFieldsRequired: true, EnforceOneOf: true}, + ExpectedJSONSchema: []string{testdata.OneOf}, + FilesToGenerate: []string{"OneOf.proto"}, + ProtoFileName: "OneOf.proto", + ObjectsToValidateFail: []string{testdata.OneOfFail}, + ObjectsToValidatePass: []string{testdata.OneOfPass}, + }, + "PackagePrefix": { + Flags: ConverterFlags{PrefixSchemaFilesWithPackage: true}, + ExpectedJSONSchema: []string{testdata.Timestamp}, + FilesToGenerate: []string{"Timestamp.proto"}, + ProtoFileName: "Timestamp.proto", + ObjectsToValidateFail: []string{testdata.TimestampFail}, + ObjectsToValidatePass: []string{testdata.TimestampPass}, + }, + "PayloadMessage": { + ExpectedJSONSchema: []string{testdata.PayloadMessage}, + FilesToGenerate: []string{"PayloadMessage.proto"}, + ProtoFileName: "PayloadMessage.proto", + ObjectsToValidateFail: []string{testdata.PayloadMessageFail}, + ObjectsToValidatePass: []string{testdata.PayloadMessagePass}, + }, + "Proto2NestedMessage": { + ExpectedJSONSchema: []string{testdata.Proto2PayloadMessage, testdata.Proto2NestedMessage}, + FilesToGenerate: []string{"Proto2PayloadMessage.proto", "Proto2NestedMessage.proto"}, + ProtoFileName: "Proto2NestedMessage.proto", + ObjectsToValidateFail: []string{testdata.Proto2PayloadMessageFail, testdata.Proto2NestedMessageFail}, + ObjectsToValidatePass: []string{testdata.Proto2PayloadMessagePass, testdata.Proto2NestedMessagePass}, + }, + "Proto2NestedObject": { + Flags: ConverterFlags{AllFieldsRequired: true}, + ExpectedJSONSchema: []string{testdata.Proto2NestedObject}, + FilesToGenerate: []string{"Proto2NestedObject.proto"}, + ProtoFileName: "Proto2NestedObject.proto", + ObjectsToValidateFail: []string{testdata.Proto2NestedObjectFail}, + ObjectsToValidatePass: []string{testdata.Proto2NestedObjectPass}, + }, + "Proto2Required": { + ExpectedJSONSchema: []string{testdata.Proto2Required}, + FilesToGenerate: []string{"Proto2Required.proto"}, + ProtoFileName: "Proto2Required.proto", + ObjectsToValidateFail: []string{testdata.Proto2RequiredFail}, + ObjectsToValidatePass: []string{testdata.Proto2RequiredPass}, + }, + "SelfReference": { + ExpectedJSONSchema: []string{testdata.SelfReference}, + FilesToGenerate: []string{"SelfReference.proto"}, + ProtoFileName: "SelfReference.proto", + ObjectsToValidateFail: []string{testdata.SelfReferenceFail}, + ObjectsToValidatePass: []string{testdata.SelfReferencePass}, + }, + "SeveralEnums": { + ExpectedJSONSchema: []string{testdata.FirstEnum, testdata.SecondEnum}, + FilesToGenerate: []string{"SeveralEnums.proto"}, + ProtoFileName: "SeveralEnums.proto", + ObjectsToValidateFail: []string{testdata.FirstEnumFail, testdata.SecondEnumFail}, + ObjectsToValidatePass: []string{testdata.FirstEnumPass, testdata.SecondEnumPass}, + }, + "SeveralMessages": { + ExpectedJSONSchema: []string{testdata.FirstMessage, testdata.SecondMessage}, + FilesToGenerate: []string{"SeveralMessages.proto"}, + ProtoFileName: "SeveralMessages.proto", + ObjectsToValidateFail: []string{testdata.FirstMessageFail, testdata.SecondMessageFail}, + ObjectsToValidatePass: []string{testdata.FirstMessagePass, testdata.SecondMessagePass}, + }, + "TargetedMessages": { + TargetedMessages: []string{"MessageKind10", "MessageKind11", "MessageKind12"}, + ExpectedJSONSchema: []string{testdata.MessageKind10, testdata.MessageKind11, testdata.MessageKind12}, + FilesToGenerate: []string{"TwelveMessages.proto"}, + ProtoFileName: "TwelveMessages.proto", + }, + "Timestamp": { + ExpectedJSONSchema: []string{testdata.Timestamp}, + FilesToGenerate: []string{"Timestamp.proto"}, + ProtoFileName: "Timestamp.proto", + ObjectsToValidateFail: []string{testdata.TimestampFail}, + ObjectsToValidatePass: []string{testdata.TimestampPass}, + }, + "WellKnown": { + ExpectedJSONSchema: []string{testdata.WellKnown}, + FilesToGenerate: []string{"WellKnown.proto"}, + ProtoFileName: "WellKnown.proto", + ObjectsToValidateFail: []string{testdata.WellKnownFail}, + ObjectsToValidatePass: []string{testdata.WellKnownPass}, + }, } } // Load the specified .proto files into a FileDescriptorSet. Any errors in loading/parsing will // immediately fail the test. -func mustReadProtoFiles(t *testing.T, includePath string, filenames ...string) *descriptor.FileDescriptorSet { +func mustReadProtoFiles(t *testing.T, includePath string, filenames ...string) *descriptorpb.FileDescriptorSet { protocBinary, err := exec.LookPath("protoc") if err != nil { t.Fatalf("Can't find 'protoc' binary in $PATH: %s", err.Error()) } - // Use protoc to output descriptor info for the specified .proto files. + // Use protoc to output descriptorpb info for the specified .proto files. var args []string args = append(args, "--descriptor_set_out=/dev/stdout") args = append(args, "--include_source_info") args = append(args, "--include_imports") + args = append(args, "-I../../") args = append(args, "--proto_path="+includePath) args = append(args, filenames...) cmd := exec.Command(protocBinary, args...) @@ -231,13 +407,31 @@ func mustReadProtoFiles(t *testing.T, includePath string, filenames ...string) * cmd.Stderr = &stderrBuf err = cmd.Run() if err != nil { - t.Fatalf("failed to load descriptor set (%s): %s: %s", + t.Fatalf("failed to load descriptorpb set (%s): %s: %s", strings.Join(cmd.Args, " "), err.Error(), stderrBuf.String()) } - fds := &descriptor.FileDescriptorSet{} + fds := &descriptorpb.FileDescriptorSet{} err = proto.Unmarshal(stdoutBuf.Bytes(), fds) if err != nil { t.Fatalf("failed to parse protoc output as FileDescriptorSet: %s", err.Error()) } return fds } + +func validateSchema(jsonSchema, jsonData string) (bool, error) { + var valid = false + + // Load the JSON schema: + schemaLoader := gojsonschema.NewStringLoader(jsonSchema) + + // Load the JSON document we'll be validating: + documentLoader := gojsonschema.NewStringLoader(jsonData) + + // Validate: + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil || result == nil { + return valid, err + } + + return result.Valid(), nil +} diff --git a/internal/converter/proto_package.go b/internal/converter/proto_package.go index 097e386e..b692779e 100644 --- a/internal/converter/proto_package.go +++ b/internal/converter/proto_package.go @@ -3,7 +3,7 @@ package converter import ( "strings" - "github.com/golang/protobuf/protoc-gen-go/descriptor" + "google.golang.org/protobuf/types/descriptorpb" ) // ProtoPackage describes a package of Protobuf, which is an container of message types. @@ -11,12 +11,28 @@ type ProtoPackage struct { name string parent *ProtoPackage children map[string]*ProtoPackage - types map[string]*descriptor.DescriptorProto + types map[string]*descriptorpb.DescriptorProto + enums map[string]*descriptorpb.EnumDescriptorProto } -func (c *Converter) lookupType(pkg *ProtoPackage, name string) (*descriptor.DescriptorProto, string, bool) { +func newProtoPackage(parent *ProtoPackage, name string) *ProtoPackage { + pkgName := name + if parent != nil { + pkgName = parent.name + "." + name + } + + return &ProtoPackage{ + name: pkgName, + parent: parent, + children: make(map[string]*ProtoPackage), + types: make(map[string]*descriptorpb.DescriptorProto), + enums: make(map[string]*descriptorpb.EnumDescriptorProto), + } +} + +func (c *Converter) lookupType(pkg *ProtoPackage, name string) (*descriptorpb.DescriptorProto, string, bool) { if strings.HasPrefix(name, ".") { - return c.relativelyLookupType(globalPkg, name[1:len(name)]) + return c.relativelyLookupType(globalPkg, name[1:]) } for ; pkg != nil; pkg = pkg.parent { @@ -27,7 +43,20 @@ func (c *Converter) lookupType(pkg *ProtoPackage, name string) (*descriptor.Desc return nil, "", false } -func (c *Converter) relativelyLookupType(pkg *ProtoPackage, name string) (*descriptor.DescriptorProto, string, bool) { +func (c *Converter) lookupEnum(pkg *ProtoPackage, name string) (*descriptorpb.EnumDescriptorProto, string, bool) { + if strings.HasPrefix(name, ".") { + return c.relativelyLookupEnum(globalPkg, name[1:]) + } + + for ; pkg != nil; pkg = pkg.parent { + if desc, pkgName, ok := c.relativelyLookupEnum(pkg, name); ok { + return desc, pkgName, ok + } + } + return nil, "", false +} + +func (c *Converter) relativelyLookupType(pkg *ProtoPackage, name string) (*descriptorpb.DescriptorProto, string, bool) { components := strings.SplitN(name, ".", 2) switch len(components) { case 0: @@ -46,7 +75,7 @@ func (c *Converter) relativelyLookupType(pkg *ProtoPackage, name string) (*descr found, ok := c.relativelyLookupNestedType(msg, components[1]) return found, pkg.name, ok } - c.logger.WithField("component", components[0]).WithField("package_name", pkg.name).Info("No such package nor message in package") + c.logger.WithField("component", components[0]).WithField("package_name", pkg.name).Debug("No such package nor message in package") return nil, "", false default: c.logger.Error("Failed to lookup type") @@ -54,6 +83,77 @@ func (c *Converter) relativelyLookupType(pkg *ProtoPackage, name string) (*descr } } +func (c *Converter) relativelyLookupNestedType(desc *descriptorpb.DescriptorProto, name string) (*descriptorpb.DescriptorProto, bool) { + components := strings.Split(name, ".") +componentLoop: + for _, component := range components { + for _, nested := range desc.GetNestedType() { + if nested.GetName() == component { + desc = nested + continue componentLoop + } + } + c.logger.WithField("component", component).WithField("description", desc.GetName()).Info("no such nested message") + return nil, false + } + return desc, true +} + +func (c *Converter) relativelyLookupEnum(pkg *ProtoPackage, name string) (*descriptorpb.EnumDescriptorProto, string, bool) { + components := strings.SplitN(name, ".", 2) + switch len(components) { + case 0: + c.logger.Debug("empty enum name") + return nil, "", false + case 1: + found, ok := pkg.enums[components[0]] + return found, pkg.name, ok + case 2: + c.logger.Tracef("Looking for %s in %s at %s (%v)", components[1], components[0], pkg.name, pkg) + if child, ok := pkg.children[components[0]]; ok { + found, pkgName, ok := c.relativelyLookupEnum(child, components[1]) + return found, pkgName, ok + } + if msg, ok := pkg.types[components[0]]; ok { + found, ok := c.relativelyLookupNestedEnum(msg, components[1]) + return found, pkg.name, ok + } + c.logger.WithField("component", components[0]).WithField("package_name", pkg.name).Debug("No such package nor message in package") + return nil, "", false + default: + c.logger.Error("Failed to lookup type") + return nil, "", false + } +} + +func (c *Converter) relativelyLookupNestedEnum(desc *descriptorpb.DescriptorProto, name string) (*descriptorpb.EnumDescriptorProto, bool) { + components := strings.Split(name, ".") + + parent := desc + + if len(components) > 1 { + // The enum is nested inside a potentially nested message definition. + msgComponents := strings.Join(components[:len(components)-1], ".") + p, found := c.relativelyLookupNestedType(parent, msgComponents) + if !found { + return nil, false + } + parent = p + } + + // The enum is nested inside of a nested message. We need to dive down the + // tree to find the message the enum is nested in. Then we need to obtain the + // enum. + enumName := components[len(components)-1] + for _, enum := range parent.GetEnumType() { + if enum.GetName() == enumName { + return enum, true + } + } + + return nil, false +} + func (c *Converter) relativelyLookupPackage(pkg *ProtoPackage, name string) (*ProtoPackage, bool) { components := strings.Split(name, ".") for _, c := range components { diff --git a/internal/converter/sourcecodeinfo.go b/internal/converter/sourcecodeinfo.go index 566a25b1..9a033426 100644 --- a/internal/converter/sourcecodeinfo.go +++ b/internal/converter/sourcecodeinfo.go @@ -1,12 +1,17 @@ package converter import ( - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" + "strings" + + "github.com/fatih/camelcase" + "github.com/iancoleman/strcase" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" ) // Protobuf tag values for relevant message fields. Full list here: -// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto +// +// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptorpb.proto const ( tag_FileDescriptor_messageType int32 = 4 tag_FileDescriptor_enumType int32 = 5 @@ -18,30 +23,30 @@ const ( ) type sourceCodeInfo struct { - lookup map[proto.Message]*descriptor.SourceCodeInfo_Location + lookup map[proto.Message]*descriptorpb.SourceCodeInfo_Location } -func (s sourceCodeInfo) GetMessage(m *descriptor.DescriptorProto) *descriptor.SourceCodeInfo_Location { +func (s sourceCodeInfo) GetMessage(m *descriptorpb.DescriptorProto) *descriptorpb.SourceCodeInfo_Location { return s.lookup[m] } -func (s sourceCodeInfo) GetField(f *descriptor.FieldDescriptorProto) *descriptor.SourceCodeInfo_Location { +func (s sourceCodeInfo) GetField(f *descriptorpb.FieldDescriptorProto) *descriptorpb.SourceCodeInfo_Location { return s.lookup[f] } -func (s sourceCodeInfo) GetEnum(e *descriptor.EnumDescriptorProto) *descriptor.SourceCodeInfo_Location { +func (s sourceCodeInfo) GetEnum(e *descriptorpb.EnumDescriptorProto) *descriptorpb.SourceCodeInfo_Location { return s.lookup[e] } -func (s sourceCodeInfo) GetEnumValue(e *descriptor.EnumValueDescriptorProto) *descriptor.SourceCodeInfo_Location { +func (s sourceCodeInfo) GetEnumValue(e *descriptorpb.EnumValueDescriptorProto) *descriptorpb.SourceCodeInfo_Location { return s.lookup[e] } -func newSourceCodeInfo(fs []*descriptor.FileDescriptorProto) *sourceCodeInfo { +func newSourceCodeInfo(fs []*descriptorpb.FileDescriptorProto) *sourceCodeInfo { // For each source location in the provided files // - resolve the (annoyingly) encoded path to its message/field/service/enum/etc definition // - store the source info by its resolved definition - lookup := map[proto.Message]*descriptor.SourceCodeInfo_Location{} + lookup := map[proto.Message]*descriptorpb.SourceCodeInfo_Location{} for _, f := range fs { for _, loc := range f.GetSourceCodeInfo().GetLocation() { declaration := getDefinitionAtPath(f, loc.Path) @@ -56,12 +61,12 @@ func newSourceCodeInfo(fs []*descriptor.FileDescriptorProto) *sourceCodeInfo { // Resolve a protobuf "file-source path" to its associated definition (eg message/field/enum/etc). // Note that some paths don't point to definitions (some reference subcomponents like name, type, // field #, etc) and will therefore return nil. -func getDefinitionAtPath(file *descriptor.FileDescriptorProto, path []int32) proto.Message { +func getDefinitionAtPath(file *descriptorpb.FileDescriptorProto, path []int32) proto.Message { // The way protobuf encodes "file-source path" is a little opaque/tricky; // this doc describes how it works: - // https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto#L730 + // https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptorpb.proto#L730 - // Starting at the root of the file descriptor, traverse its object graph by following the + // Starting at the root of the file descriptorpb, traverse its object graph by following the // specified path (and updating our position/state at each step) until either: // - we reach the definition referenced by the path (and return it) // - we hit a dead end because the path references a grammar element more granular than a @@ -69,7 +74,7 @@ func getDefinitionAtPath(file *descriptor.FileDescriptorProto, path []int32) pro var pos proto.Message = file for step := 0; step < len(path); step++ { switch p := pos.(type) { - case *descriptor.FileDescriptorProto: + case *descriptorpb.FileDescriptorProto: switch path[step] { case tag_FileDescriptor_messageType: step++ @@ -81,7 +86,7 @@ func getDefinitionAtPath(file *descriptor.FileDescriptorProto, path []int32) pro return nil // ignore all other types } - case *descriptor.DescriptorProto: + case *descriptorpb.DescriptorProto: switch path[step] { case tag_Descriptor_field: step++ @@ -99,7 +104,7 @@ func getDefinitionAtPath(file *descriptor.FileDescriptorProto, path []int32) pro return nil // ignore all other types } - case *descriptor.EnumDescriptorProto: + case *descriptorpb.EnumDescriptorProto: switch path[step] { case tag_EnumDescriptor_value: step++ @@ -114,3 +119,48 @@ func getDefinitionAtPath(file *descriptor.FileDescriptorProto, path []int32) pro } return pos } + +// formatTitleAndDescription returns a title string and a description string, made from proto comments: +func (c *Converter) formatTitleAndDescription(name string, sl *descriptorpb.SourceCodeInfo_Location) (title, description string) { + var comments []string + + // Default title is camel-cased & split name: + if name != "" { + camelName := strcase.ToCamel(name) + splitName := camelcase.Split(camelName) + title = strings.Join(splitName, " ") + } + + // Leading detached comments first: + for _, str := range sl.GetLeadingDetachedComments() { + if s := strings.TrimSpace(str); s != "" { + comments = append(comments, s) + title = s + } + } + + // Leading comments next: + if s := strings.TrimSpace(sl.GetLeadingComments()); s != "" { + comments = append(comments, s) + } + + // Trailing comments last: + if s := strings.TrimSpace(sl.GetTrailingComments()); s != "" { + comments = append(comments, s) + } + + // The description is all the comments joined together: + description = strings.Join(comments, c.commentDelimiter) + + // Strip newlines: + if !c.Flags.KeepNewLinesInDescription { + description = strings.ReplaceAll(description, "\n", "") + } + + // Return an empty string if the ExcludeCommentToken is found: + if strings.Contains(strings.Join(comments, " "), c.excludeCommentToken) { + return title, "" + } + + return +} diff --git a/internal/converter/sourcecodeinfo_test.go b/internal/converter/sourcecodeinfo_test.go index 39e470ab..f3b07ba8 100644 --- a/internal/converter/sourcecodeinfo_test.go +++ b/internal/converter/sourcecodeinfo_test.go @@ -1,8 +1,10 @@ package converter import ( - "github.com/golang/protobuf/protoc-gen-go/descriptor" "testing" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" ) func TestSourceInfoLookup(t *testing.T) { @@ -10,6 +12,7 @@ func TestSourceInfoLookup(t *testing.T) { // Note that the hardcoded indexes must reflect the declaration order in // the .proto file. fds := mustReadProtoFiles(t, sampleProtoDirectory, "MessageWithComments.proto") + protoFile := fds.File[0] msgWithComments := protoFile.MessageType[0] msgWithComments_name1 := msgWithComments.Field[0] @@ -17,36 +20,35 @@ func TestSourceInfoLookup(t *testing.T) { // Create an instance of our thing and test that it returns the expected // source data for each of our above declarations. src := newSourceCodeInfo(fds.File) - assertCommentsMatch(t, src.GetMessage(msgWithComments), &descriptor.SourceCodeInfo_Location{ - LeadingComments: strPtr(" This is a message level comment and talks about what this message is and why you should care about it!\n"), + + assertCommentsMatch(t, src.GetMessage(msgWithComments), &descriptorpb.SourceCodeInfo_Location{ + LeadingComments: proto.String(" This is a message level comment and talks about what this message is and why you should care about it!\n"), + LeadingDetachedComments: []string{" This is a leading detached comment (which becomes the title)\n"}, }) - assertCommentsMatch(t, src.GetField(msgWithComments_name1), &descriptor.SourceCodeInfo_Location{ - LeadingComments: strPtr(" This field is supposed to represent blahblahblah\n"), + + assertCommentsMatch(t, src.GetField(msgWithComments_name1), &descriptorpb.SourceCodeInfo_Location{ + LeadingComments: proto.String(" This field is supposed to represent blahblahblah\n"), }) } -func assertCommentsMatch(t *testing.T, actual, expected *descriptor.SourceCodeInfo_Location) { +func assertCommentsMatch(t *testing.T, actual, expected *descriptorpb.SourceCodeInfo_Location) { + t.Helper() + if len(actual.LeadingDetachedComments) != len(expected.LeadingDetachedComments) { - t.Fatalf("Wrong value for LeadingDetachedComments.\n got: %v\n want: %v", - actual.LeadingDetachedComments, expected.LeadingDetachedComments) + t.Fatalf("Wrong length for LeadingDetachedComments.\n got: %v\nwant: %v", actual.LeadingDetachedComments, expected.LeadingDetachedComments) } - for i := 0; i < len(actual.LeadingDetachedComments); i++ { + + for i := range actual.LeadingDetachedComments { if actual.LeadingDetachedComments[i] != expected.LeadingDetachedComments[i] { - t.Fatalf("Wrong value for LeadingDetachedComments.\n got: %v\n want: %v", - actual.LeadingDetachedComments, expected.LeadingDetachedComments) + t.Fatalf("Wrong value for LeadingDetachedComments.\n got: %v\nwant: %v", actual.LeadingDetachedComments, expected.LeadingDetachedComments) } } + if actual.GetTrailingComments() != expected.GetTrailingComments() { - t.Fatalf("Wrong value for TrailingComments.\n got: %q\n want: %q", - actual.GetTrailingComments(), expected.GetTrailingComments()) + t.Fatalf("Wrong value for TrailingComments.\n got: %q\nwant: %q", actual.GetTrailingComments(), expected.GetTrailingComments()) } + if actual.GetLeadingComments() != expected.GetLeadingComments() { - t.Fatalf("Wrong value for LeadingComments.\n got: %q\n want: %q", - actual.GetLeadingComments(), expected.GetLeadingComments()) + t.Fatalf("Wrong value for LeadingComments.\n got: %q\nwant: %q", actual.GetLeadingComments(), expected.GetLeadingComments()) } } - -// Go doesn't have syntax for addressing a string literal, so this is the next best thing. -func strPtr(s string) *string { - return &s -} diff --git a/internal/converter/testdata/array_of_enums.go b/internal/converter/testdata/array_of_enums.go index d7ab881c..72fc0117 100644 --- a/internal/converter/testdata/array_of_enums.go +++ b/internal/converter/testdata/array_of_enums.go @@ -2,26 +2,47 @@ package testdata const ArrayOfEnums = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "description": { - "type": "string" - }, - "stuff": { - "items": { - "enum": [ - "FOO", - 0, - "BAR", - 1, - "FIZZ", - 2, - "BUZZ", - 3 - ] + "$ref": "#/definitions/ArrayOfEnums", + "definitions": { + "ArrayOfEnums": { + "properties": { + "description": { + "type": "string" + }, + "stuff": { + "items": { + "enum": [ + "FOO", + 0, + "BAR", + 1, + "FIZZ", + 2, + "BUZZ", + 3 + ] + }, + "type": "array", + "title": "Inline" + } }, - "type": "array" + "additionalProperties": true, + "type": "object", + "title": "Array Of Enums" } - }, - "additionalProperties": true, - "type": "object" + } +}` + +const ArrayOfEnumsFail = `{ + "description": "something", + "stuff": [ + "FOOZ" + ] +}` + +const ArrayOfEnumsPass = `{ + "description": "something", + "stuff": [ + 3 + ] }` diff --git a/internal/converter/testdata/array_of_messages.go b/internal/converter/testdata/array_of_messages.go index 86d002dd..25ad86b1 100644 --- a/internal/converter/testdata/array_of_messages.go +++ b/internal/converter/testdata/array_of_messages.go @@ -2,60 +2,84 @@ package testdata const ArrayOfMessages = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "description": { - "type": "string" - }, - "payload": { - "items": { - "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name": { - "type": "string" - }, - "timestamp": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "complete": { - "type": "boolean" + "$ref": "#/definitions/ArrayOfMessages", + "definitions": { + "ArrayOfMessages": { + "properties": { + "description": { + "type": "string" + }, + "payload": { + "items": { + "$ref": "#/definitions/samples.PayloadMessage" }, - "topology": { - "enum": [ - "FLAT", - 0, - "NESTED_OBJECT", - 1, - "NESTED_MESSAGE", - 2, - "ARRAY_OF_TYPE", - 3, - "ARRAY_OF_OBJECT", - 4, - "ARRAY_OF_MESSAGE", - 5 - ], - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - } + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Array Of Messages" + }, + "samples.PayloadMessage": { + "properties": { + "name": { + "type": "string" }, - "additionalProperties": true, - "type": "object" + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } }, - "type": "array" + "additionalProperties": true, + "type": "object", + "title": "Payload Message" } - }, - "additionalProperties": true, - "type": "object" + } +}` + +const ArrayOfMessagesFail = `{ + "description": "something", + "payload": [ + {"topology": "cruft"} + ] +}` + +const ArrayOfMessagesPass = `{ + "description": "something", + "payload": [ + {"topology": "ARRAY_OF_MESSAGE"} + ] }` diff --git a/internal/converter/testdata/array_of_objects.go b/internal/converter/testdata/array_of_objects.go index 4f1560ea..3e0ed538 100644 --- a/internal/converter/testdata/array_of_objects.go +++ b/internal/converter/testdata/array_of_objects.go @@ -2,126 +2,154 @@ package testdata const ArrayOfObjects = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "description": { + "$ref": "#/definitions/ArrayOfObjects", + "definitions": { + "ArrayOfObjects": { + "properties": { + "description": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "payload": { + "oneOf": [ + { + "type": "null" + }, + { + "items": { + "$ref": "#/definitions/samples.ArrayOfObjects.RepeatedPayload" + }, + "type": "array" + } + ] + } + }, + "additionalProperties": true, "oneOf": [ { "type": "null" }, { - "type": "string" + "type": "object" } - ] + ], + "title": "Array Of Objects" }, - "payload": { - "items": { - "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string" - } - ] - }, - "timestamp": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string" - } - ] - }, - "id": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "integer" - } - ] - }, - "rating": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "number" - } - ] - }, - "complete": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - } - ] - }, - "topology": { - "enum": [ - "FLAT", - 0, - "NESTED_OBJECT", - 1, - "NESTED_MESSAGE", - 2, - "ARRAY_OF_TYPE", - 3, - "ARRAY_OF_OBJECT", - 4, - "ARRAY_OF_MESSAGE", - 5 - ], - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - }, - { - "type": "null" - } - ] - } + "samples.ArrayOfObjects.RepeatedPayload": { + "properties": { + "name": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "timestamp": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] }, - "additionalProperties": true, - "oneOf": [ - { - "type": "null" - }, - { - "type": "object" - } - ] + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "rating": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "number" + } + ] + }, + "complete": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + } + ] + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Topology" + } }, + "additionalProperties": true, "oneOf": [ { "type": "null" }, { - "type": "array" + "type": "object" } - ] + ], + "title": "Repeated Payload" } - }, - "additionalProperties": true, - "oneOf": [ + } +}` + +const ArrayOfObjectsFail = `{ + "description": "something", + "payload": [ { - "type": "null" - }, + "topology": "cruft" + } + ] +}` + +const ArrayOfObjectsPass = `{ + "description": "something", + "payload": [ { - "type": "object" + "topology": "ARRAY_OF_OBJECT" } ] }` diff --git a/internal/converter/testdata/array_of_primitives.go b/internal/converter/testdata/array_of_primitives.go index 9b8f3795..f91263ad 100644 --- a/internal/converter/testdata/array_of_primitives.go +++ b/internal/converter/testdata/array_of_primitives.go @@ -2,215 +2,220 @@ package testdata const ArrayOfPrimitives = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "description": { - "oneOf": [ - { - "type": "null" + "$ref": "#/definitions/ArrayOfPrimitives", + "definitions": { + "ArrayOfPrimitives": { + "properties": { + "description": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] }, - { - "type": "string" - } - ] - }, - "luckyNumbers": { - "items": { - "oneOf": [ - { - "type": "null" + "luckyNumbers": { + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] }, - { - "type": "integer" - } - ] - }, - "oneOf": [ - { - "type": "null" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] }, - { - "type": "array" - } - ] - }, - "luckyBigNumbers": { - "items": { - "oneOf": [ - { - "type": "integer" + "luckyBigNumbers": { + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - { - "type": "string" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] + }, + "keyWords": { + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] }, - { - "type": "null" - } - ] - }, - "oneOf": [ - { - "type": "null" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] }, - { - "type": "array" + "big_number": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } - ] - }, - "keyWords": { - "items": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string" - } - ] }, + "additionalProperties": true, "oneOf": [ { "type": "null" }, { - "type": "array" - } - ] - }, - "big_number": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "string" - }, - { - "type": "null" + "type": "object" } - ] + ], + "title": "Array Of Primitives" } - }, - "additionalProperties": true, - "oneOf": [ - { - "type": "null" - }, - { - "type": "object" - } - ] + } }` +const ArrayOfPrimitivesFail = `{"luckyNumbers": ["false"]}` + +const ArrayOfPrimitivesPass = `{"luckyNumbers": [1,2,3]}` + const ArrayOfPrimitivesDouble = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "description": { - "oneOf": [ - { - "type": "null" + "$ref": "#/definitions/ArrayOfPrimitives", + "definitions": { + "ArrayOfPrimitives": { + "properties": { + "description": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] }, - { - "type": "string" - } - ] - }, - "luckyNumbers": { - "items": { - "oneOf": [ - { - "type": "null" + "luckyNumbers": { + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] }, - { - "type": "integer" - } - ] - }, - "oneOf": [ - { - "type": "null" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] }, - { - "type": "array" - } - ] - }, - "luckyBigNumbers": { - "items": { - "oneOf": [ - { - "type": "integer" - }, - { - "type": "string" + "luckyBigNumbers": { + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - { - "type": "null" - } - ] - }, - "oneOf": [ - { - "type": "null" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] }, - { - "type": "array" - } - ] - }, - "keyWords": { - "items": { - "oneOf": [ - { - "type": "null" + "keyWords": { + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] }, - { - "type": "string" - } - ] - }, - "oneOf": [ - { - "type": "null" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] }, - { - "type": "array" - } - ] - }, - "big_number": { - "oneOf": [ - { - "type": "integer" + "big_number": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - { - "type": "string" - }, - { - "type": "null" + "bigNumber": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } - ] - }, - "bigNumber": { + }, + "additionalProperties": true, "oneOf": [ { - "type": "integer" - }, - { - "type": "string" + "type": "null" }, { - "type": "null" + "type": "object" } - ] - } - }, - "additionalProperties": true, - "oneOf": [ - { - "type": "null" - }, - { - "type": "object" + ], + "title": "Array Of Primitives" } - ] + } }` + +const ArrayOfPrimitivesDoubleFail = `{"bigNumber": false}` + +const ArrayOfPrimitivesDoublePass = `{"bigNumber": "2"}` diff --git a/internal/converter/testdata/bytes_payload.go b/internal/converter/testdata/bytes_payload.go new file mode 100644 index 00000000..f11e5aa0 --- /dev/null +++ b/internal/converter/testdata/bytes_payload.go @@ -0,0 +1,25 @@ +package testdata + +const BytesPayload = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/BytesPayload", + "definitions": { + "BytesPayload": { + "properties": { + "description": { + "type": "string" + }, + "payload": { + "type": "string", + "format": "binary", + "binaryEncoding": "base64" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bytes Payload" + } + } +}` + +const BytesPayloadFail = `{"payload": 12345}` diff --git a/internal/converter/testdata/cyclical_reference.go b/internal/converter/testdata/cyclical_reference.go new file mode 100644 index 00000000..b1319512 --- /dev/null +++ b/internal/converter/testdata/cyclical_reference.go @@ -0,0 +1,216 @@ +package testdata + +const CyclicalReferenceMessageM = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/M", + "definitions": { + "M": { + "properties": { + "foo": { + "$ref": "#/definitions/samples.Foo", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "M" + }, + "samples.Bar": { + "properties": { + "id": { + "type": "integer" + }, + "baz": { + "$ref": "#/definitions/samples.Baz", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bar" + }, + "samples.Baz": { + "properties": { + "enabled": { + "type": "boolean" + }, + "foo": { + "$ref": "#/definitions/samples.Foo", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Baz" + }, + "samples.Foo": { + "properties": { + "name": { + "type": "string" + }, + "bar": { + "items": { + "$ref": "#/definitions/samples.Bar" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Foo" + } + } +}` + +const CyclicalReferenceMessageFoo = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Foo", + "definitions": { + "Foo": { + "properties": { + "name": { + "type": "string" + }, + "bar": { + "items": { + "$ref": "#/definitions/samples.Bar" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Foo" + }, + "samples.Bar": { + "properties": { + "id": { + "type": "integer" + }, + "baz": { + "$ref": "#/definitions/samples.Baz", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bar" + }, + "samples.Baz": { + "properties": { + "enabled": { + "type": "boolean" + }, + "foo": { + "$ref": "#/definitions/Foo", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Baz" + } + } +}` + +const CyclicalReferenceMessageBar = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Bar", + "definitions": { + "Bar": { + "properties": { + "id": { + "type": "integer" + }, + "baz": { + "$ref": "#/definitions/samples.Baz", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bar" + }, + "samples.Baz": { + "properties": { + "enabled": { + "type": "boolean" + }, + "foo": { + "$ref": "#/definitions/samples.Foo", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Baz" + }, + "samples.Foo": { + "properties": { + "name": { + "type": "string" + }, + "bar": { + "items": { + "$ref": "#/definitions/Bar" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Foo" + } + } +}` + +const CyclicalReferenceMessageBaz = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Baz", + "definitions": { + "Baz": { + "properties": { + "enabled": { + "type": "boolean" + }, + "foo": { + "$ref": "#/definitions/samples.Foo", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Baz" + }, + "samples.Bar": { + "properties": { + "id": { + "type": "integer" + }, + "baz": { + "$ref": "#/definitions/Baz", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bar" + }, + "samples.Foo": { + "properties": { + "name": { + "type": "string" + }, + "bar": { + "items": { + "$ref": "#/definitions/samples.Bar" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Foo" + } + } +}` diff --git a/internal/converter/testdata/enum_ception.go b/internal/converter/testdata/enum_ception.go index 017afe73..8a62a6e0 100644 --- a/internal/converter/testdata/enum_ception.go +++ b/internal/converter/testdata/enum_ception.go @@ -2,39 +2,59 @@ package testdata const EnumCeption = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name": { - "type": "string" - }, - "timestamp": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "complete": { - "type": "boolean" - }, - "failureMode": { - "enum": [ - "RECURSION_ERROR", - 0, - "SYNTAX_ERROR", - 1 - ], - "oneOf": [ - { + "$ref": "#/definitions/Enumception", + "definitions": { + "Enumception": { + "properties": { + "name": { "type": "string" }, - { + "timestamp": { + "type": "string" + }, + "id": { "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "failureMode": { + "enum": [ + "RECURSION_ERROR", + 0, + "SYNTAX_ERROR", + 1 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Failure Modes", + "description": "FailureModes enum" + }, + "payload": { + "$ref": "#/definitions/samples.PayloadMessage", + "additionalProperties": true + }, + "payloads": { + "items": { + "$ref": "#/definitions/samples.PayloadMessage" + }, + "type": "array" } - ] + }, + "additionalProperties": true, + "type": "object", + "title": "Enumception" }, - "payload": { + "samples.PayloadMessage": { "properties": { "name": { "type": "string" @@ -73,72 +93,17 @@ const EnumCeption = `{ { "type": "integer" } - ] + ], + "title": "Topology" } }, "additionalProperties": true, - "type": "object" - }, - "payloads": { - "items": { - "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name": { - "type": "string" - }, - "timestamp": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "complete": { - "type": "boolean" - }, - "topology": { - "enum": [ - "FLAT", - 0, - "NESTED_OBJECT", - 1, - "NESTED_MESSAGE", - 2, - "ARRAY_OF_TYPE", - 3, - "ARRAY_OF_OBJECT", - 4, - "ARRAY_OF_MESSAGE", - 5 - ], - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - } - }, - "additionalProperties": true, - "type": "object" - }, - "type": "array" - }, - "importedEnum": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] + "type": "object", + "title": "Payload Message" } - }, - "additionalProperties": true, - "type": "object" + } }` + +const EnumCeptionFail = `{"payloads": [ {"topology": "MAP"} ]}` + +const EnumCeptionPass = `{"payloads": [ {"topology": "ARRAY_OF_MESSAGE"} ]}` diff --git a/internal/converter/testdata/enum_nested_reference.go b/internal/converter/testdata/enum_nested_reference.go new file mode 100644 index 00000000..fde985e0 --- /dev/null +++ b/internal/converter/testdata/enum_nested_reference.go @@ -0,0 +1,44 @@ +package testdata + +const EnumNestedReference = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Msg", + "definitions": { + "Msg": { + "properties": { + "nestedEnumField": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Msg" + } + } +}` + +const EnumNestedReferenceFail = `{"nestedEnumField": 8}` + +const EnumNestedReferencePass = `{"nestedEnumField": "FLAT"}` diff --git a/internal/converter/testdata/enum_with_message.go b/internal/converter/testdata/enum_with_message.go new file mode 100644 index 00000000..183a74d9 --- /dev/null +++ b/internal/converter/testdata/enum_with_message.go @@ -0,0 +1,38 @@ +package testdata + +const EnumWithMessage = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/WithFooBarBaz", + "definitions": { + "WithFooBarBaz": { + "properties": { + "enumField": { + "enum": [ + "Foo", + 0, + "Bar", + 1, + "Baz", + 2 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Foo Bar Baz" + } + }, + "additionalProperties": true, + "type": "object", + "title": "With Foo Bar Baz" + } + } +}` + +const EnumWithMessageFail = `{"enumField": 4}` + +const EnumWithMessagePass = `{"enumField": 2}` diff --git a/internal/converter/testdata/file_options.go b/internal/converter/testdata/file_options.go new file mode 100644 index 00000000..47de4448 --- /dev/null +++ b/internal/converter/testdata/file_options.go @@ -0,0 +1,27 @@ +package testdata + +const FileOptions = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FileOptions", + "definitions": { + "FileOptions": { + "properties": { + "ignore": { + "type": "boolean", + "description": "Files tagged with this will not be processed" + }, + "extension": { + "type": "string", + "description": "Override the default file extension for schemas generated from this file" + } + }, + "additionalProperties": true, + "type": "object", + "description": "Custom FileOptions" + } + } +}` + +const FileOptionsFail = `{"ignore": 12345}` + +const FileOptionsPass = `{"ignore": true}` diff --git a/internal/converter/testdata/first_enum.go b/internal/converter/testdata/first_enum.go index 3c046a83..8022de11 100644 --- a/internal/converter/testdata/first_enum.go +++ b/internal/converter/testdata/first_enum.go @@ -19,5 +19,10 @@ const FirstEnum = `{ { "type": "integer" } - ] + ], + "title": "First Enum" }` + +const FirstEnumFail = `5` + +const FirstEnumPass = `3` diff --git a/internal/converter/testdata/first_message.go b/internal/converter/testdata/first_message.go index fac59444..4d276586 100644 --- a/internal/converter/testdata/first_message.go +++ b/internal/converter/testdata/first_message.go @@ -2,23 +2,33 @@ package testdata const FirstMessage = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name1": { - "type": "string" - }, - "timestamp1": { - "type": "string" - }, - "id1": { - "type": "integer" - }, - "rating1": { - "type": "number" - }, - "complete1": { - "type": "boolean" + "$ref": "#/definitions/FirstMessage", + "definitions": { + "FirstMessage": { + "properties": { + "name1": { + "type": "string" + }, + "timestamp1": { + "type": "string" + }, + "id1": { + "type": "integer" + }, + "rating1": { + "type": "number" + }, + "complete1": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "First Message" } - }, - "additionalProperties": true, - "type": "object" + } }` + +const FirstMessageFail = `{"complete1": "hello"}` + +const FirstMessagePass = `{"complete1": true}` diff --git a/internal/converter/testdata/google_int64_value.go b/internal/converter/testdata/google_int64_value.go new file mode 100644 index 00000000..5c566ec5 --- /dev/null +++ b/internal/converter/testdata/google_int64_value.go @@ -0,0 +1,23 @@ +package testdata + +const GoogleInt64Value = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/GoogleInt64Value", + "definitions": { + "GoogleInt64Value": { + "properties": { + "big_number": { + "additionalProperties": true, + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Google Int 64 Value" + } + } +}` + +const GoogleInt64ValueFail = `{"big_number": 12345}` + +const GoogleInt64ValuePass = `{"big_number": "12345"}` diff --git a/internal/converter/testdata/google_int64_value_allow_null.go b/internal/converter/testdata/google_int64_value_allow_null.go new file mode 100644 index 00000000..f14ad639 --- /dev/null +++ b/internal/converter/testdata/google_int64_value_allow_null.go @@ -0,0 +1,38 @@ +package testdata + +const GoogleInt64ValueAllowNull = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/GoogleInt64ValueAllowNull", + "definitions": { + "GoogleInt64ValueAllowNull": { + "properties": { + "big_number": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "title": "Int 64 Value", + "description": "Wrapper message for ` + "`int64`" + `. The JSON representation for ` + "`Int64Value`" + ` is JSON string." + } + }, + "additionalProperties": true, + "oneOf": [ + { + "type": "null" + }, + { + "type": "object" + } + ], + "title": "Google Int 64 Value Allow Null" + } + } +}` + +const GoogleInt64ValueAllowNullFail = `{"big_number": 12345}` + +const GoogleInt64ValueAllowNullPass = `{"big_number": null}` diff --git a/internal/converter/testdata/google_int64_value_disallow_string.go b/internal/converter/testdata/google_int64_value_disallow_string.go new file mode 100644 index 00000000..fd6549fb --- /dev/null +++ b/internal/converter/testdata/google_int64_value_disallow_string.go @@ -0,0 +1,23 @@ +package testdata + +const GoogleInt64ValueDisallowString = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/GoogleInt64ValueDisallowString", + "definitions": { + "GoogleInt64ValueDisallowString": { + "properties": { + "big_number": { + "additionalProperties": true, + "type": "integer" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Google Int 64 Value Disallow String" + } + } +}` + +const GoogleInt64ValueDisallowStringFail = `{"big_number": "12345"}` + +const GoogleInt64ValueDisallowStringPass = `{"big_number": 12345}` diff --git a/internal/converter/testdata/google_int64_value_disallow_string_allow_null.go b/internal/converter/testdata/google_int64_value_disallow_string_allow_null.go new file mode 100644 index 00000000..e73ee97b --- /dev/null +++ b/internal/converter/testdata/google_int64_value_disallow_string_allow_null.go @@ -0,0 +1,38 @@ +package testdata + +const GoogleInt64ValueDisallowStringAllowNull = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/GoogleInt64ValueDisallowStringAllowNull", + "definitions": { + "GoogleInt64ValueDisallowStringAllowNull": { + "properties": { + "big_number": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ], + "title": "Int 64 Value", + "description": "Wrapper message for ` + "`int64`" + `. The JSON representation for ` + "`Int64Value`" + ` is JSON string." + } + }, + "additionalProperties": true, + "oneOf": [ + { + "type": "null" + }, + { + "type": "object" + } + ], + "title": "Google Int 64 Value Disallow String Allow Null" + } + } +}` + +const GoogleInt64ValueDisallowStringAllowNullFail = `{"big_number": "12345"}` + +const GoogleInt64ValueDisallowStringAllowNullPass = `{"big_number": null}` diff --git a/internal/converter/testdata/google_value.go b/internal/converter/testdata/google_value.go new file mode 100644 index 00000000..52ae1a05 --- /dev/null +++ b/internal/converter/testdata/google_value.go @@ -0,0 +1,49 @@ +package testdata + +const GoogleValue = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/GoogleValue", + "definitions": { + "GoogleValue": { + "properties": { + "arg": { + "oneOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Value", + "description": "` + "`Value`" + ` represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values. A producer of value is expected to set one of these variants. Absence of any variant indicates an error. The JSON representation for ` + "`Value`" + ` is JSON value." + }, + "some_list": { + "additionalProperties": false, + "type": "array" + }, + "some_struct": { + "additionalProperties": true, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object", + "title": "Google Value" + } + } +} +` + +const GoogleValueFail = `{"arg": null, "some_list": 4}` + +const GoogleValuePass = `{"arg": 12345, "some_list": [1,2,3,4]}` diff --git a/internal/converter/testdata/imported_enum.go b/internal/converter/testdata/imported_enum.go index fbc17be2..e35aa6be 100644 --- a/internal/converter/testdata/imported_enum.go +++ b/internal/converter/testdata/imported_enum.go @@ -1,7 +1,7 @@ package testdata const ImportedEnum = `{ - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-06/schema#", "enum": [ "VALUE_0", 0, @@ -14,10 +14,42 @@ const ImportedEnum = `{ ], "oneOf": [ { - "type": "string" + "description": "Zero", + "const": "VALUE_0" }, { - "type": "integer" + "description": "Zero", + "const": 0 + }, + { + "description": "One", + "const": "VALUE_1" + }, + { + "description": "One", + "const": 1 + }, + { + "description": "Two", + "const": "VALUE_2" + }, + { + "description": "Two", + "const": 2 + }, + { + "description": "Three", + "const": "VALUE_3" + }, + { + "description": "Three", + "const": 3 } - ] + ], + "title": "Imported Enum", + "description": "This is an enum" }` + +const ImportedEnumFail = `"VALUE_5"` + +const ImportedEnumPass = `"VALUE_3"` diff --git a/internal/converter/testdata/json_fields.go b/internal/converter/testdata/json_fields.go new file mode 100644 index 00000000..e9e376fd --- /dev/null +++ b/internal/converter/testdata/json_fields.go @@ -0,0 +1,40 @@ +package testdata + +const JSONFields = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/JSONFields", + "definitions": { + "JSONFields": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "identifier": { + "type": "integer" + }, + "someThing": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "snakeNumb": { + "type": "string" + }, + "otherNumb": { + "type": "integer" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Jsonfields" + } + } +}` + +const JSONFieldsFail = `{"someThing": "onetwothree", "other_numb": 123}` + +const JSONFieldsPass = `{"someThing": 12345, "otherNumb": 123}` diff --git a/internal/converter/testdata/maps.go b/internal/converter/testdata/maps.go index d2d10ea7..f2880ff7 100644 --- a/internal/converter/testdata/maps.go +++ b/internal/converter/testdata/maps.go @@ -2,68 +2,96 @@ package testdata const Maps = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "map_of_strings": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "map_of_ints": { - "additionalProperties": { - "type": "integer" - }, - "type": "object" - }, - "map_of_messages": { - "additionalProperties": { - "properties": { - "name": { - "type": "string" - }, - "timestamp": { + "$ref": "#/definitions/Maps", + "definitions": { + "Maps": { + "properties": { + "map_of_strings": { + "additionalProperties": { "type": "string" }, - "id": { + "type": "object" + }, + "map_of_ints": { + "additionalProperties": { "type": "integer" }, - "rating": { - "type": "number" - }, - "complete": { - "type": "boolean" + "type": "object" + }, + "map_of_messages": { + "additionalProperties": { + "$ref": "#/definitions/samples.PayloadMessage", + "additionalProperties": true }, - "topology": { - "enum": [ - "FLAT", - 0, - "NESTED_OBJECT", - 1, - "NESTED_MESSAGE", - 2, - "ARRAY_OF_TYPE", - 3, - "ARRAY_OF_OBJECT", - 4, - "ARRAY_OF_MESSAGE", - 5 - ], - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - } + "type": "object" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Maps" + }, + "samples.PayloadMessage": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" }, - "additionalProperties": true, - "type": "object" + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } }, - "type": "object" + "additionalProperties": true, + "type": "object", + "title": "Payload Message" } - }, - "additionalProperties": true, - "type": "object" + } +}` + +const MapsFail = `{ + "map_of_strings": { + "one": 1, + "two": 2, + "three": 3 + } +}` + +const MapsPass = `{ + "map_of_strings": { + "one": "1", + "two": "2", + "three": "3" + } }` diff --git a/internal/converter/testdata/message_kind_10.go b/internal/converter/testdata/message_kind_10.go new file mode 100644 index 00000000..4f6d1ea3 --- /dev/null +++ b/internal/converter/testdata/message_kind_10.go @@ -0,0 +1,30 @@ +package testdata + +const MessageKind10 = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/MessageKind10", + "definitions": { + "MessageKind10": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 10" + } + } +}` diff --git a/internal/converter/testdata/message_kind_11.go b/internal/converter/testdata/message_kind_11.go new file mode 100644 index 00000000..76aa1a57 --- /dev/null +++ b/internal/converter/testdata/message_kind_11.go @@ -0,0 +1,136 @@ +package testdata + +const MessageKind11 = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/MessageKind11", + "definitions": { + "MessageKind11": { + "properties": { + "name": { + "type": "string" + }, + "ones": { + "items": { + "$ref": "#/definitions/samples.MessageKind1" + }, + "type": "array" + }, + "kind2": { + "$ref": "#/definitions/samples.MessageKind2", + "additionalProperties": true + }, + "kind3": { + "$ref": "#/definitions/samples.MessageKind3", + "additionalProperties": true + }, + "kind4": { + "$ref": "#/definitions/samples.MessageKind4", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 11" + }, + "samples.MessageKind1": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 1" + }, + "samples.MessageKind2": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "isa": { + "type": "boolean" + }, + "hasa": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 2" + }, + "samples.MessageKind3": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "someProp": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 3" + }, + "samples.MessageKind4": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "special": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 4" + } + } +}` diff --git a/internal/converter/testdata/message_kind_12.go b/internal/converter/testdata/message_kind_12.go new file mode 100644 index 00000000..bea7372f --- /dev/null +++ b/internal/converter/testdata/message_kind_12.go @@ -0,0 +1,237 @@ +package testdata + +const MessageKind12 = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/MessageKind12", + "definitions": { + "MessageKind12": { + "properties": { + "name": { + "type": "string" + }, + "f": { + "$ref": "#/definitions/samples.MessageKind11", + "additionalProperties": true + }, + "kind5": { + "$ref": "#/definitions/samples.MessageKind5", + "additionalProperties": true + }, + "kind6": { + "$ref": "#/definitions/samples.MessageKind6", + "additionalProperties": true + }, + "kind7": { + "$ref": "#/definitions/samples.MessageKind7", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 12" + }, + "samples.MessageKind1": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 1" + }, + "samples.MessageKind11": { + "properties": { + "name": { + "type": "string" + }, + "ones": { + "items": { + "$ref": "#/definitions/samples.MessageKind1" + }, + "type": "array" + }, + "kind2": { + "$ref": "#/definitions/samples.MessageKind2", + "additionalProperties": true + }, + "kind3": { + "$ref": "#/definitions/samples.MessageKind3", + "additionalProperties": true + }, + "kind4": { + "$ref": "#/definitions/samples.MessageKind4", + "additionalProperties": true + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 11" + }, + "samples.MessageKind2": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "isa": { + "type": "boolean" + }, + "hasa": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 2" + }, + "samples.MessageKind3": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "someProp": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 3" + }, + "samples.MessageKind4": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "special": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 4" + }, + "samples.MessageKind5": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "foo": { + "type": "number" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 5" + }, + "samples.MessageKind6": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "bar": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 6" + }, + "samples.MessageKind7": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "baz": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Message Kind 7" + } + } +}` diff --git a/internal/converter/testdata/message_options.go b/internal/converter/testdata/message_options.go new file mode 100644 index 00000000..78d4ff63 --- /dev/null +++ b/internal/converter/testdata/message_options.go @@ -0,0 +1,35 @@ +package testdata + +const MessageOptions = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/MessageOptions", + "definitions": { + "MessageOptions": { + "properties": { + "ignore": { + "type": "boolean", + "description": "Messages tagged with this will not be processed" + }, + "all_fields_required": { + "type": "boolean", + "description": "Messages tagged with this will have all fields marked as \"required\":" + }, + "allow_null_values": { + "type": "boolean", + "description": "Messages tagged with this will additionally accept null values for all properties:" + }, + "disallow_additional_properties": { + "type": "boolean", + "description": "Messages tagged with this will have all fields marked as not allowing additional properties:" + } + }, + "additionalProperties": true, + "type": "object", + "description": "Custom MessageOptions" + } + } +}` + +const MessageOptionsFail = `{"ignore": 12345}` + +const MessageOptionsPass = `{"ignore": true}` diff --git a/internal/converter/testdata/message_with_comments.go b/internal/converter/testdata/message_with_comments.go index b14b869b..7ab868a5 100644 --- a/internal/converter/testdata/message_with_comments.go +++ b/internal/converter/testdata/message_with_comments.go @@ -2,13 +2,24 @@ package testdata const MessageWithComments = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name1": { - "type": "string", - "description": "This field is supposed to represent blahblahblah" + "$ref": "#/definitions/MessageWithComments", + "definitions": { + "MessageWithComments": { + "properties": { + "name1": { + "type": "string", + "description": "This field is supposed to represent blahblahblah" + }, + "excludedComment": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "This is a leading detached comment (which becomes the title)", + "description": "This is a leading detached comment (which becomes the title) This is a message level comment and talks about what this message is and why you should care about it!" } - }, - "additionalProperties": true, - "type": "object", - "description": "This is a message level comment and talks about what this message is and why you should care about it!" + } }` + +const MessageWithCommentsFail = `{"name1": 12345}` diff --git a/internal/converter/testdata/nested_message.go b/internal/converter/testdata/nested_message.go index 15dba579..4e4e68bd 100644 --- a/internal/converter/testdata/nested_message.go +++ b/internal/converter/testdata/nested_message.go @@ -2,8 +2,23 @@ package testdata const NestedMessage = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "payload": { + "$ref": "#/definitions/NestedMessage", + "definitions": { + "NestedMessage": { + "properties": { + "payload": { + "$ref": "#/definitions/samples.PayloadMessage", + "additionalProperties": true + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Nested Message" + }, + "samples.PayloadMessage": { "properties": { "name": { "type": "string" @@ -42,16 +57,25 @@ const NestedMessage = `{ { "type": "integer" } - ] + ], + "title": "Topology" } }, "additionalProperties": true, - "type": "object" - }, - "description": { - "type": "string" + "type": "object", + "title": "Payload Message" } - }, - "additionalProperties": true, - "type": "object" + } +}` + +const NestedMessageFail = `{ + "payload": { + "topology": "ROUND" + } +}` + +const NestedMessagePass = `{ + "payload": { + "topology": "FLAT" + } }` diff --git a/internal/converter/testdata/nested_object.go b/internal/converter/testdata/nested_object.go index 3becb18d..7003f6d3 100644 --- a/internal/converter/testdata/nested_object.go +++ b/internal/converter/testdata/nested_object.go @@ -2,8 +2,23 @@ package testdata const NestedObject = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "payload": { + "$ref": "#/definitions/NestedObject", + "definitions": { + "NestedObject": { + "properties": { + "payload": { + "$ref": "#/definitions/samples.NestedObject.NestedPayload", + "additionalProperties": true + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Nested Object" + }, + "samples.NestedObject.NestedPayload": { "properties": { "name": { "type": "string" @@ -42,16 +57,21 @@ const NestedObject = `{ { "type": "integer" } - ] + ], + "title": "Topology" } }, "additionalProperties": true, - "type": "object" - }, - "description": { - "type": "string" + "type": "object", + "title": "Nested Payload" } - }, - "additionalProperties": true, - "type": "object" + } +}` + +const NestedObjectFail = `{"payload": false}` + +const NestedObjectPass = `{ + "payload": { + "topology": "NESTED_OBJECT" + } }` diff --git a/internal/converter/testdata/oneof.go b/internal/converter/testdata/oneof.go new file mode 100644 index 00000000..95f63a49 --- /dev/null +++ b/internal/converter/testdata/oneof.go @@ -0,0 +1,98 @@ +package testdata + +const OneOf = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/OneOf", + "definitions": { + "OneOf": { + "required": [ + "something" + ], + "properties": { + "bar": { + "$ref": "#/definitions/samples.OneOf.Bar", + "additionalProperties": true + }, + "baz": { + "$ref": "#/definitions/samples.OneOf.Baz", + "additionalProperties": true + }, + "something": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "allOf": [ + { + "oneOf": [ + { + "not": { + "anyOf": [ + { + "required": [ + "bar" + ] + }, + { + "required": [ + "baz" + ] + } + ] + } + }, + { + "required": [ + "bar" + ] + }, + { + "required": [ + "baz" + ] + } + ] + } + ], + "title": "One Of" + }, + "samples.OneOf.Bar": { + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "integer" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Bar" + }, + "samples.OneOf.Baz": { + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Baz" + } + } +}` + +const OneOfFail = `{ + "something": true, + "bar": {"foo": 1}, + "baz": {"foo": "one"} +}` + +const OneOfPass = `{ + "something": true, + "bar": {"foo": 1} +}` diff --git a/internal/converter/testdata/payload_message.go b/internal/converter/testdata/payload_message.go index 8cf27a4e..3580c724 100644 --- a/internal/converter/testdata/payload_message.go +++ b/internal/converter/testdata/payload_message.go @@ -2,47 +2,58 @@ package testdata const PayloadMessage = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name": { - "type": "string" - }, - "timestamp": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "complete": { - "type": "boolean" - }, - "topology": { - "enum": [ - "FLAT", - 0, - "NESTED_OBJECT", - 1, - "NESTED_MESSAGE", - 2, - "ARRAY_OF_TYPE", - 3, - "ARRAY_OF_OBJECT", - 4, - "ARRAY_OF_MESSAGE", - 5 - ], - "oneOf": [ - { + "$ref": "#/definitions/PayloadMessage", + "definitions": { + "PayloadMessage": { + "properties": { + "name": { "type": "string" }, - { + "timestamp": { + "type": "string" + }, + "id": { "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" } - ] + }, + "additionalProperties": true, + "type": "object", + "title": "Payload Message" } - }, - "additionalProperties": true, - "type": "object" + } }` + +const PayloadMessageFail = `{"topology": "MAP"}` + +const PayloadMessagePass = `{"topology": "ARRAY_OF_MESSAGE"}` diff --git a/internal/converter/testdata/payload_message_2.go b/internal/converter/testdata/payload_message_2.go new file mode 100644 index 00000000..db7da771 --- /dev/null +++ b/internal/converter/testdata/payload_message_2.go @@ -0,0 +1,76 @@ +package testdata + +const PayloadMessage2 = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/PayloadMessage2", + "definitions": { + "PayloadMessage2": { + "required": [ + "name", + "timestamp", + "id", + "rating", + "complete", + "topology" + ], + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Payload Message 2", + "description": "PayloadMessage2 contains some common types PayloadMessage2 is used throughout the test suite and can have multi-line comments" + } + } +}` + +const PayloadMessage2Fail = `{ +}` + +const PayloadMessage2Pass = `{ + "name": "test", + "timestamp": "1970-01-01T00:00:00Z", + "id": 1, + "rating": 100, + "complete": true, + "topology": "FLAT" +}` diff --git a/internal/converter/testdata/proto/ArrayOfPrimitives.proto b/internal/converter/testdata/proto/ArrayOfPrimitives.proto index b8c52170..08da8028 100644 --- a/internal/converter/testdata/proto/ArrayOfPrimitives.proto +++ b/internal/converter/testdata/proto/ArrayOfPrimitives.proto @@ -6,5 +6,5 @@ message ArrayOfPrimitives { repeated int32 luckyNumbers = 2; repeated int64 luckyBigNumbers = 3; repeated string keyWords = 4; - int64 big_number = 5; + int64 big_number = 5; } diff --git a/internal/converter/testdata/proto/BytesPayload.proto b/internal/converter/testdata/proto/BytesPayload.proto new file mode 100644 index 00000000..93d73ed5 --- /dev/null +++ b/internal/converter/testdata/proto/BytesPayload.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; +package samples; + +message BytesPayload { + string description = 1; + bytes payload = 2; +} diff --git a/internal/converter/testdata/proto/CyclicalReference.proto b/internal/converter/testdata/proto/CyclicalReference.proto new file mode 100644 index 00000000..77e9647d --- /dev/null +++ b/internal/converter/testdata/proto/CyclicalReference.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package samples; + +message M { + Foo foo = 1; +} + +message Foo { + string name = 1; + repeated Bar bar = 2; +} + +message Bar { + uint32 id = 1; + Baz baz = 2; +} + +message Baz { + bool enabled = 1; + Foo foo = 2; +} diff --git a/internal/converter/testdata/proto/EnumNestedReference.proto b/internal/converter/testdata/proto/EnumNestedReference.proto new file mode 100644 index 00000000..cfea653b --- /dev/null +++ b/internal/converter/testdata/proto/EnumNestedReference.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "PayloadMessage.proto"; + +message Msg { + PayloadMessage.Topology nestedEnumField = 1; +} diff --git a/internal/converter/testdata/proto/EnumWithMessage.proto b/internal/converter/testdata/proto/EnumWithMessage.proto new file mode 100644 index 00000000..f0e0ef8b --- /dev/null +++ b/internal/converter/testdata/proto/EnumWithMessage.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +package samples; + +enum FooBarBaz { + Foo = 0; + Bar = 1; + Baz = 2; +} + +message WithFooBarBaz { + FooBarBaz enumField = 1; +} diff --git a/internal/converter/testdata/proto/Enumception.proto b/internal/converter/testdata/proto/Enumception.proto index f2bdb180..292659ef 100644 --- a/internal/converter/testdata/proto/Enumception.proto +++ b/internal/converter/testdata/proto/Enumception.proto @@ -2,9 +2,10 @@ syntax = "proto3"; package samples; import "PayloadMessage.proto"; -import "ImportedEnum.proto"; message Enumception { + + // FailureModes enum enum FailureModes { RECURSION_ERROR = 0; SYNTAX_ERROR = 1; @@ -18,5 +19,4 @@ message Enumception { FailureModes failureMode = 6; PayloadMessage payload = 7; repeated PayloadMessage payloads = 8; - ImportedEnum importedEnum = 9; } diff --git a/internal/converter/testdata/proto/GoogleInt64Value.proto b/internal/converter/testdata/proto/GoogleInt64Value.proto new file mode 100644 index 00000000..5f37e367 --- /dev/null +++ b/internal/converter/testdata/proto/GoogleInt64Value.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/wrappers.proto"; + +message GoogleInt64Value { + google.protobuf.Int64Value big_number = 1; +} diff --git a/internal/converter/testdata/proto/GoogleInt64ValueAllowNull.proto b/internal/converter/testdata/proto/GoogleInt64ValueAllowNull.proto new file mode 100644 index 00000000..83ba8746 --- /dev/null +++ b/internal/converter/testdata/proto/GoogleInt64ValueAllowNull.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/wrappers.proto"; + +message GoogleInt64ValueAllowNull { + google.protobuf.Int64Value big_number = 1; +} diff --git a/internal/converter/testdata/proto/GoogleInt64ValueDisallowString.proto b/internal/converter/testdata/proto/GoogleInt64ValueDisallowString.proto new file mode 100644 index 00000000..069b12b0 --- /dev/null +++ b/internal/converter/testdata/proto/GoogleInt64ValueDisallowString.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/wrappers.proto"; + +message GoogleInt64ValueDisallowString { + google.protobuf.Int64Value big_number = 1; +} diff --git a/internal/converter/testdata/proto/GoogleInt64ValueDisallowStringAllowNull.proto b/internal/converter/testdata/proto/GoogleInt64ValueDisallowStringAllowNull.proto new file mode 100644 index 00000000..61e96f17 --- /dev/null +++ b/internal/converter/testdata/proto/GoogleInt64ValueDisallowStringAllowNull.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/wrappers.proto"; + +message GoogleInt64ValueDisallowStringAllowNull { + google.protobuf.Int64Value big_number = 1; +} diff --git a/internal/converter/testdata/proto/GoogleValue.proto b/internal/converter/testdata/proto/GoogleValue.proto new file mode 100644 index 00000000..0572668b --- /dev/null +++ b/internal/converter/testdata/proto/GoogleValue.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/struct.proto"; + +message GoogleValue { + google.protobuf.Value arg = 1; + google.protobuf.ListValue some_list = 2; + google.protobuf.Struct some_struct = 3; +} \ No newline at end of file diff --git a/internal/converter/testdata/proto/ImportedEnum.proto b/internal/converter/testdata/proto/ImportedEnum.proto deleted file mode 100644 index bfeb0194..00000000 --- a/internal/converter/testdata/proto/ImportedEnum.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; -package samples; - -enum ImportedEnum { - VALUE_0 = 0; - VALUE_1 = 1; - VALUE_2 = 2; - VALUE_3 = 3; -} diff --git a/internal/converter/testdata/proto/JSONFields.proto b/internal/converter/testdata/proto/JSONFields.proto new file mode 100644 index 00000000..b0b1f5cc --- /dev/null +++ b/internal/converter/testdata/proto/JSONFields.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package samples; + + +message JSONFields { + string name = 1; + string timestamp = 2; + int32 id = 3 [json_name="identifier"]; + float some_thing = 4 [json_name="someThing"]; + bool complete = 5; + int64 snake_numb = 6; + int32 other_numb = 7; +} diff --git a/internal/converter/testdata/proto/MessageWithComments.proto b/internal/converter/testdata/proto/MessageWithComments.proto index 43958911..57666059 100644 --- a/internal/converter/testdata/proto/MessageWithComments.proto +++ b/internal/converter/testdata/proto/MessageWithComments.proto @@ -1,9 +1,14 @@ syntax = "proto3"; package samples; +// This is a leading detached comment (which becomes the title) + // This is a message level comment and talks about what this message is and why you should care about it! message MessageWithComments { // This field is supposed to represent blahblahblah string name1 = 1; + + // The comment for this field should be omitted because it contains @exclude + string excludedComment = 2; } diff --git a/internal/converter/testdata/proto/NestedMessage.proto b/internal/converter/testdata/proto/NestedMessage.proto index d166e96f..0245f087 100644 --- a/internal/converter/testdata/proto/NestedMessage.proto +++ b/internal/converter/testdata/proto/NestedMessage.proto @@ -6,4 +6,4 @@ import "PayloadMessage.proto"; message NestedMessage { PayloadMessage payload = 1; string description = 2; -} \ No newline at end of file +} diff --git a/internal/converter/testdata/proto/NoPackage.proto b/internal/converter/testdata/proto/NoPackage.proto new file mode 100644 index 00000000..88fa8922 --- /dev/null +++ b/internal/converter/testdata/proto/NoPackage.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +message NoPackage { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; +} diff --git a/internal/converter/testdata/proto/OneOf.proto b/internal/converter/testdata/proto/OneOf.proto new file mode 100644 index 00000000..f1a11b8d --- /dev/null +++ b/internal/converter/testdata/proto/OneOf.proto @@ -0,0 +1,20 @@ +syntax="proto3"; +package samples; + +message OneOf { + + oneof choice { + Bar bar = 1; + Baz baz = 2; + } + + bool something = 3; + + message Bar { + int32 foo = 1; + } + + message Baz { + string foo = 1; + } +} diff --git a/internal/converter/testdata/proto/PayloadMessage2.proto b/internal/converter/testdata/proto/PayloadMessage2.proto new file mode 100644 index 00000000..839fd7d7 --- /dev/null +++ b/internal/converter/testdata/proto/PayloadMessage2.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +package samples; + +// PayloadMessage2 contains some common types +// +// PayloadMessage2 is used throughout the test suite +// and can have multi-line comments +message PayloadMessage2 { + enum Topology { + FLAT = 0; + NESTED_OBJECT = 1; + NESTED_MESSAGE = 2; + ARRAY_OF_TYPE = 3; + ARRAY_OF_OBJECT = 4; + ARRAY_OF_MESSAGE = 5; + } + + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + Topology topology = 6; +} diff --git a/internal/converter/testdata/proto/Proto2NestedMessage.proto b/internal/converter/testdata/proto/Proto2NestedMessage.proto new file mode 100644 index 00000000..096cca95 --- /dev/null +++ b/internal/converter/testdata/proto/Proto2NestedMessage.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; +package samples; + +message Proto2PayloadMessage { + enum Topology { + FLAT = 0; + NESTED_OBJECT = 1; + NESTED_MESSAGE = 2; + ARRAY_OF_TYPE = 3; + ARRAY_OF_OBJECT = 4; + ARRAY_OF_MESSAGE = 5; + } + + required string name = 1; + optional string timestamp = 2; + required int32 id = 3; + optional float rating = 4; + optional bool complete = 5; + optional Topology topology = 6; +} + +message Proto2NestedMessage { + required Proto2PayloadMessage payload = 1; + optional string description = 2; +} \ No newline at end of file diff --git a/internal/converter/testdata/proto/Proto2NestedObject.proto b/internal/converter/testdata/proto/Proto2NestedObject.proto new file mode 100644 index 00000000..09f9aea9 --- /dev/null +++ b/internal/converter/testdata/proto/Proto2NestedObject.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; +package samples; + +message Proto2NestedObject { + message NestedPayload { + enum Topology { + FLAT = 0; + NESTED_OBJECT = 1; + NESTED_MESSAGE = 2; + ARRAY_OF_TYPE = 3; + ARRAY_OF_OBJECT = 4; + ARRAY_OF_MESSAGE = 5; + } + + required string name = 1; + optional string timestamp = 2; + required int32 id = 3; + optional float rating = 4; + optional bool complete = 5; + optional Topology topology = 6; + } + + required NestedPayload payload = 1; + optional string description = 2; +} diff --git a/internal/converter/testdata/proto/Proto2Required.proto b/internal/converter/testdata/proto/Proto2Required.proto new file mode 100644 index 00000000..8d2aec38 --- /dev/null +++ b/internal/converter/testdata/proto/Proto2Required.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +package samples; + +message Proto2Required { + required string query = 1; + optional int32 page_number = 2; + optional int32 result_per_page = 3; +} diff --git a/internal/converter/testdata/proto/SelfReference.proto b/internal/converter/testdata/proto/SelfReference.proto new file mode 100644 index 00000000..bfd6bb82 --- /dev/null +++ b/internal/converter/testdata/proto/SelfReference.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; +package samples; + +message Foo { + string name = 1; + repeated Foo bar = 2; +} diff --git a/internal/converter/testdata/proto/Timestamp.proto b/internal/converter/testdata/proto/Timestamp.proto new file mode 100644 index 00000000..1606edb6 --- /dev/null +++ b/internal/converter/testdata/proto/Timestamp.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package samples; + +import "google/protobuf/timestamp.proto"; + +message Timestamp { + google.protobuf.Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/internal/converter/testdata/proto/TwelveMessages.proto b/internal/converter/testdata/proto/TwelveMessages.proto new file mode 100644 index 00000000..2c525312 --- /dev/null +++ b/internal/converter/testdata/proto/TwelveMessages.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; +package samples; + +message MessageKind1 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; +} + +message MessageKind2 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + bool isa = 6; + bool hasa = 7; +} + +message MessageKind3 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + string someProp = 6; +} + +message MessageKind4 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + string special = 6; +} + +message MessageKind5 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + double foo = 6; +} + +message MessageKind6 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + string bar = 6; +} + +message MessageKind7 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; + int64 baz = 6; +} + +message MessageKind8 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; +} + +message MessageKind9 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; +} + +message MessageKind10 { + string name = 1; + string timestamp = 2; + int32 id = 3; + float rating = 4; + bool complete = 5; +} + +message MessageKind11 { + string name = 1; + repeated MessageKind1 ones = 2; + oneof iface { + MessageKind2 kind2 = 3; + MessageKind3 kind3 = 4; + MessageKind4 kind4 = 5; + } +} + +message MessageKind12 { + string name = 1; + MessageKind11 f = 2; + oneof iface { + MessageKind5 kind5 = 3; + MessageKind6 kind6 = 4; + MessageKind7 kind7 = 5; + } +} diff --git a/internal/converter/testdata/proto/WellKnown.proto b/internal/converter/testdata/proto/WellKnown.proto index f2ff5aff..827c2c4d 100644 --- a/internal/converter/testdata/proto/WellKnown.proto +++ b/internal/converter/testdata/proto/WellKnown.proto @@ -1,5 +1,7 @@ syntax = "proto3"; package samples; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; import "google/protobuf/wrappers.proto"; message WellKnown { @@ -7,5 +9,8 @@ message WellKnown { map map_of_integers = 2; map map_of_scalar_integers = 3; repeated google.protobuf.Int32Value list_of_integers = 4; + // This is a duration: + google.protobuf.Duration duration = 5; + google.protobuf.Struct struct = 6; } diff --git a/internal/converter/testdata/proto2_nested_message.go b/internal/converter/testdata/proto2_nested_message.go new file mode 100644 index 00000000..ba9d996c --- /dev/null +++ b/internal/converter/testdata/proto2_nested_message.go @@ -0,0 +1,90 @@ +package testdata + +const Proto2NestedMessage = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Proto2NestedMessage", + "definitions": { + "Proto2NestedMessage": { + "required": [ + "payload" + ], + "properties": { + "payload": { + "$ref": "#/definitions/samples.Proto2PayloadMessage", + "additionalProperties": false + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Proto 2 Nested Message" + }, + "samples.Proto2PayloadMessage": { + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Proto 2 Payload Message" + } + } +}` + +const Proto2NestedMessageFail = `{ + "payload": { + "topology": "FLAT" + } +}` + +const Proto2NestedMessagePass = `{ + "payload": { + "id": 1, + "name": "something", + "topology": "FLAT" + } +}` diff --git a/internal/converter/testdata/proto2_nested_object.go b/internal/converter/testdata/proto2_nested_object.go new file mode 100644 index 00000000..ed0b363b --- /dev/null +++ b/internal/converter/testdata/proto2_nested_object.go @@ -0,0 +1,99 @@ +package testdata + +const Proto2NestedObject = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Proto2NestedObject", + "definitions": { + "Proto2NestedObject": { + "required": [ + "payload", + "description" + ], + "properties": { + "payload": { + "$ref": "#/definitions/samples.Proto2NestedObject.NestedPayload", + "additionalProperties": false + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Proto 2 Nested Object" + }, + "samples.Proto2NestedObject.NestedPayload": { + "required": [ + "name", + "timestamp", + "id", + "rating", + "complete", + "topology" + ], + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Nested Payload" + } + } +}` + +const Proto2NestedObjectFail = `{ + "payload": { + "topology": "FLAT" + } +}` + +const Proto2NestedObjectPass = `{ + "description": "lots of attributes", + "payload": { + "name": "something", + "timestamp": "1970-01-01T00:00:00Z", + "id": 1, + "rating": 100, + "complete": true, + "topology": "FLAT" + } +}` diff --git a/internal/converter/testdata/proto2_payload_message.go b/internal/converter/testdata/proto2_payload_message.go new file mode 100644 index 00000000..80c59820 --- /dev/null +++ b/internal/converter/testdata/proto2_payload_message.go @@ -0,0 +1,69 @@ +package testdata + +const Proto2PayloadMessage = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Proto2PayloadMessage", + "definitions": { + "Proto2PayloadMessage": { + "required": [ + "name", + "id" + ], + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Proto 2 Payload Message" + } + } +}` + +const Proto2PayloadMessageFail = `{ + "complete": false +}` + +const Proto2PayloadMessagePass = `{ + "id": 1, + "name": "something", + "topology": "FLAT" +}` diff --git a/internal/converter/testdata/proto2_required.go b/internal/converter/testdata/proto2_required.go new file mode 100644 index 00000000..70fb5feb --- /dev/null +++ b/internal/converter/testdata/proto2_required.go @@ -0,0 +1,36 @@ +package testdata + +const Proto2Required = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Proto2Required", + "definitions": { + "Proto2Required": { + "required": [ + "query" + ], + "properties": { + "query": { + "type": "string" + }, + "page_number": { + "type": "integer" + }, + "result_per_page": { + "type": "integer" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Proto 2 Required" + } + } +}` + +const Proto2RequiredFail = `{ + "page_number": 4 +}` + +const Proto2RequiredPass = `{ + "query": "what?", + "page_number": 4 +}` diff --git a/internal/converter/testdata/second_enum.go b/internal/converter/testdata/second_enum.go index e3142f77..6364f9a4 100644 --- a/internal/converter/testdata/second_enum.go +++ b/internal/converter/testdata/second_enum.go @@ -19,5 +19,10 @@ const SecondEnum = `{ { "type": "integer" } - ] + ], + "title": "Second Enum" }` + +const SecondEnumFail = `"VALUE_3"` + +const SecondEnumPass = `"VALUE_7"` diff --git a/internal/converter/testdata/second_message.go b/internal/converter/testdata/second_message.go index a66933ef..719e05a6 100644 --- a/internal/converter/testdata/second_message.go +++ b/internal/converter/testdata/second_message.go @@ -2,23 +2,33 @@ package testdata const SecondMessage = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "name2": { - "type": "string" - }, - "timestamp2": { - "type": "string" - }, - "id2": { - "type": "integer" - }, - "rating2": { - "type": "number" - }, - "complete2": { - "type": "boolean" + "$ref": "#/definitions/SecondMessage", + "definitions": { + "SecondMessage": { + "properties": { + "name2": { + "type": "string" + }, + "timestamp2": { + "type": "string" + }, + "id2": { + "type": "integer" + }, + "rating2": { + "type": "number" + }, + "complete2": { + "type": "boolean" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Second Message" } - }, - "additionalProperties": true, - "type": "object" + } }` + +const SecondMessageFail = `{"complete2": "hello"}` + +const SecondMessagePass = `{"complete2": true}` diff --git a/internal/converter/testdata/self_reference.go b/internal/converter/testdata/self_reference.go new file mode 100644 index 00000000..8403c795 --- /dev/null +++ b/internal/converter/testdata/self_reference.go @@ -0,0 +1,45 @@ +package testdata + +const SelfReference = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Foo", + "definitions": { + "Foo": { + "properties": { + "name": { + "type": "string" + }, + "bar": { + "items": { + "$ref": "#/definitions/Foo" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Foo" + } + } +}` + +const SelfReferenceFail = `{ + "bar": [ + { + "name": false + } + ] +}` + +const SelfReferencePass = `{ + "bar": [ + { + "name": "referenced-bar", + "bar": [ + { + "name": "barception" + } + ] + } + ] +}` diff --git a/internal/converter/testdata/timestamp.go b/internal/converter/testdata/timestamp.go new file mode 100644 index 00000000..5f06e7de --- /dev/null +++ b/internal/converter/testdata/timestamp.go @@ -0,0 +1,23 @@ +package testdata + +const Timestamp = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Timestamp", + "definitions": { + "Timestamp": { + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Timestamp" + } + } +}` + +const TimestampFail = `{"timestamp": "twelve oclock"}` + +const TimestampPass = `{"timestamp": "1970-01-01T00:00:00Z"}` diff --git a/internal/converter/testdata/type_names_with_no_package.go b/internal/converter/testdata/type_names_with_no_package.go new file mode 100644 index 00000000..73dcdc15 --- /dev/null +++ b/internal/converter/testdata/type_names_with_no_package.go @@ -0,0 +1,85 @@ +package testdata + +const TypeNamesWithNoPackage = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ArrayOfMessages", + "definitions": { + "ArrayOfMessages": { + "properties": { + "description": { + "type": "string" + }, + "payload": { + "items": { + "$ref": "#/definitions/PayloadMessage" + }, + "type": "array" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Array Of Messages" + }, + "PayloadMessage": { + "properties": { + "name": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rating": { + "type": "number" + }, + "complete": { + "type": "boolean" + }, + "topology": { + "enum": [ + "FLAT", + 0, + "NESTED_OBJECT", + 1, + "NESTED_MESSAGE", + 2, + "ARRAY_OF_TYPE", + 3, + "ARRAY_OF_OBJECT", + 4, + "ARRAY_OF_MESSAGE", + 5 + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Topology" + } + }, + "additionalProperties": true, + "type": "object", + "title": "Payload Message" + } + } +}` + +const TypeNamesWithNoPackageFail = `{ + "description": "something", + "payload": [ + {"topology": "cruft"} + ] +}` + +const TypeNamesWithNoPackagePass = `{ + "description": "something", + "payload": [ + {"topology": "ARRAY_OF_MESSAGE"} + ] +}` diff --git a/internal/converter/testdata/wellknown.go b/internal/converter/testdata/wellknown.go index 02b49d8d..0e244041 100644 --- a/internal/converter/testdata/wellknown.go +++ b/internal/converter/testdata/wellknown.go @@ -2,50 +2,53 @@ package testdata const WellKnown = `{ "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "string_value": { - "oneOf": [ - { - "type": "null" - }, - { + "$ref": "#/definitions/WellKnown", + "definitions": { + "WellKnown": { + "properties": { + "string_value": { + "additionalProperties": true, "type": "string" - } - ] - }, - "map_of_integers": { - "additionalProperties": { - "oneOf": [ - { - "type": "null" - }, - { + }, + "map_of_integers": { + "additionalProperties": { + "additionalProperties": true, "type": "integer" - } - ] - }, - "type": "object" - }, - "map_of_scalar_integers": { - "additionalProperties": { - "type": "integer" - }, - "type": "object" - }, - "list_of_integers": { - "items": { - "oneOf": [ - { - "type": "null" }, - { + "type": "object" + }, + "map_of_scalar_integers": { + "additionalProperties": { "type": "integer" - } - ] + }, + "type": "object" + }, + "list_of_integers": { + "items": { + "type": "integer", + "title": "Int 32 Value", + "description": "Wrapper message for ` + "`int32`" + `. The JSON representation for ` + "`Int32Value`" + ` is JSON number." + }, + "type": "array" + }, + "duration": { + "pattern": "^([0-9]+\\.?[0-9]*|\\.[0-9]+)s$", + "type": "string", + "description": "This is a duration:", + "format": "regex" + }, + "struct": { + "additionalProperties": true, + "type": "object" + } }, - "type": "array" + "additionalProperties": true, + "type": "object", + "title": "Well Known" } - }, - "additionalProperties": true, - "type": "object" + } }` + +const WellKnownFail = `{"duration": "9"}` + +const WellKnownPass = `{"duration": "9s"}` diff --git a/internal/converter/types.go b/internal/converter/types.go index 4e5b0ae1..45772f16 100644 --- a/internal/converter/types.go +++ b/internal/converter/types.go @@ -6,89 +6,87 @@ import ( "strings" "github.com/alecthomas/jsonschema" - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/iancoleman/orderedmap" "github.com/xeipuuv/gojsonschema" + "google.golang.org/protobuf/types/descriptorpb" ) var ( - globalPkg = &ProtoPackage{ - name: "", - parent: nil, - children: make(map[string]*ProtoPackage), - types: make(map[string]*descriptor.DescriptorProto), - } - - wellKnownTypes = map[string]string{ - "DoubleValue": gojsonschema.TYPE_NUMBER, - "FloatValue": gojsonschema.TYPE_NUMBER, - "Int64Value": gojsonschema.TYPE_STRING, - "UInt64Value": gojsonschema.TYPE_STRING, - "Int32Value": gojsonschema.TYPE_INTEGER, - "UInt32Value": gojsonschema.TYPE_INTEGER, - "BoolValue": gojsonschema.TYPE_BOOLEAN, - "StringValue": gojsonschema.TYPE_STRING, - "BytesValue": gojsonschema.TYPE_STRING, + globalPkg = newProtoPackage(nil, "") + + wellKnownTypes = map[string]bool{ + "BoolValue": true, + "BytesValue": true, + "DoubleValue": true, + "Duration": true, + "FloatValue": true, + "Int32Value": true, + "Int64Value": true, + "ListValue": true, + "StringValue": true, + "Struct": true, + "UInt32Value": true, + "UInt64Value": true, + "Value": true, } ) -func (c *Converter) registerType(pkgName *string, msg *descriptor.DescriptorProto) { +func (c *Converter) registerEnum(pkgName string, enum *descriptorpb.EnumDescriptorProto) { pkg := globalPkg - if pkgName != nil { - for _, node := range strings.Split(*pkgName, ".") { + if pkgName != "" { + for _, node := range strings.Split(pkgName, ".") { if pkg == globalPkg && node == "" { // Skips leading "." continue } child, ok := pkg.children[node] if !ok { - child = &ProtoPackage{ - name: pkg.name + "." + node, - parent: pkg, - children: make(map[string]*ProtoPackage), - types: make(map[string]*descriptor.DescriptorProto), - } + child = newProtoPackage(pkg, node) pkg.children[node] = child } pkg = child } } - pkg.types[msg.GetName()] = msg + pkg.enums[enum.GetName()] = enum } -func (c *Converter) relativelyLookupNestedType(desc *descriptor.DescriptorProto, name string) (*descriptor.DescriptorProto, bool) { - components := strings.Split(name, ".") -componentLoop: - for _, component := range components { - for _, nested := range desc.GetNestedType() { - if nested.GetName() == component { - desc = nested - continue componentLoop +func (c *Converter) registerType(pkgName string, msgDesc *descriptorpb.DescriptorProto) { + pkg := globalPkg + if pkgName != "" { + for _, node := range strings.Split(pkgName, ".") { + if pkg == globalPkg && node == "" { + // Skips leading "." + continue + } + child, ok := pkg.children[node] + if !ok { + child = newProtoPackage(pkg, node) + pkg.children[node] = child } + pkg = child } - c.logger.WithField("component", component).WithField("description", desc.GetName()).Info("no such nested message") - return nil, false } - return desc, true + pkg.types[msgDesc.GetName()] = msgDesc } // Convert a proto "field" (essentially a type-switch with some recursion): -func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDescriptorProto, msg *descriptor.DescriptorProto) (*jsonschema.Type, error) { +func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptorpb.FieldDescriptorProto, msgDesc *descriptorpb.DescriptorProto, duplicatedMessages map[*descriptorpb.DescriptorProto]string, messageFlags ConverterFlags) (*jsonschema.Type, error) { // Prepare a new jsonschema.Type for our eventual return value: jsonSchemaType := &jsonschema.Type{} // Generate a description from src comments (if available) if src := c.sourceInfo.GetField(desc); src != nil { - jsonSchemaType.Description = formatDescription(src) + jsonSchemaType.Title, jsonSchemaType.Description = c.formatTitleAndDescription("", src) } // Switch the types, and pick a JSONSchema equivalent: switch desc.GetType() { - case descriptor.FieldDescriptorProto_TYPE_DOUBLE, - descriptor.FieldDescriptorProto_TYPE_FLOAT: - if c.AllowNullValues { + + // Float32: + case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE, + descriptorpb.FieldDescriptorProto_TYPE_FLOAT: + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, {Type: gojsonschema.TYPE_NUMBER}, @@ -97,12 +95,13 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.Type = gojsonschema.TYPE_NUMBER } - case descriptor.FieldDescriptorProto_TYPE_INT32, - descriptor.FieldDescriptorProto_TYPE_UINT32, - descriptor.FieldDescriptorProto_TYPE_FIXED32, - descriptor.FieldDescriptorProto_TYPE_SFIXED32, - descriptor.FieldDescriptorProto_TYPE_SINT32: - if c.AllowNullValues { + // Int32: + case descriptorpb.FieldDescriptorProto_TYPE_INT32, + descriptorpb.FieldDescriptorProto_TYPE_UINT32, + descriptorpb.FieldDescriptorProto_TYPE_FIXED32, + descriptorpb.FieldDescriptorProto_TYPE_SFIXED32, + descriptorpb.FieldDescriptorProto_TYPE_SINT32: + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, {Type: gojsonschema.TYPE_INTEGER}, @@ -111,56 +110,91 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.Type = gojsonschema.TYPE_INTEGER } - case descriptor.FieldDescriptorProto_TYPE_INT64, - descriptor.FieldDescriptorProto_TYPE_UINT64, - descriptor.FieldDescriptorProto_TYPE_FIXED64, - descriptor.FieldDescriptorProto_TYPE_SFIXED64, - descriptor.FieldDescriptorProto_TYPE_SINT64: - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_INTEGER}) - if !c.DisallowBigIntsAsStrings { - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) + // Int64: + case descriptorpb.FieldDescriptorProto_TYPE_INT64, + descriptorpb.FieldDescriptorProto_TYPE_UINT64, + descriptorpb.FieldDescriptorProto_TYPE_FIXED64, + descriptorpb.FieldDescriptorProto_TYPE_SFIXED64, + descriptorpb.FieldDescriptorProto_TYPE_SINT64: + + // As integer: + if c.Flags.DisallowBigIntsAsStrings { + if messageFlags.AllowNullValues { + jsonSchemaType.OneOf = []*jsonschema.Type{ + {Type: gojsonschema.TYPE_INTEGER}, + {Type: gojsonschema.TYPE_NULL}, + } + } else { + jsonSchemaType.Type = gojsonschema.TYPE_INTEGER + } } - if c.AllowNullValues { - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL}) + + // As string: + if !c.Flags.DisallowBigIntsAsStrings { + if messageFlags.AllowNullValues { + jsonSchemaType.OneOf = []*jsonschema.Type{ + {Type: gojsonschema.TYPE_STRING}, + {Type: gojsonschema.TYPE_NULL}, + } + } else { + jsonSchemaType.Type = gojsonschema.TYPE_STRING + } } - case descriptor.FieldDescriptorProto_TYPE_STRING, - descriptor.FieldDescriptorProto_TYPE_BYTES: - if c.AllowNullValues { + // String: + case descriptorpb.FieldDescriptorProto_TYPE_STRING: + stringDef := &jsonschema.Type{Type: gojsonschema.TYPE_STRING} + + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, - {Type: gojsonschema.TYPE_STRING}, + stringDef, } } else { - jsonSchemaType.Type = gojsonschema.TYPE_STRING + jsonSchemaType.Type = stringDef.Type + jsonSchemaType.MinLength = stringDef.MinLength + jsonSchemaType.MaxLength = stringDef.MaxLength + jsonSchemaType.Pattern = stringDef.Pattern } - case descriptor.FieldDescriptorProto_TYPE_ENUM: - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_INTEGER}) - if c.AllowNullValues { - jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL}) + // Bytes: + case descriptorpb.FieldDescriptorProto_TYPE_BYTES: + if messageFlags.AllowNullValues { + jsonSchemaType.OneOf = []*jsonschema.Type{ + {Type: gojsonschema.TYPE_NULL}, + { + Type: gojsonschema.TYPE_STRING, + Format: "binary", + BinaryEncoding: "base64", + }, + } + } else { + jsonSchemaType.Type = gojsonschema.TYPE_STRING + jsonSchemaType.Format = "binary" + jsonSchemaType.BinaryEncoding = "base64" } - // Go through all the enums we have, see if we can match any to this field by name: - for _, enumDescriptor := range msg.GetEnumType() { - - // Each one has several values: - for _, enumValue := range enumDescriptor.Value { + // ENUM: + case descriptorpb.FieldDescriptorProto_TYPE_ENUM: - // Figure out the entire name of this field: - fullFieldName := fmt.Sprintf(".%v.%v", *msg.Name, *enumDescriptor.Name) + // Go through all the enums we have, see if we can match any to this field. + fullEnumIdentifier := strings.TrimPrefix(desc.GetTypeName(), ".") + matchedEnum, _, ok := c.lookupEnum(curPkg, fullEnumIdentifier) + if !ok { + return nil, fmt.Errorf("unable to resolve enum type: %s", desc.GetType().String()) + } - // If we find ENUM values for this field then put them into the JSONSchema list of allowed ENUM values: - if strings.HasSuffix(desc.GetTypeName(), fullFieldName) { - jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Name) - jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Number) - } - } + // We already have a converter for standalone ENUMs, so just use that: + enumSchema, err := c.convertEnumType(matchedEnum, messageFlags) + if err != nil { + return nil, err } - case descriptor.FieldDescriptorProto_TYPE_BOOL: - if c.AllowNullValues { + jsonSchemaType = &enumSchema + + // Bool: + case descriptorpb.FieldDescriptorProto_TYPE_BOOL: + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, {Type: gojsonschema.TYPE_BOOLEAN}, @@ -169,22 +203,40 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.Type = gojsonschema.TYPE_BOOLEAN } - case descriptor.FieldDescriptorProto_TYPE_GROUP, - descriptor.FieldDescriptorProto_TYPE_MESSAGE: - jsonSchemaType.Type = gojsonschema.TYPE_OBJECT - if desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_OPTIONAL { + // Group (object): + case descriptorpb.FieldDescriptorProto_TYPE_GROUP, descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: + + switch desc.GetTypeName() { + // Make sure that durations match a particular string pattern (eg 3.4s): + case ".google.protobuf.Duration": + jsonSchemaType.Type = gojsonschema.TYPE_STRING + jsonSchemaType.Format = "regex" + jsonSchemaType.Pattern = `^([0-9]+\.?[0-9]*|\.[0-9]+)s$` + case ".google.protobuf.Timestamp": + jsonSchemaType.Type = gojsonschema.TYPE_STRING + jsonSchemaType.Format = "date-time" + case ".google.protobuf.Value", ".google.protobuf.Struct": + jsonSchemaType.Type = gojsonschema.TYPE_OBJECT jsonSchemaType.AdditionalProperties = []byte("true") - } - if desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REQUIRED { - jsonSchemaType.AdditionalProperties = []byte("false") + default: + jsonSchemaType.Type = gojsonschema.TYPE_OBJECT + if desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL { + jsonSchemaType.AdditionalProperties = []byte("true") + } + if desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED { + jsonSchemaType.AdditionalProperties = []byte("false") + } + if messageFlags.DisallowAdditionalProperties { + jsonSchemaType.AdditionalProperties = []byte("false") + } } default: return nil, fmt.Errorf("unrecognized field type: %s", desc.GetType().String()) } - // Recurse array of primitive types: - if desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REPEATED && jsonSchemaType.Type != gojsonschema.TYPE_OBJECT { + // Recurse basic array: + if desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED && jsonSchemaType.Type != gojsonschema.TYPE_OBJECT { jsonSchemaType.Items = &jsonschema.Type{} if len(jsonSchemaType.Enum) > 0 { @@ -196,7 +248,7 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.Items.OneOf = jsonSchemaType.OneOf } - if c.AllowNullValues { + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, {Type: gojsonschema.TYPE_ARRAY}, @@ -205,7 +257,6 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.Type = gojsonschema.TYPE_ARRAY jsonSchemaType.OneOf = []*jsonschema.Type{} } - return jsonSchemaType, nil } @@ -218,8 +269,7 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes } // Recurse the recordType: - recursedJSONSchemaType, err := c.convertMessageType(curPkg, recordType, pkgName) - + recursedJSONSchemaType, err := c.recursiveConvertMessageType(curPkg, recordType, pkgName, duplicatedMessages, false) if err != nil { return nil, err } @@ -231,14 +281,16 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes case recordType.Options.GetMapEntry(): c.logger. WithField("field_name", recordType.GetName()). - WithField("msg_name", *msg.Name). + WithField("msgDesc_name", *msgDesc.Name). Tracef("Is a map") - // Make sure we have a "value": + if recursedJSONSchemaType.Properties == nil { - return nil, fmt.Errorf("Unable to find 'value' property of MAP type") + return nil, fmt.Errorf("Unable to find properties of MAP type") } - value, ok := recursedJSONSchemaType.Properties.Get("value") - if !ok { + + // Make sure we have a "value": + value, valuePresent := recursedJSONSchemaType.Properties.Get("value") + if !valuePresent { return nil, fmt.Errorf("Unable to find 'value' property of MAP type") } @@ -250,65 +302,228 @@ func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptor.FieldDes jsonSchemaType.AdditionalProperties = additionalPropertiesJSON // Arrays: - case desc.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REPEATED: - jsonSchemaType.Items = &recursedJSONSchemaType + case desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED: + jsonSchemaType.Items = recursedJSONSchemaType jsonSchemaType.Type = gojsonschema.TYPE_ARRAY - // Objects: - default: - isPrimitive := true - for _, t := range recursedJSONSchemaType.OneOf { - if t.Type == gojsonschema.TYPE_OBJECT || t.Type == gojsonschema.TYPE_ARRAY { - isPrimitive = false + // Build up the list of required fields: + if messageFlags.AllFieldsRequired && len(recursedJSONSchemaType.OneOf) == 0 && recursedJSONSchemaType.Properties != nil { + for _, property := range recursedJSONSchemaType.Properties.Keys() { + jsonSchemaType.Items.Required = append(jsonSchemaType.Items.Required, property) } } - if recursedJSONSchemaType.OneOf != nil && isPrimitive { - jsonSchemaType.AdditionalProperties = nil - jsonSchemaType.Type = "" - jsonSchemaType.OneOf = recursedJSONSchemaType.OneOf - } else { - jsonSchemaType.Properties = recursedJSONSchemaType.Properties + jsonSchemaType.Items.Required = dedupe(jsonSchemaType.Items.Required) + + // Not maps, not arrays: + default: + + // If we've got optional types then just take those: + if recursedJSONSchemaType.OneOf != nil { + return recursedJSONSchemaType, nil + } + + // If we're not an object then set the type from whatever we recursed: + if recursedJSONSchemaType.Type != gojsonschema.TYPE_OBJECT { + jsonSchemaType.Type = recursedJSONSchemaType.Type + } + + // Assume the attrbutes of the recursed value: + jsonSchemaType.Properties = recursedJSONSchemaType.Properties + jsonSchemaType.Ref = recursedJSONSchemaType.Ref + jsonSchemaType.Required = recursedJSONSchemaType.Required + + // Build up the list of required fields: + if messageFlags.AllFieldsRequired && len(recursedJSONSchemaType.OneOf) == 0 && recursedJSONSchemaType.Properties != nil { + for _, property := range recursedJSONSchemaType.Properties.Keys() { + jsonSchemaType.Required = append(jsonSchemaType.Required, property) + } } } - // Optionally allow NULL values, if not already nullable - if c.AllowNullValues && jsonSchemaType.OneOf == nil { + // Optionally allow NULL values: + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, - {Type: jsonSchemaType.Type}, + {Type: jsonSchemaType.Type, Items: jsonSchemaType.Items}, } jsonSchemaType.Type = "" + jsonSchemaType.Items = nil } } + jsonSchemaType.Required = dedupe(jsonSchemaType.Required) + return jsonSchemaType, nil } // Converts a proto "MESSAGE" into a JSON-Schema: -func (c *Converter) convertMessageType(curPkg *ProtoPackage, msg *descriptor.DescriptorProto, pkgName string) (jsonschema.Type, error) { - if msg.Name != nil && pkgName == ".google.protobuf" { - if jsonType, ok := wellKnownTypes[*msg.Name]; ok { - schema := jsonschema.Type{ - OneOf: []*jsonschema.Type{ - {Type: gojsonschema.TYPE_NULL}, - {Type: jsonType}, - }, - } - return schema, nil +func (c *Converter) convertMessageType(curPkg *ProtoPackage, msgDesc *descriptorpb.DescriptorProto) (*jsonschema.Schema, error) { + + // Get a list of any nested messages in our schema: + duplicatedMessages, err := c.findNestedMessages(curPkg, msgDesc) + if err != nil { + return nil, err + } + + // Build up a list of JSONSchema type definitions for every message: + definitions := jsonschema.Definitions{} + for refmsgDesc, nameWithPackage := range duplicatedMessages { + var typeName string + if c.Flags.TypeNamesWithNoPackage { + typeName = refmsgDesc.GetName() + } else { + typeName = nameWithPackage } + refType, err := c.recursiveConvertMessageType(curPkg, refmsgDesc, "", duplicatedMessages, true) + if err != nil { + return nil, err + } + + // Add the schema to our definitions: + definitions[typeName] = refType } - // Prepare a new jsonschema: - jsonSchemaType := jsonschema.Type{ - Version: jsonschema.Version, + // Put together a JSON schema with our discovered definitions, and a $ref for the root type: + newJSONSchema := &jsonschema.Schema{ + Type: &jsonschema.Type{ + Ref: fmt.Sprintf("%s%s", c.refPrefix, msgDesc.GetName()), + Version: c.schemaVersion, + }, + Definitions: definitions, } + + return newJSONSchema, nil +} + +// findNestedMessages takes a message, and returns a map mapping pointers to messages nested within it: +// these messages become definitions which can be referenced (instead of repeating them every time they're used) +func (c *Converter) findNestedMessages(curPkg *ProtoPackage, msgDesc *descriptorpb.DescriptorProto) (map[*descriptorpb.DescriptorProto]string, error) { + + // Get a list of all nested messages, and how often they occur: + nestedMessages := make(map[*descriptorpb.DescriptorProto]string) + if err := c.recursiveFindNestedMessages(curPkg, msgDesc, msgDesc.GetName(), nestedMessages); err != nil { + return nil, err + } + + // Now filter them: + result := make(map[*descriptorpb.DescriptorProto]string) + for message, messageName := range nestedMessages { + if !message.GetOptions().GetMapEntry() && !strings.HasPrefix(messageName, ".google.protobuf.") { + result[message] = strings.TrimLeft(messageName, ".") + } + } + + return result, nil +} + +func (c *Converter) recursiveFindNestedMessages(curPkg *ProtoPackage, msgDesc *descriptorpb.DescriptorProto, typeName string, nestedMessages map[*descriptorpb.DescriptorProto]string) error { + if _, present := nestedMessages[msgDesc]; present { + return nil + } + nestedMessages[msgDesc] = typeName + + for _, desc := range msgDesc.GetField() { + descType := desc.GetType() + if descType != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE && descType != descriptorpb.FieldDescriptorProto_TYPE_GROUP { + // no nested messages + continue + } + + typeName := desc.GetTypeName() + recordType, _, ok := c.lookupType(curPkg, typeName) + if !ok { + return fmt.Errorf("no such message type named %s", typeName) + } + if err := c.recursiveFindNestedMessages(curPkg, recordType, typeName, nestedMessages); err != nil { + return err + } + } + + return nil +} + +func (c *Converter) recursiveConvertMessageType(curPkg *ProtoPackage, msgDesc *descriptorpb.DescriptorProto, pkgName string, duplicatedMessages map[*descriptorpb.DescriptorProto]string, ignoreDuplicatedMessages bool) (*jsonschema.Type, error) { + + // Prepare a new jsonschema: + jsonSchemaType := new(jsonschema.Type) + + // Set some per-message flags from config and options: + messageFlags := c.Flags + // Generate a description from src comments (if available) - if src := c.sourceInfo.GetMessage(msg); src != nil { - jsonSchemaType.Description = formatDescription(src) + if src := c.sourceInfo.GetMessage(msgDesc); src != nil { + jsonSchemaType.Title, jsonSchemaType.Description = c.formatTitleAndDescription(msgDesc.GetName(), src) + } + + // Handle google's well-known types: + if name := msgDesc.GetName(); wellKnownTypes[name] && pkgName == ".google.protobuf" { + switch name { + case "DoubleValue", "FloatValue": + jsonSchemaType.Type = gojsonschema.TYPE_NUMBER + case "Int32Value", "UInt32Value": + jsonSchemaType.Type = gojsonschema.TYPE_INTEGER + case "Int64Value", "UInt64Value": + // BigInt as ints + if messageFlags.DisallowBigIntsAsStrings { + jsonSchemaType.Type = gojsonschema.TYPE_INTEGER + } else { + + // BigInt as strings + jsonSchemaType.Type = gojsonschema.TYPE_STRING + } + + case "BoolValue": + jsonSchemaType.Type = gojsonschema.TYPE_BOOLEAN + case "BytesValue", "StringValue": + jsonSchemaType.Type = gojsonschema.TYPE_STRING + case "Value": + jsonSchemaType.OneOf = []*jsonschema.Type{ + {Type: gojsonschema.TYPE_ARRAY}, + {Type: gojsonschema.TYPE_BOOLEAN}, + {Type: gojsonschema.TYPE_NUMBER}, + {Type: gojsonschema.TYPE_OBJECT}, + {Type: gojsonschema.TYPE_STRING}, + } + // jsonSchemaType.AdditionalProperties = []byte("true") + case "Duration": + jsonSchemaType.Type = gojsonschema.TYPE_STRING + case "Struct": + jsonSchemaType.Type = gojsonschema.TYPE_OBJECT + // jsonSchemaType.AdditionalProperties = []byte("true") + case "ListValue": + jsonSchemaType.Type = gojsonschema.TYPE_ARRAY + } + + // If we're allowing nulls then prepare a OneOf: + if messageFlags.AllowNullValues { + jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL}, &jsonschema.Type{Type: jsonSchemaType.Type}) + // and clear the Type that was previously set. + jsonSchemaType.Type = "" + return jsonSchemaType, nil + } + + // Otherwise just return this simple type: + return jsonSchemaType, nil + } + + // Set defaults: + jsonSchemaType.Properties = orderedmap.New() + + // Look up references: + if nameWithPackage, ok := duplicatedMessages[msgDesc]; ok && !ignoreDuplicatedMessages { + var typeName string + if c.Flags.TypeNamesWithNoPackage { + typeName = msgDesc.GetName() + } else { + typeName = nameWithPackage + } + return &jsonschema.Type{ + Ref: fmt.Sprintf("%s%s", c.refPrefix, typeName), + }, nil } // Optionally allow NULL values: - if c.AllowNullValues { + if messageFlags.AllowNullValues { jsonSchemaType.OneOf = []*jsonschema.Type{ {Type: gojsonschema.TYPE_NULL}, {Type: gojsonschema.TYPE_OBJECT}, @@ -318,44 +533,93 @@ func (c *Converter) convertMessageType(curPkg *ProtoPackage, msg *descriptor.Des } // disallowAdditionalProperties will prevent validation where extra fields are found (outside of the schema): - if c.DisallowAdditionalProperties { + if messageFlags.DisallowAdditionalProperties { jsonSchemaType.AdditionalProperties = []byte("false") } else { jsonSchemaType.AdditionalProperties = []byte("true") } - c.logger.WithField("message_str", proto.MarshalTextString(msg)).Trace("Converting message") - for _, fieldDesc := range msg.GetField() { - recursedJSONSchemaType, err := c.convertField(curPkg, fieldDesc, msg) + c.logger.WithField("message_str", msgDesc.String()).Trace("Converting message") + for _, fieldDesc := range msgDesc.GetField() { + + // Convert the field into a JSONSchema type: + recursedJSONSchemaType, err := c.convertField(curPkg, fieldDesc, msgDesc, duplicatedMessages, messageFlags) if err != nil { - c.logger.WithError(err).WithField("field_name", fieldDesc.GetName()).WithField("message_name", msg.GetName()).Error("Failed to convert field") - return jsonSchemaType, err + c.logger.WithError(err).WithField("field_name", fieldDesc.GetName()).WithField("message_name", msgDesc.GetName()).Error("Failed to convert field") + return nil, err } - c.logger.WithField("field_name", fieldDesc.GetName()).WithField("type", recursedJSONSchemaType.Type).Debug("Converted field") - if jsonSchemaType.Properties == nil { - jsonSchemaType.Properties = orderedmap.New() + c.logger.WithField("field_name", fieldDesc.GetName()).WithField("type", recursedJSONSchemaType.Type).Trace("Converted field") + + // If this field is part of a OneOf declaration then build that here: + if c.Flags.EnforceOneOf && fieldDesc.OneofIndex != nil && !fieldDesc.GetProto3Optional() { + for { + if *fieldDesc.OneofIndex < int32(len(jsonSchemaType.AllOf)) { + break + } + var notAnyOf = &jsonschema.Type{Not: &jsonschema.Type{AnyOf: []*jsonschema.Type{}}} + jsonSchemaType.AllOf = append(jsonSchemaType.AllOf, &jsonschema.Type{OneOf: []*jsonschema.Type{notAnyOf}}) + } + if c.Flags.UseJSONFieldnamesOnly { + jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf = append(jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf, &jsonschema.Type{Required: []string{fieldDesc.GetJsonName()}}) + jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf[0].Not.AnyOf = append(jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf[0].Not.AnyOf, &jsonschema.Type{Required: []string{fieldDesc.GetJsonName()}}) + } else { + jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf = append(jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf, &jsonschema.Type{Required: []string{fieldDesc.GetName()}}) + jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf[0].Not.AnyOf = append(jsonSchemaType.AllOf[*fieldDesc.OneofIndex].OneOf[0].Not.AnyOf, &jsonschema.Type{Required: []string{fieldDesc.GetName()}}) + } } - jsonSchemaType.Properties.Set(fieldDesc.GetName(), recursedJSONSchemaType) - if c.UseProtoAndJSONFieldnames && fieldDesc.GetName() != fieldDesc.GetJsonName() { + + // Figure out which field names we want to use: + switch { + case c.Flags.UseJSONFieldnamesOnly: + jsonSchemaType.Properties.Set(fieldDesc.GetJsonName(), recursedJSONSchemaType) + case c.Flags.UseProtoAndJSONFieldNames: + jsonSchemaType.Properties.Set(fieldDesc.GetName(), recursedJSONSchemaType) jsonSchemaType.Properties.Set(fieldDesc.GetJsonName(), recursedJSONSchemaType) + default: + jsonSchemaType.Properties.Set(fieldDesc.GetName(), recursedJSONSchemaType) + } + + // Enforce all_fields_required: + if messageFlags.AllFieldsRequired { + if fieldDesc.OneofIndex == nil && !fieldDesc.GetProto3Optional() { + if c.Flags.UseJSONFieldnamesOnly { + jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetJsonName()) + } else { + jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetName()) + } + } } + + // Look for required fields by the proto2 "required" flag: + if fieldDesc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED && fieldDesc.OneofIndex == nil { + if c.Flags.UseJSONFieldnamesOnly { + jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetJsonName()) + } else { + jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetName()) + } + } + } + + // Remove empty properties to keep the final output as clean as possible: + if len(jsonSchemaType.Properties.Keys()) == 0 { + jsonSchemaType.Properties = nil } + // Dedupe required fields: + jsonSchemaType.Required = dedupe(jsonSchemaType.Required) + return jsonSchemaType, nil } -func formatDescription(sl *descriptor.SourceCodeInfo_Location) string { - var lines []string - for _, str := range sl.GetLeadingDetachedComments() { - if s := strings.TrimSpace(str); s != "" { - lines = append(lines, s) +func dedupe(inputStrings []string) []string { + appended := make(map[string]bool) + outputStrings := []string{} + + for _, inputString := range inputStrings { + if !appended[inputString] { + outputStrings = append(outputStrings, inputString) + appended[inputString] = true } } - if s := strings.TrimSpace(sl.GetLeadingComments()); s != "" { - lines = append(lines, s) - } - if s := strings.TrimSpace(sl.GetTrailingComments()); s != "" { - lines = append(lines, s) - } - return strings.Join(lines, "\n\n") + return outputStrings } diff --git a/jsonschemas/Enumception.jsonschema b/jsonschemas/Enumception.jsonschema index 12d18e1c..530e2164 100644 --- a/jsonschemas/Enumception.jsonschema +++ b/jsonschemas/Enumception.jsonschema @@ -125,18 +125,8 @@ "type": "object" }, "type": "array" - }, - "importedEnum": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] } }, "additionalProperties": false, "type": "object" -} \ No newline at end of file +}