openapi: Improve operationId/request/response naming strategy (#19319)
This commit is contained in:
parent
28e68ae86d
commit
4564a3534b
3
changelog/19319.txt
Normal file
3
changelog/19319.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
openapi: Improve operationId/request/response naming strategy
|
||||||
|
```
|
|
@ -244,7 +244,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range paths {
|
for pathIndex, path := range paths {
|
||||||
// Construct a top level PathItem which will be populated as the path is processed.
|
// Construct a top level PathItem which will be populated as the path is processed.
|
||||||
pi := OASPathItem{
|
pi := OASPathItem{
|
||||||
Description: cleanString(p.HelpSynopsis),
|
Description: cleanString(p.HelpSynopsis),
|
||||||
|
@ -252,7 +252,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
|
|
||||||
pi.Sudo = specialPathMatch(path, sudoPaths)
|
pi.Sudo = specialPathMatch(path, sudoPaths)
|
||||||
pi.Unauthenticated = specialPathMatch(path, unauthPaths)
|
pi.Unauthenticated = specialPathMatch(path, unauthPaths)
|
||||||
pi.DisplayAttrs = p.DisplayAttrs
|
pi.DisplayAttrs = withoutOperationHints(p.DisplayAttrs)
|
||||||
|
|
||||||
// If the newer style Operations map isn't defined, create one from the legacy fields.
|
// If the newer style Operations map isn't defined, create one from the legacy fields.
|
||||||
operations := p.Operations
|
operations := p.Operations
|
||||||
|
@ -294,7 +294,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
Pattern: t.pattern,
|
Pattern: t.pattern,
|
||||||
Enum: field.AllowedValues,
|
Enum: field.AllowedValues,
|
||||||
Default: field.Default,
|
Default: field.Default,
|
||||||
DisplayAttrs: field.DisplayAttrs,
|
DisplayAttrs: withoutOperationHints(field.DisplayAttrs),
|
||||||
},
|
},
|
||||||
Required: required,
|
Required: required,
|
||||||
Deprecated: field.Deprecated,
|
Deprecated: field.Deprecated,
|
||||||
|
@ -331,9 +331,19 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
|
|
||||||
op := NewOASOperation()
|
op := NewOASOperation()
|
||||||
|
|
||||||
|
operationID := constructOperationID(
|
||||||
|
path,
|
||||||
|
pathIndex,
|
||||||
|
p.DisplayAttrs,
|
||||||
|
opType,
|
||||||
|
props.DisplayAttrs,
|
||||||
|
requestResponsePrefix,
|
||||||
|
)
|
||||||
|
|
||||||
op.Summary = props.Summary
|
op.Summary = props.Summary
|
||||||
op.Description = props.Description
|
op.Description = props.Description
|
||||||
op.Deprecated = props.Deprecated
|
op.Deprecated = props.Deprecated
|
||||||
|
op.OperationID = operationID
|
||||||
|
|
||||||
// Add any fields not present in the path as body parameters for POST.
|
// Add any fields not present in the path as body parameters for POST.
|
||||||
if opType == logical.CreateOperation || opType == logical.UpdateOperation {
|
if opType == logical.CreateOperation || opType == logical.UpdateOperation {
|
||||||
|
@ -363,7 +373,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
Enum: field.AllowedValues,
|
Enum: field.AllowedValues,
|
||||||
Default: field.Default,
|
Default: field.Default,
|
||||||
Deprecated: field.Deprecated,
|
Deprecated: field.Deprecated,
|
||||||
DisplayAttrs: field.DisplayAttrs,
|
DisplayAttrs: withoutOperationHints(field.DisplayAttrs),
|
||||||
}
|
}
|
||||||
if openapiField.baseType == "array" {
|
if openapiField.baseType == "array" {
|
||||||
p.Items = &OASSchema{
|
p.Items = &OASSchema{
|
||||||
|
@ -381,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 := constructRequestResponseName(path, requestResponsePrefix, "Request")
|
requestName := hyphenatedToTitleCase(operationID) + "Request"
|
||||||
doc.Components.Schemas[requestName] = s
|
doc.Components.Schemas[requestName] = s
|
||||||
op.RequestBody = &OASRequestBody{
|
op.RequestBody = &OASRequestBody{
|
||||||
Required: true,
|
Required: true,
|
||||||
|
@ -477,7 +487,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
Enum: field.AllowedValues,
|
Enum: field.AllowedValues,
|
||||||
Default: field.Default,
|
Default: field.Default,
|
||||||
Deprecated: field.Deprecated,
|
Deprecated: field.Deprecated,
|
||||||
DisplayAttrs: field.DisplayAttrs,
|
DisplayAttrs: withoutOperationHints(field.DisplayAttrs),
|
||||||
}
|
}
|
||||||
if openapiField.baseType == "array" {
|
if openapiField.baseType == "array" {
|
||||||
p.Items = &OASSchema{
|
p.Items = &OASSchema{
|
||||||
|
@ -488,7 +498,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resp.Fields) != 0 {
|
if len(resp.Fields) != 0 {
|
||||||
responseName := constructRequestResponseName(path, requestResponsePrefix, "Response")
|
responseName := hyphenatedToTitleCase(operationID) + "Response"
|
||||||
doc.Components.Schemas[responseName] = responseSchema
|
doc.Components.Schemas[responseName] = responseSchema
|
||||||
content = OASContent{
|
content = OASContent{
|
||||||
"application/json": &OASMediaTypeObject{
|
"application/json": &OASMediaTypeObject{
|
||||||
|
@ -520,33 +530,6 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// constructRequestResponseName joins the given path with prefix & suffix into
|
|
||||||
// a CamelCase request or response name.
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
|
|
||||||
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(title.String(token))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString(suffix)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// specialPathMatch checks whether the given path matches one of the special
|
// specialPathMatch checks whether the given path matches one of the special
|
||||||
// paths, taking into account * and + wildcards (e.g. foo/+/bar/*)
|
// paths, taking into account * and + wildcards (e.g. foo/+/bar/*)
|
||||||
func specialPathMatch(path string, specialPaths []string) bool {
|
func specialPathMatch(path string, specialPaths []string) bool {
|
||||||
|
@ -599,6 +582,117 @@ func specialPathMatch(path string, specialPaths []string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// constructOperationID joins the given inputs into a hyphen-separated
|
||||||
|
// lower-case operation id, which is also used as a prefix for request and
|
||||||
|
// response names.
|
||||||
|
//
|
||||||
|
// The OperationPrefix / -Verb / -Suffix found in display attributes will be
|
||||||
|
// used, if provided. Otherwise, the function falls back to using the path and
|
||||||
|
// the operation.
|
||||||
|
//
|
||||||
|
// Examples of generated operation identifiers:
|
||||||
|
// - kvv2-write
|
||||||
|
// - kvv2-read
|
||||||
|
// - google-cloud-login
|
||||||
|
// - google-cloud-write-role
|
||||||
|
func constructOperationID(
|
||||||
|
path string,
|
||||||
|
pathIndex int,
|
||||||
|
pathAttributes *DisplayAttributes,
|
||||||
|
operation logical.Operation,
|
||||||
|
operationAttributes *DisplayAttributes,
|
||||||
|
defaultPrefix string,
|
||||||
|
) string {
|
||||||
|
var (
|
||||||
|
prefix string
|
||||||
|
verb string
|
||||||
|
suffix string
|
||||||
|
)
|
||||||
|
|
||||||
|
if operationAttributes != nil {
|
||||||
|
prefix = operationAttributes.OperationPrefix
|
||||||
|
verb = operationAttributes.OperationVerb
|
||||||
|
suffix = operationAttributes.OperationSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathAttributes != nil {
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = pathAttributes.OperationPrefix
|
||||||
|
}
|
||||||
|
if verb == "" {
|
||||||
|
verb = pathAttributes.OperationVerb
|
||||||
|
}
|
||||||
|
if suffix == "" {
|
||||||
|
suffix = pathAttributes.OperationSuffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single suffix string can contain multiple pipe-delimited strings. To
|
||||||
|
// determine the actual suffix, we attempt to match it by the index of the
|
||||||
|
// paths returned from `expandPattern(...)`. For example:
|
||||||
|
//
|
||||||
|
// pki/
|
||||||
|
// Pattern: "keys/generate/(internal|exported|kms)",
|
||||||
|
// DisplayAttrs: {
|
||||||
|
// ...
|
||||||
|
// OperationSuffix: "internal-key|exported-key|kms-key",
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// will expand into three paths and corresponding suffixes:
|
||||||
|
//
|
||||||
|
// path 0: "keys/generate/internal" suffix: internal-key
|
||||||
|
// path 1: "keys/generate/exported" suffix: exported-key
|
||||||
|
// path 2: "keys/generate/kms" suffix: kms-key
|
||||||
|
//
|
||||||
|
pathIndexOutOfRange := false
|
||||||
|
|
||||||
|
if suffixes := strings.Split(suffix, "|"); len(suffixes) > 1 || pathIndex > 0 {
|
||||||
|
// if the index is out of bounds, fall back to the old logic
|
||||||
|
if pathIndex >= len(suffixes) {
|
||||||
|
suffix = ""
|
||||||
|
pathIndexOutOfRange = true
|
||||||
|
} else {
|
||||||
|
suffix = suffixes[pathIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a helper that hyphenates & lower-cases the slice except the empty elements
|
||||||
|
toLowerHyphenate := func(parts []string) string {
|
||||||
|
filtered := make([]string, 0, len(parts))
|
||||||
|
for _, e := range parts {
|
||||||
|
if e != "" {
|
||||||
|
filtered = append(filtered, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ToLower(strings.Join(filtered, "-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to using the path + operation to construct the operation id
|
||||||
|
var (
|
||||||
|
needPrefix = prefix == "" && verb == ""
|
||||||
|
needVerb = verb == ""
|
||||||
|
needSuffix = suffix == "" && (verb == "" || pathIndexOutOfRange)
|
||||||
|
)
|
||||||
|
|
||||||
|
if needPrefix {
|
||||||
|
prefix = defaultPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if needVerb {
|
||||||
|
if operation == logical.UpdateOperation {
|
||||||
|
verb = "write"
|
||||||
|
} else {
|
||||||
|
verb = string(operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needSuffix {
|
||||||
|
suffix = toLowerHyphenate(nonWordRe.Split(path, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return toLowerHyphenate([]string{prefix, verb, suffix})
|
||||||
|
}
|
||||||
|
|
||||||
// expandPattern expands a regex pattern by generating permutations of any optional parameters
|
// expandPattern expands a regex pattern by generating permutations of any optional parameters
|
||||||
// and changing named parameters into their {openapi} equivalents.
|
// and changing named parameters into their {openapi} equivalents.
|
||||||
func expandPattern(pattern string) ([]string, error) {
|
func expandPattern(pattern string) ([]string, error) {
|
||||||
|
@ -890,6 +984,40 @@ func splitFields(allFields map[string]*FieldSchema, pattern string) (pathFields,
|
||||||
return pathFields, bodyFields
|
return pathFields, bodyFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withoutOperationHints returns a copy of the given DisplayAttributes without
|
||||||
|
// OperationPrefix / OperationVerb / OperationSuffix since we don't need these
|
||||||
|
// fields in the final output.
|
||||||
|
func withoutOperationHints(in *DisplayAttributes) *DisplayAttributes {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copy := *in
|
||||||
|
|
||||||
|
copy.OperationPrefix = ""
|
||||||
|
copy.OperationVerb = ""
|
||||||
|
copy.OperationSuffix = ""
|
||||||
|
|
||||||
|
// return nil if all fields are empty to avoid empty JSON objects
|
||||||
|
if copy == (DisplayAttributes{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyphenatedToTitleCase(in string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
title := cases.Title(language.English, cases.NoLower)
|
||||||
|
|
||||||
|
for _, word := range strings.Split(in, "-") {
|
||||||
|
b.WriteString(title.String(word))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// cleanedResponse is identical to logical.Response but with nulls
|
// cleanedResponse is identical to logical.Response but with nulls
|
||||||
// removed from from JSON encoding
|
// removed from from JSON encoding
|
||||||
type cleanedResponse struct {
|
type cleanedResponse struct {
|
||||||
|
@ -924,6 +1052,9 @@ func cleanResponse(resp *logical.Response) *cleanedResponse {
|
||||||
// postSysToolsRandomUrlbytes_2
|
// postSysToolsRandomUrlbytes_2
|
||||||
//
|
//
|
||||||
// An optional user-provided suffix ("context") may also be appended.
|
// An optional user-provided suffix ("context") may also be appended.
|
||||||
|
//
|
||||||
|
// Deprecated: operationID's are now populated using `constructOperationID`.
|
||||||
|
// This function is here for backwards compatibility with older plugins.
|
||||||
func (d *OASDocument) CreateOperationIDs(context string) {
|
func (d *OASDocument) CreateOperationIDs(context string) {
|
||||||
opIDCount := make(map[string]int)
|
opIDCount := make(map[string]int)
|
||||||
var paths []string
|
var paths []string
|
||||||
|
@ -951,6 +1082,10 @@ func (d *OASDocument) CreateOperationIDs(context string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oasOperation.OperationID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Discard "_mount_path" from any {thing_mount_path} parameters
|
// Discard "_mount_path" from any {thing_mount_path} parameters
|
||||||
path = strings.Replace(path, "_mount_path", "", 1)
|
path = strings.Replace(path, "_mount_path", "", 1)
|
||||||
|
|
||||||
|
|
|
@ -564,66 +564,6 @@ func TestOpenAPI_Paths(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenAPI_OperationID(t *testing.T) {
|
|
||||||
path1 := &Path{
|
|
||||||
Pattern: "foo/" + GenericNameRegex("id"),
|
|
||||||
Fields: map[string]*FieldSchema{
|
|
||||||
"id": {Type: TypeString},
|
|
||||||
},
|
|
||||||
Operations: map[logical.Operation]OperationHandler{
|
|
||||||
logical.ReadOperation: &PathOperation{},
|
|
||||||
logical.UpdateOperation: &PathOperation{},
|
|
||||||
logical.DeleteOperation: &PathOperation{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
path2 := &Path{
|
|
||||||
Pattern: "Foo/" + GenericNameRegex("id"),
|
|
||||||
Fields: map[string]*FieldSchema{
|
|
||||||
"id": {Type: TypeString},
|
|
||||||
},
|
|
||||||
Operations: map[logical.Operation]OperationHandler{
|
|
||||||
logical.ReadOperation: &PathOperation{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, context := range []string{"", "bar"} {
|
|
||||||
doc := NewOASDocument("version")
|
|
||||||
err := documentPath(path1, nil, "kv", logical.TypeLogical, doc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = documentPath(path2, nil, "kv", logical.TypeLogical, doc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
doc.CreateOperationIDs(context)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
path string
|
|
||||||
op string
|
|
||||||
opID string
|
|
||||||
}{
|
|
||||||
{"/Foo/{id}", "get", "getFooId"},
|
|
||||||
{"/foo/{id}", "get", "getFooId_2"},
|
|
||||||
{"/foo/{id}", "post", "postFooId"},
|
|
||||||
{"/foo/{id}", "delete", "deleteFooId"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
actual := getPathOp(doc.Paths[test.path], test.op).OperationID
|
|
||||||
expected := test.opID
|
|
||||||
if context != "" {
|
|
||||||
expected += "_" + context
|
|
||||||
}
|
|
||||||
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("expected %v, got %v", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpenAPI_CustomDecoder(t *testing.T) {
|
func TestOpenAPI_CustomDecoder(t *testing.T) {
|
||||||
p := &Path{
|
p := &Path{
|
||||||
Pattern: "foo",
|
Pattern: "foo",
|
||||||
|
@ -712,6 +652,216 @@ func TestOpenAPI_CleanResponse(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenAPI_constructOperationID(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
path string
|
||||||
|
pathIndex int
|
||||||
|
pathAttributes *DisplayAttributes
|
||||||
|
operation logical.Operation
|
||||||
|
operationAttributes *DisplayAttributes
|
||||||
|
defaultPrefix string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
path: "",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.Operation(""),
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
"simple-read": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.ReadOperation,
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "test-read-path-to-thing",
|
||||||
|
},
|
||||||
|
"simple-write": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "test-write-path-to-thing",
|
||||||
|
},
|
||||||
|
"operation-verb": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationVerb: "do-something"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "do-something",
|
||||||
|
},
|
||||||
|
"operation-verb-override": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationVerb: "do-something"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationVerb: "do-something-else"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "do-something-else",
|
||||||
|
},
|
||||||
|
"operation-prefix": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "my-prefix-write-path-to-thing",
|
||||||
|
},
|
||||||
|
"operation-prefix-override": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-write-path-to-thing",
|
||||||
|
},
|
||||||
|
"operation-prefix-and-suffix": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: nil,
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "my-prefix-write-my-suffix",
|
||||||
|
},
|
||||||
|
"operation-prefix-and-suffix-override": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-write-better-suffix",
|
||||||
|
},
|
||||||
|
"operation-prefix-verb-suffix": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-create-better-suffix",
|
||||||
|
},
|
||||||
|
"operation-prefix-verb-suffix-override": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix", OperationVerb: "Login"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-login-better-suffix",
|
||||||
|
},
|
||||||
|
"operation-prefix-verb": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationVerb: "Login"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-login",
|
||||||
|
},
|
||||||
|
"operation-verb-suffix": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationVerb: "Login", OperationSuffix: "better-suffix"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "login-better-suffix",
|
||||||
|
},
|
||||||
|
"pipe-delimited-suffix-0": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 0,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-write-suffix0",
|
||||||
|
},
|
||||||
|
"pipe-delimited-suffix-1": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 1,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-write-suffix1",
|
||||||
|
},
|
||||||
|
"pipe-delimited-suffix-2-fallback": {
|
||||||
|
path: "path/to/thing",
|
||||||
|
pathIndex: 2,
|
||||||
|
pathAttributes: nil,
|
||||||
|
operation: logical.UpdateOperation,
|
||||||
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
||||||
|
defaultPrefix: "test",
|
||||||
|
expected: "better-prefix-write-path-to-thing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
name, test := name, test
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
actual := constructOperationID(
|
||||||
|
test.path,
|
||||||
|
test.pathIndex,
|
||||||
|
test.pathAttributes,
|
||||||
|
test.operation,
|
||||||
|
test.operationAttributes,
|
||||||
|
test.defaultPrefix,
|
||||||
|
)
|
||||||
|
if actual != test.expected {
|
||||||
|
t.Fatalf("expected: %s; got: %s", test.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAPI_hyphenatedToTitleCase(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
in string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
"simple": {
|
||||||
|
in: "test",
|
||||||
|
expected: "Test",
|
||||||
|
},
|
||||||
|
"two-words": {
|
||||||
|
in: "two-words",
|
||||||
|
expected: "TwoWords",
|
||||||
|
},
|
||||||
|
"three-words": {
|
||||||
|
in: "one-two-three",
|
||||||
|
expected: "OneTwoThree",
|
||||||
|
},
|
||||||
|
"not-hyphenated": {
|
||||||
|
in: "something_like_this",
|
||||||
|
expected: "Something_like_this",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
name, test := name, test
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
actual := hyphenatedToTitleCase(test.in)
|
||||||
|
if actual != test.expected {
|
||||||
|
t.Fatalf("expected: %s; got: %s", test.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) {
|
func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
@ -227,6 +227,24 @@ type DisplayAttributes struct {
|
||||||
// Action is the verb to use for the operation.
|
// Action is the verb to use for the operation.
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
|
|
||||||
|
// OperationPrefix is a hyphenated lower-case string used to construct
|
||||||
|
// OpenAPI OperationID. It is typically the name of the plugin.
|
||||||
|
OperationPrefix string `json:"operationPrefix,omitempty"`
|
||||||
|
|
||||||
|
// OperationPrefix is a hyphenated lower-case string used to construct
|
||||||
|
// OpenAPI OperationID. It is typically an action to be performed
|
||||||
|
// (e.g. "generate", "sign", "login", etc.). If not specified, the verb
|
||||||
|
// defaults to `logical.Operation.String()` (e.g. "read", "delete", etc.).
|
||||||
|
OperationVerb string `json:"operationVerb,omitempty"`
|
||||||
|
|
||||||
|
// OperationPrefix is a hyphenated lower-case string used to construct
|
||||||
|
// OpenAPI OperationID. It is typically the name of the resource on which
|
||||||
|
// the action is performed (e.g. "role", "credentials", etc.). A pipe (|)
|
||||||
|
// separator can be used to list different suffixes for various permutations
|
||||||
|
// of the `Path.Pattern` regular expression. If not specified, the suffix
|
||||||
|
// defaults to the `Path.Pattern` split by dashes.
|
||||||
|
OperationSuffix string `json:"operationSuffix,omitempty"`
|
||||||
|
|
||||||
// EditType is the optional type of form field needed for a property
|
// EditType is the optional type of form field needed for a property
|
||||||
// This is only necessary for a "textarea" or "file"
|
// This is only necessary for a "textarea" or "file"
|
||||||
EditType string `json:"editType,omitempty"`
|
EditType string `json:"editType,omitempty"`
|
||||||
|
@ -261,6 +279,7 @@ type PathOperation struct {
|
||||||
Deprecated bool
|
Deprecated bool
|
||||||
ForwardPerformanceSecondary bool
|
ForwardPerformanceSecondary bool
|
||||||
ForwardPerformanceStandby bool
|
ForwardPerformanceStandby bool
|
||||||
|
DisplayAttrs *DisplayAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PathOperation) Handler() OperationFunc {
|
func (p *PathOperation) Handler() OperationFunc {
|
||||||
|
@ -277,6 +296,7 @@ func (p *PathOperation) Properties() OperationProperties {
|
||||||
Deprecated: p.Deprecated,
|
Deprecated: p.Deprecated,
|
||||||
ForwardPerformanceSecondary: p.ForwardPerformanceSecondary,
|
ForwardPerformanceSecondary: p.ForwardPerformanceSecondary,
|
||||||
ForwardPerformanceStandby: p.ForwardPerformanceStandby,
|
ForwardPerformanceStandby: p.ForwardPerformanceStandby,
|
||||||
|
DisplayAttrs: p.DisplayAttrs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
sdk/framework/testdata/legacy.json
vendored
17
sdk/framework/testdata/legacy.json
vendored
|
@ -24,9 +24,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLookupId",
|
"operationId": "kv-read-lookup-id",
|
||||||
"summary": "Synopsis",
|
"summary": "Synopsis",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK"
|
||||||
|
@ -34,15 +36,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "postLookupId",
|
"operationId": "kv-write-lookup-id",
|
||||||
"summary": "Synopsis",
|
"summary": "Synopsis",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/KvLookupRequest"
|
"$ref": "#/components/schemas/kv-write-lookup-id-request"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +61,7 @@
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"KvLookupRequest": {
|
"kv-write-lookup-id-request": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"token": {
|
||||||
|
@ -69,4 +73,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
sdk/framework/testdata/operations.json
vendored
34
sdk/framework/testdata/operations.json
vendored
|
@ -37,8 +37,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getFooId",
|
"operationId": "kv-read-foo-id",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"summary": "My Summary",
|
"summary": "My Summary",
|
||||||
"description": "My Description",
|
"description": "My Description",
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -58,8 +60,10 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "postFooId",
|
"operationId": "kv-write-foo-id",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"summary": "Update Summary",
|
"summary": "Update Summary",
|
||||||
"description": "Update Description",
|
"description": "Update Description",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
|
@ -67,7 +71,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/KvFooRequest"
|
"$ref": "#/components/schemas/kv-write-foo-id-request"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,9 +86,11 @@
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"KvFooRequest": {
|
"kv-write-foo-id-request": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["age"],
|
"required": [
|
||||||
|
"age"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"flavors": {
|
"flavors": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
@ -96,7 +102,11 @@
|
||||||
"age": {
|
"age": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "the age",
|
"description": "the age",
|
||||||
"enum": [1, 2, 3],
|
"enum": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
"x-vault-displayAttrs": {
|
"x-vault-displayAttrs": {
|
||||||
"name": "Age",
|
"name": "Age",
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
|
@ -113,9 +123,13 @@
|
||||||
"x-abc-token": {
|
"x-abc-token": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "a header value",
|
"description": "a header value",
|
||||||
"enum": ["a", "b", "c"]
|
"enum": [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"maximum" : {
|
"maximum": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "a maximum value",
|
"description": "a maximum value",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
|
|
13
sdk/framework/testdata/operations_list.json
vendored
13
sdk/framework/testdata/operations_list.json
vendored
|
@ -36,8 +36,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getFooId",
|
"operationId": "kv-list-foo-id",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"summary": "List Summary",
|
"summary": "List Summary",
|
||||||
"description": "List Description",
|
"description": "List Description",
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -53,7 +55,9 @@
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["true"]
|
"enum": [
|
||||||
|
"true"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -61,7 +65,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
sdk/framework/testdata/responses.json
vendored
17
sdk/framework/testdata/responses.json
vendored
|
@ -14,8 +14,10 @@
|
||||||
"description": "Synopsis",
|
"description": "Synopsis",
|
||||||
"x-vault-unauthenticated": true,
|
"x-vault-unauthenticated": true,
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "deleteFoo",
|
"operationId": "kv-delete-foo",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"summary": "Delete stuff",
|
"summary": "Delete stuff",
|
||||||
"responses": {
|
"responses": {
|
||||||
"204": {
|
"204": {
|
||||||
|
@ -24,8 +26,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getFoo",
|
"operationId": "kv-read-foo",
|
||||||
"tags": ["secrets"],
|
"tags": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
"summary": "My Summary",
|
"summary": "My Summary",
|
||||||
"description": "My Description",
|
"description": "My Description",
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -34,7 +38,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/KvFooResponse"
|
"$ref": "#/components/schemas/kv-read-foo-response"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +49,7 @@
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"KvFooResponse": {
|
"kv-read-foo-response": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"field_a": {
|
"field_a": {
|
||||||
|
@ -61,4 +65,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue