diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bbb2513..da4f2fa 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -41,6 +41,24 @@ builds: goarm: - "6" - "7" + - id: protoc-gen-jsonschema + main: ./cmd/protoc-gen-jsonschema + binary: protoc-gen-jsonschema + env: + - CGO_ENABLED=0 + ldflags: + - -w -s -X github.com/marnixbouhuis/confpb/internal/version.GitReleaseTag={{.Tag}} -X github.com/marnixbouhuis/confpb/internal/version.ReleaseVersion={{.Version}} -X github.com/marnixbouhuis/confpb/internal/version.ShortCommit={{.ShortCommit}} + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm + - arm64 + goarm: + - "6" + - "7" checksum: split: true archives: @@ -66,6 +84,17 @@ archives: format_overrides: - goos: windows format: zip + - id: protoc-gen-jsonschema + builds: [ "protoc-gen-jsonschema" ] + format: tar.gz + name_template: >- + {{ .Binary }}_ + {{- title .Os }}_ + {{- .Arch }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip changelog: disable: true release: diff --git a/README.md b/README.md index 5acc8e4..83cd7cb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ - `protoc-gen-env`: Automatically map environment variables to protobuf fields in Go code. - `protoc-gen-default`: Define default values for protobuf fields directly in your `.proto` files. -- erging & Overlaying: Combine multiple protobuf files, allowing one to overlay values from another. +- `protoc-gen-jsonschema`: Generate JSON Schema files for your configuration, enabling IDE autocompletion. +- Merging & Overlaying: Combine multiple protobuf files, allowing one to overlay values from another. - Multi-format Parsing: Seamlessly parse configurations from various formats such as YAML, JSON, binary protobuf, and text protobuf. ## Why Use `confpb`? @@ -125,7 +126,7 @@ Installing a prebuilt binary from release is recommended as this binary contains ### Option 1: Download prebuilt binaries 1. Head to the [releases page](https://github.com/MarnixBouhuis/confpb/releases) to download the appropriate binary for your platform. - - Download both `protoc-gen-env` and `protoc-gen-default`. + - Download `protoc-gen-env`, `protoc-gen-default` and `protoc-gen-jsonschema` (depending on what features you require). 2. Extract the downloaded archives. 3. Move the binaries to a directory in your `$PATH`, such as `/usr/local/bin` or `/bin`. @@ -133,4 +134,5 @@ Installing a prebuilt binary from release is recommended as this binary contains ```bash $ go install github.com/marnixbouhuis/confpb/cmd/protoc-gen-default@latest $ go install github.com/marnixbouhuis/confpb/cmd/protoc-gen-env@latest +$ go install github.com/marnixbouhuis/confpb/cmd/protoc-gen-jsonschema@latest ``` diff --git a/cmd/protoc-gen-jsonschema/main.go b/cmd/protoc-gen-jsonschema/main.go new file mode 100644 index 0000000..227f5b6 --- /dev/null +++ b/cmd/protoc-gen-jsonschema/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/marnixbouhuis/confpb/internal/codegen" + "github.com/marnixbouhuis/confpb/internal/codegen/jsonschemagen" +) + +func main() { + codegen.RunProtocPlugin(jsonschemagen.GenerateFile) +} diff --git a/examples/sample-app/gen/config/v1/config.defaultpb.go b/examples/sample-app/gen/config/v1/config.defaultpb.go index 809e4bd..7bf1dda 100644 --- a/examples/sample-app/gen/config/v1/config.defaultpb.go +++ b/examples/sample-app/gen/config/v1/config.defaultpb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-default. DO NOT EDIT. // Version: unknown, (unknown, unknown) -// Runtime version: 2 +// Runtime version: 7 package configv1 @@ -13,10 +13,10 @@ import ( const ( // Make sure that the generated code version is supported by the installed runtime. // If this compile time check fails, re-generated the code with a newer version of protoc-gen-default. - _ = runtime.EnforceVersion(2 - runtime.MinimumSupportedCodegenVersion) + _ = runtime.EnforceVersion(7 - runtime.MinimumSupportedCodegenVersion) // Make sure that the installed runtime is sufficiently up-to-date for this generated code. // If this compile time check fails, update the runtime package. - _ = runtime.EnforceVersion(runtime.Version - 2) + _ = runtime.EnforceVersion(runtime.Version - 7) ) // ApplicationConfig_ServerConfigFromDefault returns a new instance of ApplicationConfig_ServerConfig containing only default values diff --git a/examples/sample-app/gen/config/v1/config.envpb.go b/examples/sample-app/gen/config/v1/config.envpb.go index 677a5aa..f5ad31d 100644 --- a/examples/sample-app/gen/config/v1/config.envpb.go +++ b/examples/sample-app/gen/config/v1/config.envpb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-env. DO NOT EDIT. // Version: unknown, (unknown, unknown) -// Runtime version: 2 +// Runtime version: 7 package configv1 @@ -13,10 +13,10 @@ import ( const ( // Make sure that the generated code version is supported by the installed runtime. // If this compile time check fails, re-generated the code with a newer version of protoc-gen-env. - _ = runtime.EnforceVersion(2 - runtime.MinimumSupportedCodegenVersion) + _ = runtime.EnforceVersion(7 - runtime.MinimumSupportedCodegenVersion) // Make sure that the installed runtime is sufficiently up-to-date for this generated code. // If this compile time check fails, update the runtime package. - _ = runtime.EnforceVersion(runtime.Version - 2) + _ = runtime.EnforceVersion(runtime.Version - 7) ) func ApplicationConfig_ServerConfigFromEnv() (*ApplicationConfig_ServerConfig, error) { diff --git a/examples/sample-app/gen/config/v1/config.pb.go b/examples/sample-app/gen/config/v1/config.pb.go index 31d64b3..e9c0b39 100644 --- a/examples/sample-app/gen/config/v1/config.pb.go +++ b/examples/sample-app/gen/config/v1/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2-devel -// protoc v5.27.4 +// protoc v5.28.3 // source: config/v1/config.proto package configv1 @@ -184,36 +184,36 @@ var file_config_v1_config_proto_rawDesc = []byte{ 0x12, 0x4c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x09, 0x82, 0x4b, 0x06, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x09, 0x92, 0x4e, 0x06, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x09, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x09, 0x42, 0x30, 0x82, 0x4b, 0x09, 0x53, 0x4f, 0x4d, 0x45, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x8a, - 0x4b, 0x21, 0x9a, 0x02, 0x1e, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x31, 0x0a, + 0x09, 0x42, 0x30, 0x92, 0x4e, 0x09, 0x53, 0x4f, 0x4d, 0x45, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x9a, + 0x4e, 0x21, 0x9a, 0x02, 0x1e, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x31, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x32, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x33, 0x52, 0x08, 0x73, 0x6f, 0x6d, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0e, 0x82, - 0x4b, 0x0b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x52, 0x0a, 0x73, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0e, 0x92, + 0x4e, 0x0b, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x18, 0x82, 0x4b, 0x0d, - 0x53, 0x4f, 0x4d, 0x45, 0x5f, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x8a, 0x4b, 0x05, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x18, 0x92, 0x4e, 0x0d, + 0x53, 0x4f, 0x4d, 0x45, 0x5f, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x9a, 0x4e, 0x05, 0x92, 0x01, 0x02, 0x32, 0x73, 0x52, 0x0c, 0x73, 0x6f, 0x6d, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x71, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x42, 0x28, 0x8a, 0x4b, 0x25, 0xda, 0x02, 0x22, 0x0a, 0x0f, 0x62, 0x04, 0x6b, 0x65, + 0x72, 0x79, 0x42, 0x28, 0x9a, 0x4e, 0x25, 0xda, 0x02, 0x22, 0x0a, 0x0f, 0x62, 0x04, 0x6b, 0x65, 0x79, 0x31, 0xd2, 0x01, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x0a, 0x0f, 0x62, 0x04, 0x6b, 0x65, 0x79, 0x32, 0xd2, 0x01, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x48, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0x82, 0x4b, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x52, 0x04, 0x68, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0x92, 0x4e, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0d, 0x42, 0x07, 0x82, 0x4b, 0x04, 0x50, 0x4f, 0x52, 0x54, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, + 0x0d, 0x42, 0x07, 0x92, 0x4e, 0x04, 0x50, 0x4f, 0x52, 0x54, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x3b, 0x0a, 0x0d, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, diff --git a/examples/sample-app/gen/config/v1/config.schema.json b/examples/sample-app/gen/config/v1/config.schema.json new file mode 100644 index 0000000..6a05571 --- /dev/null +++ b/examples/sample-app/gen/config/v1/config.schema.json @@ -0,0 +1,162 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "config.v1.ApplicationConfig": { + "title": "config.v1.ApplicationConfig", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "keyValue": { + "title": "config.v1.ApplicationConfig.key_value", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "config.v1.ApplicationConfig.KeyValueEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "type": [ + "object" + ] + }, + "server": { + "title": "config.v1.ApplicationConfig.server", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/config.v1.ApplicationConfig.ServerConfig" + } + ] + }, + "serverList": { + "items": { + "title": "config.v1.ApplicationConfig.server_list", + "description": "// Multiple values can also be set for nested messages, the env key specified for the list will be used as prefix.\n// Values can be set using:\n// - SERVER_LIST_1_HOST = \"1.2.3.4\"\n// - SERVER_LIST_1_HOST = \"8080\"\n// - SERVER_LIST_2_HOST = \"127.0.0.1\"\n// - SERVER_LIST_2_HOST = \"433\"\n// ...\n", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/config.v1.ApplicationConfig.ServerConfig" + } + ] + }, + "type": [ + "array" + ] + }, + "someDuration": { + "title": "config.v1.ApplicationConfig.some_duration", + "description": "// Some types have a special mapping. For durations, strings are parsed to durations (e.g. 10s, 10m30s, 1h).\n// Other types that use special parsing include: timestamps, structs, struct values, and fields with the \"bytes\" type.\n", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^-?([0-9]*[.])?[0-9]+(s)$" + }, + "someList": { + "items": { + "title": "config.v1.ApplicationConfig.some_list", + "description": "// Multiple values can be set for a list using:\n// - SOME_LIST_1 = \"item1\"\n// - SOME_LIST_2 = \"item2\"\n// - SOME_LIST_3 = \"item3\"\n// - SOME_LIST_4 = \"item4\"\n// ...\n", + "deprecated": false, + "type": [ + "string" + ] + }, + "type": [ + "array" + ] + } + }, + "type": [ + "object" + ] + }, + "config.v1.ApplicationConfig.ServerConfig": { + "title": "config.v1.ApplicationConfig.ServerConfig", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "host": { + "title": "config.v1.ApplicationConfig.ServerConfig.host", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "port": { + "title": "config.v1.ApplicationConfig.ServerConfig.port", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + } + }, + "type": [ + "object" + ] + } + }, + "title": "config.v1", + "description": "Code generated by protoc-gen-jsonschema. DO NOT EDIT. Schema definitions for config.v1.", + "oneOf": [ + { + "title": "Configuration file format for: config.v1.ApplicationConfig.ServerConfig.", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "@type": { + "type": [ + "string" + ], + "const": "type.googleapis.com/config.v1.ApplicationConfig.ServerConfig" + } + }, + "allOf": [ + { + "$ref": "#/$defs/config.v1.ApplicationConfig.ServerConfig" + } + ], + "type": [ + "object" + ], + "required": [ + "@type" + ] + }, + { + "title": "Configuration file format for: config.v1.ApplicationConfig.", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "@type": { + "type": [ + "string" + ], + "const": "type.googleapis.com/config.v1.ApplicationConfig" + } + }, + "allOf": [ + { + "$ref": "#/$defs/config.v1.ApplicationConfig" + } + ], + "type": [ + "object" + ], + "required": [ + "@type" + ] + } + ] +} diff --git a/examples/sample-app/generate.sh b/examples/sample-app/generate.sh index 4cc3164..878b9e6 100755 --- a/examples/sample-app/generate.sh +++ b/examples/sample-app/generate.sh @@ -7,6 +7,7 @@ rm -rf ./gen/* # - proto-gen-go: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest # - protoc-gen-default: download / install from this repo's release page. # - protoc-gen-env: download / install from this repo's release page. +# - protoc-gen-jsonschema: download / install from this repo's release page. protoc \ --go_out=./gen/ \ --go_opt=paths=source_relative \ @@ -14,6 +15,8 @@ protoc \ --default_opt=paths=source_relative \ --env_out=./gen/ \ --env_opt=paths=source_relative \ + --jsonschema_out=./gen/ \ + --jsonschema_opt=paths=source_relative \ --proto_path=$PWD/../../proto/ \ --proto_path=./proto/ \ $(find ./proto/ -name "*.proto") diff --git a/internal/codegen/jsonschemagen/e2e/snapshot_test.go b/internal/codegen/jsonschemagen/e2e/snapshot_test.go new file mode 100644 index 0000000..a478068 --- /dev/null +++ b/internal/codegen/jsonschemagen/e2e/snapshot_test.go @@ -0,0 +1,20 @@ +package e2e_test + +import ( + "testing" + + "github.com/marnixbouhuis/confpb/internal/codegen/jsonschemagen" + "github.com/marnixbouhuis/confpb/internal/codegen/testutil" + "github.com/stretchr/testify/require" +) + +func TestSnapshot(t *testing.T) { + t.Parallel() + + res := testutil.RunGeneratorForFiles(t, jsonschemagen.GenerateFile, testDataFS, "testdata/e2e.proto") + content := testutil.GetFileFromGenerationResult(t, res, "e2e.schema.json") + + expected, err := testDataFS.ReadFile("testdata/e2e.schema.json") + require.NoError(t, err) + require.Equal(t, string(expected), content) +} diff --git a/internal/codegen/jsonschemagen/e2e/testdata/e2e.proto b/internal/codegen/jsonschemagen/e2e/testdata/e2e.proto new file mode 100644 index 0000000..9de8865 --- /dev/null +++ b/internal/codegen/jsonschemagen/e2e/testdata/e2e.proto @@ -0,0 +1,163 @@ +edition = "2023"; +package testgen; + +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = ".;main"; + +enum TestEnum { + ENUM_UNSPECIFIED = 0; + ENUM_ONE = 1 [deprecated = true]; + ENUM_TWO = 2; +} + +message E2E { + message SubMessage { + string foo = 1; + SubMessage recursive = 2; + E2E recursive_e2e = 3; + } + + E2E recursive = 1; + + // Some comment for double field + double double_field = 2; + // Some comment for float field + float float_field = 3; + int32 int32_field = 4; + int64 int64_field = 5; + uint32 uint32_field = 6; + uint64 uint64_field = 7; + sint32 sint32_field = 8; + sint64 sint64_field = 9; + fixed32 fixed32_field = 10; + fixed64 fixed64_field = 11; + sfixed32 sfixed32_field = 12; + sfixed64 sfixed64_field = 13; + bool bool_field = 14; + string string_field = 15; + bytes bytes_field = 16; + google.protobuf.Duration duration = 17; + google.protobuf.Timestamp timestamp = 18; + google.protobuf.Struct struct = 19; + google.protobuf.Value value = 20; + SubMessage sub_message = 21; + TestEnum enum = 22; + + // Some comment for deprecated double field + double double_field_deprecated = 23 [deprecated = true]; + // Some comment for deprecated float field + float float_field_deprecated = 24 [deprecated = true]; + int32 int32_field_deprecated = 25 [deprecated = true]; + int64 int64_field_deprecated = 26 [deprecated = true]; + uint32 uint32_field_deprecated = 27 [deprecated = true]; + uint64 uint64_field_deprecated = 28 [deprecated = true]; + sint32 sint32_field_deprecated = 29 [deprecated = true]; + sint64 sint64_field_deprecated = 30 [deprecated = true]; + fixed32 fixed32_field_deprecated = 31 [deprecated = true]; + fixed64 fixed64_field_deprecated = 32 [deprecated = true]; + sfixed32 sfixed32_field_deprecated = 33 [deprecated = true]; + sfixed64 sfixed64_field_deprecated = 34 [deprecated = true]; + bool bool_field_deprecated = 35 [deprecated = true]; + string string_field_deprecated = 36 [deprecated = true]; + bytes bytes_field_deprecated = 37 [deprecated = true]; + google.protobuf.Duration duration_deprecated = 38 [deprecated = true]; + google.protobuf.Timestamp timestamp_deprecated = 39 [deprecated = true]; + google.protobuf.Struct struct_deprecated = 40 [deprecated = true]; + google.protobuf.Value value_deprecated = 41 [deprecated = true]; + SubMessage sub_message_deprecated = 42 [deprecated = true]; + TestEnum enum_deprecated = 43 [deprecated = true]; + + // Some comment for repeated double field + repeated double double_field_repeated = 44; + // Some comment for repeated float field + repeated float float_field_repeated = 45; + repeated int32 int32_field_repeated = 46; + repeated int64 int64_field_repeated = 47; + repeated uint32 uint32_field_repeated = 48; + repeated uint64 uint64_field_repeated = 49; + repeated sint32 sint32_field_repeated = 50; + repeated sint64 sint64_field_repeated = 51; + repeated fixed32 fixed32_field_repeated = 52; + repeated fixed64 fixed64_field_repeated = 53; + repeated sfixed32 sfixed32_field_repeated = 54; + repeated sfixed64 sfixed64_field_repeated = 55; + repeated bool bool_field_repeated = 56; + repeated string string_field_repeated = 57; + repeated bytes bytes_field_repeated = 58; + repeated google.protobuf.Duration duration_repeated = 59; + repeated google.protobuf.Timestamp timestamp_repeated = 60; + repeated google.protobuf.Struct struct_repeated = 61; + repeated google.protobuf.Value value_repeated = 62; + repeated SubMessage sub_message_repeated = 63; + repeated TestEnum enum_repeated = 64; + + // All possible map key types + map map_int32_string = 65; + map map_int64_string = 66; + map map_uint32_string = 67; + map map_uint64_string = 68; + map map_sint32_string = 69; + map map_sint64_string = 70; + map map_fixed32_string = 71; + map map_fixed64_string = 72; + map map_sfixed32_string = 73; + map map_sfixed64_string = 74; + map map_bool_string = 75; + map map_string_string = 76; + + // All possible map value types + map map_string_double = 77; + map map_string_float = 78; + map map_string_int32 = 79; + map map_string_int64 = 80; + map map_string_uint32 = 81; + map map_string_uint64 = 82; + map map_string_sint32 = 83; + map map_string_sint64 = 84; + map map_string_fixed32 = 85; + map map_string_fixed64 = 86; + map map_string_sfixed32 = 87; + map map_string_sfixed64 = 88; + map map_string_bool = 89; + map map_string_bytes = 90; + map map_string_enum = 91; + map map_string_message = 92; + + oneof oneof_a { + E2E recursive_oneof = 93; + + // Some comment for double field + double double_field_oneof = 94; + // Some comment for float field + float float_field_oneof = 95; + int32 int32_field_oneof = 96; + int64 int64_field_oneof = 97; + uint32 uint32_field_oneof = 98; + uint64 uint64_field_oneof = 99; + sint32 sint32_field_oneof = 100; + sint64 sint64_field_oneof = 101; + fixed32 fixed32_field_oneof = 102; + fixed64 fixed64_field_oneof = 103; + sfixed32 sfixed32_field_oneof = 104; + sfixed64 sfixed64_field_oneof = 105; + bool bool_field_oneof = 106; + string string_field_oneof = 107; + bytes bytes_field_oneof = 108; + google.protobuf.Duration duration_oneof = 109; + google.protobuf.Timestamp timestamp_oneof = 110; + google.protobuf.Struct struct_oneof = 111; + google.protobuf.Value value_oneof = 112; + SubMessage sub_message_oneof = 113; + TestEnum enum_oneof = 114; + } + + // Make sure multiple oneof groups are supported + oneof oneof_b { + string oneof_b_option_1 = 115; + string oneof_b_option_2 = 116; + string oneof_b_option_3 = 117; + } +} diff --git a/internal/codegen/jsonschemagen/e2e/testdata/e2e.schema.json b/internal/codegen/jsonschemagen/e2e/testdata/e2e.schema.json new file mode 100644 index 0000000..308876d --- /dev/null +++ b/internal/codegen/jsonschemagen/e2e/testdata/e2e.schema.json @@ -0,0 +1,1848 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "testgen.E2E": { + "title": "testgen.E2E", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "boolField": { + "title": "testgen.E2E.bool_field", + "description": "", + "deprecated": false, + "type": [ + "boolean" + ] + }, + "boolFieldDeprecated": { + "title": "testgen.E2E.bool_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "boolean" + ] + }, + "boolFieldRepeated": { + "items": { + "title": "testgen.E2E.bool_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "boolean" + ] + }, + "type": [ + "array" + ] + }, + "bytesField": { + "title": "testgen.E2E.bytes_field", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "contentEncoding": "base64" + }, + "bytesFieldDeprecated": { + "title": "testgen.E2E.bytes_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "string" + ], + "contentEncoding": "base64" + }, + "bytesFieldRepeated": { + "items": { + "title": "testgen.E2E.bytes_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "contentEncoding": "base64" + }, + "type": [ + "array" + ] + }, + "doubleField": { + "title": "testgen.E2E.double_field", + "description": "// Some comment for double field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 1.7976931348623157e+308, + "minimum": 5e-324 + }, + "doubleFieldDeprecated": { + "title": "testgen.E2E.double_field_deprecated", + "description": "// Some comment for deprecated double field\n", + "deprecated": true, + "type": [ + "number" + ], + "maximum": 1.7976931348623157e+308, + "minimum": 5e-324 + }, + "doubleFieldRepeated": { + "items": { + "title": "testgen.E2E.double_field_repeated", + "description": "// Some comment for repeated double field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 1.7976931348623157e+308, + "minimum": 5e-324 + }, + "type": [ + "array" + ] + }, + "duration": { + "title": "testgen.E2E.duration", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^-?([0-9]*[.])?[0-9]+(s)$" + }, + "durationDeprecated": { + "title": "testgen.E2E.duration_deprecated", + "description": "", + "deprecated": true, + "type": [ + "string" + ], + "pattern": "^-?([0-9]*[.])?[0-9]+(s)$" + }, + "durationRepeated": { + "items": { + "title": "testgen.E2E.duration_repeated", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^-?([0-9]*[.])?[0-9]+(s)$" + }, + "type": [ + "array" + ] + }, + "enum": { + "title": "testgen.E2E.enum", + "description": "", + "deprecated": false, + "oneOf": [ + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_UNSPECIFIED" + }, + { + "deprecated": true, + "type": [ + "string" + ], + "const": "ENUM_ONE" + }, + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_TWO" + } + ] + }, + "enumDeprecated": { + "title": "testgen.E2E.enum_deprecated", + "description": "", + "deprecated": true, + "oneOf": [ + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_UNSPECIFIED" + }, + { + "deprecated": true, + "type": [ + "string" + ], + "const": "ENUM_ONE" + }, + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_TWO" + } + ] + }, + "enumRepeated": { + "items": { + "title": "testgen.E2E.enum_repeated", + "description": "", + "deprecated": false, + "oneOf": [ + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_UNSPECIFIED" + }, + { + "deprecated": true, + "type": [ + "string" + ], + "const": "ENUM_ONE" + }, + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_TWO" + } + ] + }, + "type": [ + "array" + ] + }, + "fixed32Field": { + "title": "testgen.E2E.fixed32_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "fixed32FieldDeprecated": { + "title": "testgen.E2E.fixed32_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "fixed32FieldRepeated": { + "items": { + "title": "testgen.E2E.fixed32_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "type": [ + "array" + ] + }, + "fixed64Field": { + "title": "testgen.E2E.fixed64_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "fixed64FieldDeprecated": { + "title": "testgen.E2E.fixed64_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "fixed64FieldRepeated": { + "items": { + "title": "testgen.E2E.fixed64_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "type": [ + "array" + ] + }, + "floatField": { + "title": "testgen.E2E.float_field", + "description": "// Some comment for float field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 3.4028234663852886e+38, + "minimum": 1.401298464324817e-45 + }, + "floatFieldDeprecated": { + "title": "testgen.E2E.float_field_deprecated", + "description": "// Some comment for deprecated float field\n", + "deprecated": true, + "type": [ + "number" + ], + "maximum": 3.4028234663852886e+38, + "minimum": 1.401298464324817e-45 + }, + "floatFieldRepeated": { + "items": { + "title": "testgen.E2E.float_field_repeated", + "description": "// Some comment for repeated float field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 3.4028234663852886e+38, + "minimum": 1.401298464324817e-45 + }, + "type": [ + "array" + ] + }, + "int32Field": { + "title": "testgen.E2E.int32_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "int32FieldDeprecated": { + "title": "testgen.E2E.int32_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "int32FieldRepeated": { + "items": { + "title": "testgen.E2E.int32_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "array" + ] + }, + "int64Field": { + "title": "testgen.E2E.int64_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "int64FieldDeprecated": { + "title": "testgen.E2E.int64_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "int64FieldRepeated": { + "items": { + "title": "testgen.E2E.int64_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "array" + ] + }, + "mapBoolString": { + "title": "testgen.E2E.map_bool_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapBoolStringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^(true|false)$" + }, + "type": [ + "object" + ] + }, + "mapFixed32String": { + "title": "testgen.E2E.map_fixed32_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapFixed32StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^([0-9]|[1-9][0-9]{1,9})$" + }, + "type": [ + "object" + ] + }, + "mapFixed64String": { + "title": "testgen.E2E.map_fixed64_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapFixed64StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^([0-9]|[1-9][0-9]{1,19})$" + }, + "type": [ + "object" + ] + }, + "mapInt32String": { + "title": "testgen.E2E.map_int32_string", + "description": "// All possible map key types\n", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapInt32StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,9})$" + }, + "type": [ + "object" + ] + }, + "mapInt64String": { + "title": "testgen.E2E.map_int64_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapInt64StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,19})$" + }, + "type": [ + "object" + ] + }, + "mapSfixed32String": { + "title": "testgen.E2E.map_sfixed32_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapSfixed32StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,9})$" + }, + "type": [ + "object" + ] + }, + "mapSfixed64String": { + "title": "testgen.E2E.map_sfixed64_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapSfixed64StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,19})$" + }, + "type": [ + "object" + ] + }, + "mapSint32String": { + "title": "testgen.E2E.map_sint32_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapSint32StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,9})$" + }, + "type": [ + "object" + ] + }, + "mapSint64String": { + "title": "testgen.E2E.map_sint64_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapSint64StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^-?([0-9]|[1-9][0-9]{1,19})$" + }, + "type": [ + "object" + ] + }, + "mapStringBool": { + "title": "testgen.E2E.map_string_bool", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringBoolEntry.value", + "description": "", + "deprecated": false, + "type": [ + "boolean" + ] + }, + "type": [ + "object" + ] + }, + "mapStringBytes": { + "title": "testgen.E2E.map_string_bytes", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringBytesEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "contentEncoding": "base64" + }, + "type": [ + "object" + ] + }, + "mapStringDouble": { + "title": "testgen.E2E.map_string_double", + "description": "// All possible map value types\n", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringDoubleEntry.value", + "description": "", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 1.7976931348623157e+308, + "minimum": 5e-324 + }, + "type": [ + "object" + ] + }, + "mapStringEnum": { + "title": "testgen.E2E.map_string_enum", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringEnumEntry.value", + "description": "", + "deprecated": false, + "oneOf": [ + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_UNSPECIFIED" + }, + { + "deprecated": true, + "type": [ + "string" + ], + "const": "ENUM_ONE" + }, + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_TWO" + } + ] + }, + "type": [ + "object" + ] + }, + "mapStringFixed32": { + "title": "testgen.E2E.map_string_fixed32", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringFixed32Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "type": [ + "object" + ] + }, + "mapStringFixed64": { + "title": "testgen.E2E.map_string_fixed64", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringFixed64Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "type": [ + "object" + ] + }, + "mapStringFloat": { + "title": "testgen.E2E.map_string_float", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringFloatEntry.value", + "description": "", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 3.4028234663852886e+38, + "minimum": 1.401298464324817e-45 + }, + "type": [ + "object" + ] + }, + "mapStringInt32": { + "title": "testgen.E2E.map_string_int32", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringInt32Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "object" + ] + }, + "mapStringInt64": { + "title": "testgen.E2E.map_string_int64", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringInt64Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "object" + ] + }, + "mapStringMessage": { + "title": "testgen.E2E.map_string_message", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringMessageEntry.value", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + }, + "type": [ + "object" + ] + }, + "mapStringSfixed32": { + "title": "testgen.E2E.map_string_sfixed32", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringSfixed32Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "object" + ] + }, + "mapStringSfixed64": { + "title": "testgen.E2E.map_string_sfixed64", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringSfixed64Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "object" + ] + }, + "mapStringSint32": { + "title": "testgen.E2E.map_string_sint32", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringSint32Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "object" + ] + }, + "mapStringSint64": { + "title": "testgen.E2E.map_string_sint64", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringSint64Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "object" + ] + }, + "mapStringString": { + "title": "testgen.E2E.map_string_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringStringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "type": [ + "object" + ] + }, + "mapStringUint32": { + "title": "testgen.E2E.map_string_uint32", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringUint32Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "type": [ + "object" + ] + }, + "mapStringUint64": { + "title": "testgen.E2E.map_string_uint64", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapStringUint64Entry.value", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "type": [ + "object" + ] + }, + "mapUint32String": { + "title": "testgen.E2E.map_uint32_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapUint32StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^([0-9]|[1-9][0-9]{1,9})$" + }, + "type": [ + "object" + ] + }, + "mapUint64String": { + "title": "testgen.E2E.map_uint64_string", + "description": "", + "deprecated": false, + "additionalProperties": { + "title": "testgen.E2E.MapUint64StringEntry.value", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "propertyNames": { + "pattern": "^([0-9]|[1-9][0-9]{1,19})$" + }, + "type": [ + "object" + ] + }, + "recursive": { + "title": "testgen.E2E.recursive", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E" + } + ] + }, + "sfixed32Field": { + "title": "testgen.E2E.sfixed32_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "sfixed32FieldDeprecated": { + "title": "testgen.E2E.sfixed32_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "sfixed32FieldRepeated": { + "items": { + "title": "testgen.E2E.sfixed32_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "array" + ] + }, + "sfixed64Field": { + "title": "testgen.E2E.sfixed64_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "sfixed64FieldDeprecated": { + "title": "testgen.E2E.sfixed64_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "sfixed64FieldRepeated": { + "items": { + "title": "testgen.E2E.sfixed64_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "array" + ] + }, + "sint32Field": { + "title": "testgen.E2E.sint32_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "sint32FieldDeprecated": { + "title": "testgen.E2E.sint32_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "sint32FieldRepeated": { + "items": { + "title": "testgen.E2E.sint32_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + }, + "type": [ + "array" + ] + }, + "sint64Field": { + "title": "testgen.E2E.sint64_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "sint64FieldDeprecated": { + "title": "testgen.E2E.sint64_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "sint64FieldRepeated": { + "items": { + "title": "testgen.E2E.sint64_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + }, + "type": [ + "array" + ] + }, + "stringField": { + "title": "testgen.E2E.string_field", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "stringFieldDeprecated": { + "title": "testgen.E2E.string_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "string" + ] + }, + "stringFieldRepeated": { + "items": { + "title": "testgen.E2E.string_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "type": [ + "array" + ] + }, + "struct": { + "title": "testgen.E2E.struct", + "description": "", + "deprecated": false, + "additionalProperties": true, + "type": [ + "object" + ] + }, + "structDeprecated": { + "title": "testgen.E2E.struct_deprecated", + "description": "", + "deprecated": true, + "additionalProperties": true, + "type": [ + "object" + ] + }, + "structRepeated": { + "items": { + "title": "testgen.E2E.struct_repeated", + "description": "", + "deprecated": false, + "additionalProperties": true, + "type": [ + "object" + ] + }, + "type": [ + "array" + ] + }, + "subMessage": { + "title": "testgen.E2E.sub_message", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + }, + "subMessageDeprecated": { + "title": "testgen.E2E.sub_message_deprecated", + "description": "", + "deprecated": true, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + }, + "subMessageRepeated": { + "items": { + "title": "testgen.E2E.sub_message_repeated", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + }, + "type": [ + "array" + ] + }, + "timestamp": { + "title": "testgen.E2E.timestamp", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}%3A\\d{2}%3A\\d{2}(?:%2E\\d+)?[A-Z]?(?:[.-](?:08%3A\\d{2}|\\d{2}[A-Z]))?$" + }, + "timestampDeprecated": { + "title": "testgen.E2E.timestamp_deprecated", + "description": "", + "deprecated": true, + "type": [ + "string" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}%3A\\d{2}%3A\\d{2}(?:%2E\\d+)?[A-Z]?(?:[.-](?:08%3A\\d{2}|\\d{2}[A-Z]))?$" + }, + "timestampRepeated": { + "items": { + "title": "testgen.E2E.timestamp_repeated", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}%3A\\d{2}%3A\\d{2}(?:%2E\\d+)?[A-Z]?(?:[.-](?:08%3A\\d{2}|\\d{2}[A-Z]))?$" + }, + "type": [ + "array" + ] + }, + "uint32Field": { + "title": "testgen.E2E.uint32_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "uint32FieldDeprecated": { + "title": "testgen.E2E.uint32_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "uint32FieldRepeated": { + "items": { + "title": "testgen.E2E.uint32_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + }, + "type": [ + "array" + ] + }, + "uint64Field": { + "title": "testgen.E2E.uint64_field", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "uint64FieldDeprecated": { + "title": "testgen.E2E.uint64_field_deprecated", + "description": "", + "deprecated": true, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "uint64FieldRepeated": { + "items": { + "title": "testgen.E2E.uint64_field_repeated", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + }, + "type": [ + "array" + ] + }, + "value": { + "title": "testgen.E2E.value", + "description": "", + "deprecated": false + }, + "valueDeprecated": { + "title": "testgen.E2E.value_deprecated", + "description": "", + "deprecated": true + }, + "valueRepeated": { + "items": { + "title": "testgen.E2E.value_repeated", + "description": "", + "deprecated": false + }, + "type": [ + "array" + ] + } + }, + "oneOf": [ + { + "properties": { + "recursiveOneof": { + "title": "testgen.E2E.recursive_oneof", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E" + } + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "doubleFieldOneof": { + "title": "testgen.E2E.double_field_oneof", + "description": "// Some comment for double field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 1.7976931348623157e+308, + "minimum": 5e-324 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "floatFieldOneof": { + "title": "testgen.E2E.float_field_oneof", + "description": "// Some comment for float field\n", + "deprecated": false, + "type": [ + "number" + ], + "maximum": 3.4028234663852886e+38, + "minimum": 1.401298464324817e-45 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "int32FieldOneof": { + "title": "testgen.E2E.int32_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "int64FieldOneof": { + "title": "testgen.E2E.int64_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "uint32FieldOneof": { + "title": "testgen.E2E.uint32_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "uint64FieldOneof": { + "title": "testgen.E2E.uint64_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "sint32FieldOneof": { + "title": "testgen.E2E.sint32_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "sint64FieldOneof": { + "title": "testgen.E2E.sint64_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "fixed32FieldOneof": { + "title": "testgen.E2E.fixed32_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 4294967295, + "minimum": 0 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "fixed64FieldOneof": { + "title": "testgen.E2E.fixed64_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 18446744073709552000, + "minimum": 0 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "sfixed32FieldOneof": { + "title": "testgen.E2E.sfixed32_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 2147483647, + "minimum": -2147483648 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "sfixed64FieldOneof": { + "title": "testgen.E2E.sfixed64_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "integer" + ], + "maximum": 9223372036854776000, + "minimum": -9223372036854776000 + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "boolFieldOneof": { + "title": "testgen.E2E.bool_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "boolean" + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "stringFieldOneof": { + "title": "testgen.E2E.string_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "bytesFieldOneof": { + "title": "testgen.E2E.bytes_field_oneof", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "contentEncoding": "base64" + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "durationOneof": { + "title": "testgen.E2E.duration_oneof", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^-?([0-9]*[.])?[0-9]+(s)$" + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "timestampOneof": { + "title": "testgen.E2E.timestamp_oneof", + "description": "", + "deprecated": false, + "type": [ + "string" + ], + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}%3A\\d{2}%3A\\d{2}(?:%2E\\d+)?[A-Z]?(?:[.-](?:08%3A\\d{2}|\\d{2}[A-Z]))?$" + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "structOneof": { + "title": "testgen.E2E.struct_oneof", + "description": "", + "deprecated": false, + "additionalProperties": true, + "type": [ + "object" + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "valueOneof": { + "title": "testgen.E2E.value_oneof", + "description": "", + "deprecated": false + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "subMessageOneof": { + "title": "testgen.E2E.sub_message_oneof", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "enumOneof": { + "title": "testgen.E2E.enum_oneof", + "description": "", + "deprecated": false, + "oneOf": [ + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_UNSPECIFIED" + }, + { + "deprecated": true, + "type": [ + "string" + ], + "const": "ENUM_ONE" + }, + { + "deprecated": false, + "type": [ + "string" + ], + "const": "ENUM_TWO" + } + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "oneofBOption1": { + "title": "testgen.E2E.oneof_b_option_1", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "oneofBOption2": { + "title": "testgen.E2E.oneof_b_option_2", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + } + }, + "type": [ + "object" + ] + }, + { + "properties": { + "oneofBOption3": { + "title": "testgen.E2E.oneof_b_option_3", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + } + }, + "type": [ + "object" + ] + } + ], + "type": [ + "object" + ] + }, + "testgen.E2E.SubMessage": { + "title": "testgen.E2E.SubMessage", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "foo": { + "title": "testgen.E2E.SubMessage.foo", + "description": "", + "deprecated": false, + "type": [ + "string" + ] + }, + "recursive": { + "title": "testgen.E2E.SubMessage.recursive", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ] + }, + "recursiveE2e": { + "title": "testgen.E2E.SubMessage.recursive_e2e", + "description": "", + "deprecated": false, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E" + } + ] + } + }, + "type": [ + "object" + ] + } + }, + "title": "testgen", + "description": "Code generated by e2e.test. DO NOT EDIT. Schema definitions for testgen.", + "oneOf": [ + { + "title": "Configuration file format for: testgen.E2E.", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "@type": { + "type": [ + "string" + ], + "const": "type.googleapis.com/testgen.E2E" + } + }, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E" + } + ], + "type": [ + "object" + ], + "required": [ + "@type" + ] + }, + { + "title": "Configuration file format for: testgen.E2E.SubMessage.", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "@type": { + "type": [ + "string" + ], + "const": "type.googleapis.com/testgen.E2E.SubMessage" + } + }, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E.SubMessage" + } + ], + "type": [ + "object" + ], + "required": [ + "@type" + ] + }, + { + "title": "Configuration file format for: testgen.E2E.", + "description": "", + "deprecated": false, + "additionalProperties": false, + "properties": { + "@type": { + "type": [ + "string" + ], + "const": "type.googleapis.com/testgen.E2E" + } + }, + "allOf": [ + { + "$ref": "#/$defs/testgen.E2E" + } + ], + "type": [ + "object" + ], + "required": [ + "@type" + ] + } + ] +} diff --git a/internal/codegen/jsonschemagen/e2e/testdata_test.go b/internal/codegen/jsonschemagen/e2e/testdata_test.go new file mode 100644 index 0000000..bfe7d5e --- /dev/null +++ b/internal/codegen/jsonschemagen/e2e/testdata_test.go @@ -0,0 +1,8 @@ +package e2e_test + +import ( + "embed" +) + +//go:embed testdata/* +var testDataFS embed.FS diff --git a/internal/codegen/jsonschemagen/jsonschema.go b/internal/codegen/jsonschemagen/jsonschema.go new file mode 100644 index 0000000..bc023be --- /dev/null +++ b/internal/codegen/jsonschemagen/jsonschema.go @@ -0,0 +1,439 @@ +package jsonschemagen + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "sort" + + "github.com/marnixbouhuis/confpb/internal/codegen" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +// JSONSchemaCore is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/core.json +type JSONSchemaCore struct { + Schema *string `json:"$schema,omitempty"` + ID *string `json:"$id,omitempty"` + Ref *string `json:"$ref,omitempty"` + Anchor *string `json:"$anchor,omitempty"` + DynamicRef *string `json:"$dynamicRef,omitempty"` + DynamicAnchor *string `json:"$dynamicAnchor,omitempty"` + Vocabulary map[string]bool `json:"$vocabulary,omitempty"` + Comment *string `json:"$comment,omitempty"` + Defs map[string]*JSONSchema `json:"$defs,omitempty"` +} + +// JSONSchemaMetaData is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/meta-data.json +type JSONSchemaMetaData struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Default interface{} `json:"default,omitempty"` + Deprecated *bool `json:"deprecated,omitempty"` + ReadOnly *bool `json:"readOnly,omitempty"` + WriteOnly *bool `json:"writeOnly,omitempty"` + Examples []interface{} `json:"examples,omitempty"` +} + +// JSONSchemaApplicator is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/applicator.json +type JSONSchemaApplicator struct { + PrefixItems []*JSONSchema `json:"prefixItems,omitempty"` + Items *JSONSchema `json:"items,omitempty"` + Contains *JSONSchema `json:"contains,omitempty"` + AdditionalProperties interface{} `json:"additionalProperties,omitempty"` + Properties map[string]*JSONSchema `json:"properties,omitempty"` + PatternProperties map[string]*JSONSchema `json:"patternProperties,omitempty"` + DependentProperties map[string]*JSONSchema `json:"dependentProperties,omitempty"` + PropertyNames *JSONSchema `json:"propertyNames,omitempty"` + If *JSONSchema `json:"if,omitempty"` + Then *JSONSchema `json:"then,omitempty"` + Else *JSONSchema `json:"else,omitempty"` + AllOf []*JSONSchema `json:"allOf,omitempty"` + AnyOf []*JSONSchema `json:"anyOf,omitempty"` + OneOf []*JSONSchema `json:"oneOf,omitempty"` + Not *JSONSchema `json:"not,omitempty"` +} + +// JSONSchemaUnevaluated is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/unevaluated.json +type JSONSchemaUnevaluated struct { + UnevaluatedItems *JSONSchema `json:"unevaluatedItems,omitempty"` + UnevaluatedProperties *JSONSchema `json:"unevaluatedProperties,omitempty"` +} + +// JSONSchemaValidation is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/validation.json +type JSONSchemaValidation struct { + Type []string `json:"type,omitempty"` // This is technically a string OR an array, since we use this for code gen only it does not matter. + Const interface{} `json:"const,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + MultipleOf *uint64 `json:"multipleOf,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + MinLength *uint64 `json:"minLength,omitempty"` + Pattern *string `json:"pattern,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty"` + MinItems *uint64 `json:"minItems,omitempty"` + UniqueItems *bool `json:"uniqueItems,omitempty"` + MaxContains *uint64 `json:"maxContains,omitempty"` + MinContains *uint64 `json:"minContains,omitempty"` + MaxProperties *uint64 `json:"maxProperties,omitempty"` + MinProperties *uint64 `json:"minProperties,omitempty"` + Required []string `json:"required,omitempty"` + DependentRequired map[string][]string `json:"dependentRequired,omitempty"` +} + +// JSONSchemaFormatAnnotation is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/format-annotation.json +type JSONSchemaFormatAnnotation struct { + Format *string `json:"format,omitempty"` +} + +// JSONSchemaContent is a go struct that implements the structure defined in: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/meta/content.json +type JSONSchemaContent struct { + ContentEncoding *string `json:"contentEncoding,omitempty"` + ContentMediaType *string `json:"contentMediaType,omitempty"` + ContentSchema *JSONSchema `json:"contentSchema,omitempty"` +} + +// JSONSchema is a go struct that maps to a JSON schema document, defined in draft 2020-12: +// https://github.com/json-schema-org/json-schema-spec/blob/0d2e45422eda1dd5d3eb76905cb816b612d63a5b/schema.json +type JSONSchema struct { + *JSONSchemaCore + *JSONSchemaMetaData + *JSONSchemaApplicator + *JSONSchemaUnevaluated + *JSONSchemaValidation + *JSONSchemaFormatAnnotation + *JSONSchemaContent +} + +func ptr[T any](v T) *T { + return &v +} + +var _ codegen.FileGeneratorFunc = GenerateFile + +func GenerateFile(plugin *protogen.Plugin, file *protogen.File) error { + name, _ := os.Executable() // Ignore error, this is not critical + name = filepath.Base(name) + + root := &JSONSchema{ + JSONSchemaCore: &JSONSchemaCore{ + Schema: ptr("https://json-schema.org/draft/2020-12/schema"), + Defs: make(map[string]*JSONSchema), + }, + JSONSchemaMetaData: &JSONSchemaMetaData{ + Title: ptr(string(file.Desc.FullName())), + Description: ptr(fmt.Sprintf("Code generated by %s. DO NOT EDIT. Schema definitions for %s.", name, file.Desc.FullName())), + Deprecated: file.Proto.Options.Deprecated, + }, + JSONSchemaApplicator: &JSONSchemaApplicator{ + OneOf: make([]*JSONSchema, 0), + }, + } + + err := codegen.IterateMessages(file.Messages, func(message *protogen.Message) error { + return processMessage(root, message) + }) + if err != nil { + return fmt.Errorf("failed to generate JSON schema: %w", err) + } + + b, err := json.MarshalIndent(root, "", " ") + if err != nil { + return fmt.Errorf("failed to marshall JSON schema: %w", err) + } + + fileName := file.GeneratedFilenamePrefix + ".schema.json" + g := plugin.NewGeneratedFile(fileName, file.GoImportPath) + g.P(string(b)) + + return nil +} + +func fieldToSchema(root *JSONSchema, field *protogen.Field) (*JSONSchema, error) { + var isFieldDeprecated bool + if opts, isOpts := field.Desc.Options().(*descriptorpb.FieldOptions); isOpts { + isFieldDeprecated = opts.GetDeprecated() + } + + schema := &JSONSchema{ + JSONSchemaMetaData: &JSONSchemaMetaData{ + Title: ptr(string(field.Desc.FullName())), + Description: ptr(field.Comments.Leading.String()), + Deprecated: &isFieldDeprecated, + }, + JSONSchemaValidation: &JSONSchemaValidation{}, + JSONSchemaApplicator: &JSONSchemaApplicator{}, + JSONSchemaContent: &JSONSchemaContent{}, + } + + // Handle map fields + if field.Desc.IsMap() { + keyType := field.Message.Fields[0] + valueType := field.Message.Fields[1] + + // Create schema for map values + valueSchema, err := fieldToSchema(root, valueType) + if err != nil { + return nil, fmt.Errorf("failed to create schema for map value type: %w", err) + } + + // Maps are represented as objects in JSON + schema.Type = []string{"object"} + schema.AdditionalProperties = valueSchema + + // If the key type isn't string we need to add a pattern to validate the property names. + // Possible key types according to https://protobuf.dev/reference/protobuf/proto3-spec/#map_field are: + // keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" | + // "fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string" + //nolint:exhaustive // not all kinds can be used as map keys + switch keyType.Desc.Kind() { + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + schema.PropertyNames = &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Pattern: ptr(`^-?([0-9]|[1-9][0-9]{1,9})$`), // -2147483648 to 2147483647 + }, + } + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + schema.PropertyNames = &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Pattern: ptr(`^([0-9]|[1-9][0-9]{1,9})$`), // 0 to 4294967295 + }, + } + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + schema.PropertyNames = &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Pattern: ptr(`^-?([0-9]|[1-9][0-9]{1,19})$`), // -9223372036854775808 to 9223372036854775807 + }, + } + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + schema.PropertyNames = &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Pattern: ptr(`^([0-9]|[1-9][0-9]{1,19})$`), // 0 to 18446744073709551615 + }, + } + case protoreflect.BoolKind: + schema.PropertyNames = &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Pattern: ptr(`^(true|false)$`), + }, + } + } + + return schema, nil + } + + switch field.Desc.Kind() { + case protoreflect.BoolKind: + schema.Type = []string{"boolean"} + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + schema.Type = []string{"integer"} + schema.Minimum = ptr(float64(math.MinInt32)) + schema.Maximum = ptr(float64(math.MaxInt32)) + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + schema.Type = []string{"integer"} + schema.Minimum = ptr(float64(0)) + schema.Maximum = ptr(float64(math.MaxUint32)) + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + schema.Type = []string{"integer"} + schema.Minimum = ptr(float64(math.MinInt64)) + schema.Maximum = ptr(float64(math.MaxInt64)) + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + schema.Type = []string{"integer"} + schema.Minimum = ptr(float64(0)) + schema.Maximum = ptr(float64(math.MaxUint64)) + case protoreflect.FloatKind: + schema.Type = []string{"number"} + schema.Minimum = ptr(math.SmallestNonzeroFloat32) + schema.Maximum = ptr(math.MaxFloat32) + case protoreflect.DoubleKind: + schema.Type = []string{"number"} + schema.Minimum = ptr(math.SmallestNonzeroFloat64) + schema.Maximum = ptr(math.MaxFloat64) + case protoreflect.StringKind: + schema.Type = []string{"string"} + case protoreflect.BytesKind: + schema.Type = []string{"string"} + schema.ContentEncoding = ptr("base64") + case protoreflect.EnumKind: + for _, val := range field.Enum.Values { + var isValueDeprecated bool + if opts, isOpts := val.Desc.Options().(*descriptorpb.EnumValueOptions); isOpts { + isValueDeprecated = opts.GetDeprecated() + } + + schema.OneOf = append(schema.OneOf, &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"string"}, + Const: string(val.Desc.Name()), + }, + JSONSchemaMetaData: &JSONSchemaMetaData{ + Deprecated: &isValueDeprecated, + }, + }) + } + case protoreflect.MessageKind: + switch field.Message.Desc.FullName() { + case "google.protobuf.Timestamp": + schema.Type = []string{"string"} + // Match RFC3339 timestamp + schema.Pattern = ptr(`^\d{4}-\d{2}-\d{2}T\d{2}%3A\d{2}%3A\d{2}(?:%2E\d+)?[A-Z]?(?:[.-](?:08%3A\d{2}|\d{2}[A-Z]))?$`) + case "google.protobuf.Duration": + schema.Type = []string{"string"} + schema.Pattern = ptr("^-?([0-9]*[.])?[0-9]+(s)$") // Allow values like "3s", "-3s", "3.000000001s", "3.000001s" + case "google.protobuf.Struct": + schema.Type = []string{"object"} + schema.AdditionalProperties = true + case "google.protobuf.Value": + // Match any value, specify no type + schema.Type = nil + default: + // Regular message, make sure it's present in the Defs of the root schema + messageName := string(field.Message.Desc.FullName()) + if _, hasDef := root.Defs[messageName]; !hasDef { + if err := processMessage(root, field.Message); err != nil { + return nil, fmt.Errorf("failed to generate schema for referenced message: %w", err) + } + } + schema.AllOf = []*JSONSchema{{ + JSONSchemaCore: &JSONSchemaCore{ + Ref: ptr("#/$defs/" + messageName), + }, + }} + } + case protoreflect.GroupKind: + // No support needed since we the minimum proto version that we support is proto3. + return nil, errors.New("groups are not supported") + } + + if field.Desc.IsList() { + return &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"array"}, + }, + JSONSchemaApplicator: &JSONSchemaApplicator{ + Items: schema, + }, + }, nil + } + + return schema, nil +} + +func processMessage(root *JSONSchema, message *protogen.Message) error { + name := string(message.Desc.FullName()) + + var isMessageDeprecated bool + if opts, isOpts := message.Desc.Options().(*descriptorpb.MessageOptions); isOpts { + isMessageDeprecated = opts.GetDeprecated() + } + + schema := &JSONSchema{ + JSONSchemaMetaData: &JSONSchemaMetaData{ + Title: ptr(string(message.Desc.FullName())), + Description: ptr(message.Comments.Leading.String()), + Deprecated: &isMessageDeprecated, + }, + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"object"}, + }, + JSONSchemaApplicator: &JSONSchemaApplicator{ + Properties: make(map[string]*JSONSchema), + AdditionalProperties: false, + }, + } + + // Before processing any fields, store a reference of the schema in the defs, otherwise we get into infinite loops + // when processing circular dependent messages + root.Defs[name] = schema + + oneofFieldGroups := make(map[string][]*JSONSchema) + for _, field := range message.Fields { + if field.Desc.IsWeak() { + return fmt.Errorf("field \"%s\" is invalid, weak fields are not supported", field.Desc.FullName()) + } + + fieldSchema, err := fieldToSchema(root, field) + if err != nil { + return fmt.Errorf("failed to convert field \"%s\" to schema: %w", field.Desc.FullName(), err) + } + + if field.Oneof == nil { + // Not part of oneof group, add it to the schema properties directly + schema.Properties[field.Desc.JSONName()] = fieldSchema + continue + } + + // Field is part of oneof group + oneof, hasOneof := oneofFieldGroups[field.Oneof.GoName] + if !hasOneof { + oneof = make([]*JSONSchema, 0, 1) + } + oneof = append(oneof, &JSONSchema{ + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"object"}, + }, + JSONSchemaApplicator: &JSONSchemaApplicator{ + Properties: map[string]*JSONSchema{ + field.Desc.JSONName(): fieldSchema, + }, + }, + }) + oneofFieldGroups[field.Oneof.GoName] = oneof + } + + // Add oneof groups to schema in sorted order, this way we ensure we have a stable JSON output + oneofNames := make([]string, 0, len(oneofFieldGroups)) + for name := range oneofFieldGroups { + oneofNames = append(oneofNames, name) + } + sort.Strings(oneofNames) + + for _, oneofName := range oneofNames { + schema.OneOf = append(schema.OneOf, oneofFieldGroups[oneofName]...) + } + + // Register configuration file format for message in root schema. + root.OneOf = append(root.OneOf, &JSONSchema{ + JSONSchemaMetaData: &JSONSchemaMetaData{ + Title: ptr(fmt.Sprintf("Configuration file format for: %s.", name)), + Description: ptr(message.Comments.Leading.String()), + Deprecated: &isMessageDeprecated, + }, + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"object"}, + Required: []string{"@type"}, + }, + JSONSchemaApplicator: &JSONSchemaApplicator{ + Properties: map[string]*JSONSchema{ + "@type": { + JSONSchemaValidation: &JSONSchemaValidation{ + Type: []string{"string"}, + Const: "type.googleapis.com/" + name, + }, + }, + }, + AdditionalProperties: false, + AllOf: []*JSONSchema{{ + JSONSchemaCore: &JSONSchemaCore{ + Ref: ptr("#/$defs/" + name), + }, + }}, + }, + }) + + return nil +} diff --git a/internal/codegen/testutil/runner.go b/internal/codegen/testutil/runner.go index 2dbc54b..41135fe 100644 --- a/internal/codegen/testutil/runner.go +++ b/internal/codegen/testutil/runner.go @@ -65,6 +65,26 @@ type e2eTestInfo struct { Output string } +// GetFileFromGenerationResult returns the contents of an output file from a GenerationResult. +func GetFileFromGenerationResult(t *testing.T, res *GenerationResult, path string) string { + t.Helper() + + for _, file := range res.Resp.File { + if file.GetName() == path { + return file.GetContent() + } + } + + t.Log("File not found:", path) + t.Log("Available files:") + for _, file := range res.Resp.File { + t.Log("-", file.GetName()) + } + t.Fail() + + return "" +} + func RunTestInE2ERunner(t *testing.T, res *GenerationResult, testCode string) { t.Helper()