From a54678fb6b0de08ff21d9fabb128e65c1f76ba1f Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Mon, 5 Dec 2022 11:11:06 -0500 Subject: [PATCH] Add logic to generate openapi response structures (#18192) --- changelog/18192.txt | 3 ++ sdk/framework/openapi.go | 56 +++++++++++++++++++++++---- sdk/framework/openapi_test.go | 10 +++++ sdk/framework/path.go | 7 ++-- sdk/framework/testdata/responses.json | 19 ++++++--- sdk/go.mod | 2 +- 6 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 changelog/18192.txt diff --git a/changelog/18192.txt b/changelog/18192.txt new file mode 100644 index 000000000..56e377e47 --- /dev/null +++ b/changelog/18192.txt @@ -0,0 +1,3 @@ +```release-note:improvement +openapi: Add logic to generate openapi response structures +``` diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go index 9851948eb..59515e335 100644 --- a/sdk/framework/openapi.go +++ b/sdk/framework/openapi.go @@ -13,6 +13,8 @@ import ( "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/version" "github.com/mitchellh/mapstructure" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // OpenAPI specification (OAS): https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md @@ -389,7 +391,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st // Set the final request body. Only JSON request data is supported. if len(s.Properties) > 0 || s.Example != nil { - requestName := constructRequestName(requestResponsePrefix, path) + requestName := constructRequestResponseName(path, requestResponsePrefix, "Request") doc.Components.Schemas[requestName] = s op.RequestBody = &OASRequestBody{ Required: true, @@ -469,6 +471,41 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st } } } + + responseSchema := &OASSchema{ + Type: "object", + Properties: make(map[string]*OASSchema), + } + + for name, field := range resp.Fields { + openapiField := convertType(field.Type) + p := OASSchema{ + Type: openapiField.baseType, + Description: cleanString(field.Description), + Format: openapiField.format, + Pattern: openapiField.pattern, + Enum: field.AllowedValues, + Default: field.Default, + Deprecated: field.Deprecated, + DisplayAttrs: field.DisplayAttrs, + } + if openapiField.baseType == "array" { + p.Items = &OASSchema{ + Type: openapiField.items, + } + } + responseSchema.Properties[name] = &p + } + + if len(resp.Fields) != 0 { + responseName := constructRequestResponseName(path, requestResponsePrefix, "Response") + doc.Components.Schemas[responseName] = responseSchema + content = OASContent{ + "application/json": &OASMediaTypeObject{ + Schema: &OASSchema{Ref: fmt.Sprintf("#/components/schemas/%s", responseName)}, + }, + } + } } op.Responses[code] = &OASResponse{ @@ -493,14 +530,17 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st return nil } -// constructRequestName joins the given prefix with the path elements into a -// CamelCaseRequest string. +// constructRequestResponseName joins the given path with prefix & suffix into +// a CamelCase request or response name. // -// For example, prefix="kv" & path=/config/lease/{name} => KvConfigLeaseRequest -func constructRequestName(requestResponsePrefix string, path string) string { +// For example, path=/config/lease/{name}, prefix="secret", suffix="request" +// will result in "SecretConfigLeaseRequest" +func constructRequestResponseName(path, prefix, suffix string) string { var b strings.Builder - b.WriteString(strings.Title(requestResponsePrefix)) + title := cases.Title(language.English) + + b.WriteString(title.String(prefix)) // split the path by / _ - separators for _, token := range strings.FieldsFunc(path, func(r rune) bool { @@ -508,11 +548,11 @@ func constructRequestName(requestResponsePrefix string, path string) string { }) { // exclude request fields if !strings.ContainsAny(token, "{}") { - b.WriteString(strings.Title(token)) + b.WriteString(title.String(token)) } } - b.WriteString("Request") + b.WriteString(suffix) return b.String() } diff --git a/sdk/framework/openapi_test.go b/sdk/framework/openapi_test.go index 8d3ecfea0..a21e139c1 100644 --- a/sdk/framework/openapi_test.go +++ b/sdk/framework/openapi_test.go @@ -477,6 +477,16 @@ func TestOpenAPI_Paths(t *testing.T) { "amount": 42, }, }, + Fields: map[string]*FieldSchema{ + "field_a": { + Type: TypeString, + Description: "field_a description", + }, + "field_b": { + Type: TypeBool, + Description: "field_b description", + }, + }, }}, }, }, diff --git a/sdk/framework/path.go b/sdk/framework/path.go index 8a8b1c758..16dec52e1 100644 --- a/sdk/framework/path.go +++ b/sdk/framework/path.go @@ -229,9 +229,10 @@ type RequestExample struct { // Response describes and optional demonstrations an operation response. type Response struct { - Description string // summary of the the response and should always be provided - MediaType string // media type of the response, defaulting to "application/json" if empty - Example *logical.Response // example response data + Description string // summary of the the response and should always be provided + MediaType string // media type of the response, defaulting to "application/json" if empty + Fields map[string]*FieldSchema // the fields present in this response, used to generate openapi response + Example *logical.Response // example response data } // PathOperation is a concrete implementation of OperationHandler. diff --git a/sdk/framework/testdata/responses.json b/sdk/framework/testdata/responses.json index b0e197bab..4e442cfb4 100644 --- a/sdk/framework/testdata/responses.json +++ b/sdk/framework/testdata/responses.json @@ -34,11 +34,7 @@ "content": { "application/json": { "schema": { - "example": { - "data": { - "amount": 42 - } - } + "$ref": "#/components/schemas/KvFooResponse" } } } @@ -49,6 +45,19 @@ }, "components": { "schemas": { + "KvFooResponse": { + "type": "object", + "properties": { + "field_a": { + "type": "string", + "description": "field_a description" + }, + "field_b": { + "type": "boolean", + "description": "field_b description" + } + } + } } } } diff --git a/sdk/go.mod b/sdk/go.mod index eb27efc1a..002ec3f4f 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -37,6 +37,7 @@ require ( github.com/stretchr/testify v1.7.0 go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 + golang.org/x/text v0.3.3 google.golang.org/grpc v1.41.0 google.golang.org/protobuf v1.26.0 ) @@ -59,7 +60,6 @@ require ( golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect - golang.org/x/text v0.3.3 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect )