Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic to generate openapi response structures #18192

Merged
merged 4 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/18192.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
openapi: Add logic to generate openapi response structures
```
56 changes: 48 additions & 8 deletions sdk/framework/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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{
Expand All @@ -493,26 +530,29 @@ 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 {
return r == '/' || r == '_' || r == '-'
}) {
// 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()
}
Expand Down
10 changes: 10 additions & 0 deletions sdk/framework/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
}},
},
},
Expand Down
7 changes: 4 additions & 3 deletions sdk/framework/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 14 additions & 5 deletions sdk/framework/testdata/responses.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@
"content": {
"application/json": {
"schema": {
"example": {
"data": {
"amount": 42
}
}
"$ref": "#/components/schemas/KvFooResponse"
}
}
}
Expand All @@ -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"
}
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)