Add logic to generate openapi response structures (#18192)
This commit is contained in:
parent
398cf38e1e
commit
a54678fb6b
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
openapi: Add logic to generate openapi response structures
|
||||||
|
```
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue