Add logic to generate openapi response structures (#18192)

This commit is contained in:
Anton Averchenkov 2022-12-05 11:11:06 -05:00 committed by GitHub
parent 398cf38e1e
commit a54678fb6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 17 deletions

3
changelog/18192.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
openapi: Add logic to generate openapi response structures
```

View File

@ -13,6 +13,8 @@ import (
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/version" "github.com/hashicorp/vault/sdk/version"
"github.com/mitchellh/mapstructure" "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 // 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. // Set the final request body. Only JSON request data is supported.
if len(s.Properties) > 0 || s.Example != nil { if len(s.Properties) > 0 || s.Example != nil {
requestName := constructRequestName(requestResponsePrefix, path) requestName := constructRequestResponseName(path, requestResponsePrefix, "Request")
doc.Components.Schemas[requestName] = s doc.Components.Schemas[requestName] = s
op.RequestBody = &OASRequestBody{ op.RequestBody = &OASRequestBody{
Required: true, 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{ op.Responses[code] = &OASResponse{
@ -493,14 +530,17 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
return nil return nil
} }
// constructRequestName joins the given prefix with the path elements into a // constructRequestResponseName joins the given path with prefix & suffix into
// CamelCaseRequest string. // a CamelCase request or response name.
// //
// For example, prefix="kv" & path=/config/lease/{name} => KvConfigLeaseRequest // For example, path=/config/lease/{name}, prefix="secret", suffix="request"
func constructRequestName(requestResponsePrefix string, path string) string { // will result in "SecretConfigLeaseRequest"
func constructRequestResponseName(path, prefix, suffix string) string {
var b strings.Builder var b strings.Builder
b.WriteString(strings.Title(requestResponsePrefix)) title := cases.Title(language.English)
b.WriteString(title.String(prefix))
// split the path by / _ - separators // split the path by / _ - separators
for _, token := range strings.FieldsFunc(path, func(r rune) bool { for _, token := range strings.FieldsFunc(path, func(r rune) bool {
@ -508,11 +548,11 @@ func constructRequestName(requestResponsePrefix string, path string) string {
}) { }) {
// exclude request fields // exclude request fields
if !strings.ContainsAny(token, "{}") { if !strings.ContainsAny(token, "{}") {
b.WriteString(strings.Title(token)) b.WriteString(title.String(token))
} }
} }
b.WriteString("Request") b.WriteString(suffix)
return b.String() return b.String()
} }

View File

@ -477,6 +477,16 @@ func TestOpenAPI_Paths(t *testing.T) {
"amount": 42, "amount": 42,
}, },
}, },
Fields: map[string]*FieldSchema{
"field_a": {
Type: TypeString,
Description: "field_a description",
},
"field_b": {
Type: TypeBool,
Description: "field_b description",
},
},
}}, }},
}, },
}, },

View File

@ -229,9 +229,10 @@ type RequestExample struct {
// Response describes and optional demonstrations an operation response. // Response describes and optional demonstrations an operation response.
type Response struct { type Response struct {
Description string // summary of the the response and should always be provided 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 MediaType string // media type of the response, defaulting to "application/json" if empty
Example *logical.Response // example response data 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. // PathOperation is a concrete implementation of OperationHandler.

View File

@ -34,11 +34,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"example": { "$ref": "#/components/schemas/KvFooResponse"
"data": {
"amount": 42
}
}
} }
} }
} }
@ -49,6 +45,19 @@
}, },
"components": { "components": {
"schemas": { "schemas": {
"KvFooResponse": {
"type": "object",
"properties": {
"field_a": {
"type": "string",
"description": "field_a description"
},
"field_b": {
"type": "boolean",
"description": "field_b description"
}
}
}
} }
} }
} }

View File

@ -37,6 +37,7 @@ require (
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
go.uber.org/atomic v1.9.0 go.uber.org/atomic v1.9.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 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/grpc v1.41.0
google.golang.org/protobuf v1.26.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/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // 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 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
) )