From ee84fd2a96205f519ad7b86d989673d2ada03a3b Mon Sep 17 00:00:00 2001 From: Ingo Oeser Date: Fri, 25 Aug 2023 22:06:11 +0200 Subject: [PATCH] fix: Stop lying how GRPC renders durations (#401) by reporting the data types and formats that are actually used Fixes: #351 --- .../tests/protobuftypes/message.proto | 2 + .../examples/tests/protobuftypes/openapi.yaml | 15 +++++++ .../openapi_default_response.yaml | 15 +++++++ .../openapi_fq_schema_naming.yaml | 15 +++++++ .../tests/protobuftypes/openapi_json.yaml | 15 +++++++ .../protobuftypes/openapi_string_enum.yaml | 15 +++++++ cmd/protoc-gen-openapi/generator/generator.go | 15 +++++++ cmd/protoc-gen-openapi/generator/reflector.go | 3 ++ .../generator/wellknown/schemas.go | 44 +++++++++++++++++++ 9 files changed, 139 insertions(+) diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto index 6dba0b5..51160a1 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto @@ -24,6 +24,7 @@ import "google/protobuf/struct.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/protobuftypes/message/v1;message"; @@ -113,4 +114,5 @@ message Message { google.protobuf.FloatValue float_value_type = 22; google.protobuf.DoubleValue double_value_type = 23; google.protobuf.Timestamp timestamp_type = 24; + google.protobuf.Duration duration_type = 25; } diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml index 624104e..6cb6b7f 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml @@ -144,6 +144,12 @@ paths: schema: type: string format: date-time + - name: duration_type + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s responses: "200": description: OK @@ -303,6 +309,12 @@ paths: schema: type: string format: date-time + - name: duration_type + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s requestBody: content: application/json: @@ -444,6 +456,9 @@ components: timestamp_type: type: string format: date-time + duration_type: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string Message_EmbMessage: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_default_response.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_default_response.yaml index f859414..1158893 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_default_response.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_default_response.yaml @@ -144,6 +144,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s responses: "200": description: OK @@ -303,6 +309,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s requestBody: content: application/json: @@ -444,6 +456,9 @@ components: timestampType: type: string format: date-time + durationType: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string Message_EmbMessage: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_fq_schema_naming.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_fq_schema_naming.yaml index 1a67470..bd27631 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_fq_schema_naming.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_fq_schema_naming.yaml @@ -144,6 +144,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s responses: "200": description: OK @@ -303,6 +309,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s requestBody: content: application/json: @@ -460,6 +472,9 @@ components: timestampType: type: string format: date-time + durationType: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string tests.protobuftypes.message.v1.Message_EmbMessage: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml index c23150a..ae946c5 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml @@ -144,6 +144,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s responses: "200": description: OK @@ -303,6 +309,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s requestBody: content: application/json: @@ -444,6 +456,9 @@ components: timestampType: type: string format: date-time + durationType: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string Message_EmbMessage: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_string_enum.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_string_enum.yaml index f859414..1158893 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_string_enum.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_string_enum.yaml @@ -144,6 +144,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s responses: "200": description: OK @@ -303,6 +309,12 @@ paths: schema: type: string format: date-time + - name: durationType + in: query + schema: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string + description: Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s requestBody: content: application/json: @@ -444,6 +456,9 @@ components: timestampType: type: string format: date-time + durationType: + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + type: string Message_EmbMessage: type: object properties: diff --git a/cmd/protoc-gen-openapi/generator/generator.go b/cmd/protoc-gen-openapi/generator/generator.go index 5f10c13..e548ab2 100644 --- a/cmd/protoc-gen-openapi/generator/generator.go +++ b/cmd/protoc-gen-openapi/generator/generator.go @@ -351,6 +351,21 @@ func (g *OpenAPIv3Generator) _buildQueryParamsV3(field *protogen.Field, depths m }, }) return parameters + case ".google.protobuf.Duration": + fieldSchema := g.reflect.schemaOrReferenceForMessage(field.Message.Desc) + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: queryFieldName, + In: "query", + Description: fieldDescription, + Required: false, + Schema: fieldSchema, + }, + }, + }) + return parameters } if field.Desc.IsList() { diff --git a/cmd/protoc-gen-openapi/generator/reflector.go b/cmd/protoc-gen-openapi/generator/reflector.go index 97754f4..31a0f93 100644 --- a/cmd/protoc-gen-openapi/generator/reflector.go +++ b/cmd/protoc-gen-openapi/generator/reflector.go @@ -135,6 +135,9 @@ func (r *OpenAPIv3Reflector) schemaOrReferenceForMessage(message protoreflect.Me case ".google.protobuf.Timestamp": return wk.NewGoogleProtobufTimestampSchema() + case ".google.protobuf.Duration": + return wk.NewGoogleProtobufDurationSchema() + case ".google.type.Date": return wk.NewGoogleTypeDateSchema() diff --git a/cmd/protoc-gen-openapi/generator/wellknown/schemas.go b/cmd/protoc-gen-openapi/generator/wellknown/schemas.go index 6ab93e2..8840dde 100644 --- a/cmd/protoc-gen-openapi/generator/wellknown/schemas.go +++ b/cmd/protoc-gen-openapi/generator/wellknown/schemas.go @@ -94,6 +94,50 @@ func NewGoogleProtobufTimestampSchema() *v3.SchemaOrReference { Schema: &v3.Schema{Type: "string", Format: "date-time"}}} } +// google.protobuf.Duration is serialized as a string +func NewGoogleProtobufDurationSchema() *v3.SchemaOrReference { + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + // From: https://github.com/protocolbuffers/protobuf/blob/ece5ef6b9b6fa66ef4638335612284379ee4548f/src/google/protobuf/duration.proto + // In JSON format, the Duration type is encoded as a string rather than an + // object, where the string ends in the suffix "s" (indicating seconds) and + // is preceded by the number of seconds, with nanoseconds expressed as + // fractional seconds. For example, 3 seconds with 0 nanoseconds should be + // encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should + // be expressed in JSON format as "3.000000001s", and 3 seconds and 1 + // microsecond should be expressed in JSON format as "3.000001s". + // + // The fields of message google.protobuf.Duration are further described as: + // "int64 seconds" + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + // `int32 nanos` + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + // + // This leads to the regex below limiting range from -315.576,000,000s to 315,576,000,000s + // allowing -0.999,999,999s to 0.999,999,999s in the floating precision range. + // That full range cannot be expressed precisly in float64 as demonstrated in + // the example at https://go.dev/play/p/XNtuhwdyu8Y for your reference. + // So the well known type google.protobuf.Duration needs a string. + // + // Please note that JSON schemas duration format is NOT the same, as that uses + // a different syntax starting with "P", supports daylight saving times and other + // different features, so it is NOT compatible. + Schema: &v3.Schema{ + Type: "string", + Pattern: `^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$`, + Description: "Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s", + }, + }, + } +} + // google.type.Date is serialized as a string func NewGoogleTypeDateSchema() *v3.SchemaOrReference { return &v3.SchemaOrReference{