Skip to content
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
4 changes: 4 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@
"allow-unexported-struct-field-names": {
"type": "boolean",
"description": "AllowUnexportedStructFieldNames makes it possible to output structs that have fields that are unexported.\nThis is expected to be used in conjunction with an extension such as `x-go-name` to override the output name, and `x-oapi-codegen-extra-tags` to not produce JSON tags for `encoding/json`.\nNOTE that this can be confusing to users of your OpenAPI specification, who may see a field present and therefore be expecting to see it in the response, without understanding the nuance of how `oapi-codegen` generates the code."
},
"preserve-original-operation-id-casing-in-embedded-spec": {
"type": "boolean",
"description": "When `oapi-codegen` parses the original OpenAPI specification, it will apply the configured `output-options.name-normalizer` to each operation's `operationId` before that is used to generate code from.\nHowever, this is also applied to the copy of the `operationId`s in the `embedded-spec` generation, which means that the embedded OpenAPI specification is then out-of-sync with the input specificiation.\nTo ensure that the `operationId` in the embedded spec is preserved as-is from the input specification, set this. NOTE that this will not impact generated code.\nNOTE that if you're using `include-operation-ids` or `exclude-operation-ids` you may want to ensure that the `operationId`s used are correct."
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: "3.0.0"
info:
title: "my spec"
version: 1.0.0
paths:
/pet:
get:
operationId: getPet
responses:
200:
content:
application/json:
schema:
type: string
delete:
# Via https://spec.openapis.org/oas/v3.0.3.html
# operationId: Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.
operationId: this-is-a-kebabAndCamel_SNAKE
responses:
200:
content:
application/json:
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=../../../../configuration-schema.json
package: preserveoriginaloperationidcasinginembeddedspec
output: spec.gen.go
generate:
embedded-spec: true
output-options:
skip-prune: false
compatibility:
preserve-original-operation-id-casing-in-embedded-spec: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package preserveoriginaloperationidcasinginembeddedspec

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml api.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package preserveoriginaloperationidcasinginembeddedspec

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSpecReturnsOperationIdAsOriginallySpecified(t *testing.T) {
spec, err := GetSwagger()
require.NoError(t, err)

path := spec.Paths.Find("/pet")
require.NotNil(t, path, "The path /pet could not be found")

operation := path.GetOperation(http.MethodGet)
require.NotNil(t, operation, "The GET operation on the path /pet could not be found")

// this should be the raw operationId from the spec
assert.Equal(t, "getPet", operation.OperationID)

operation = path.GetOperation(http.MethodDelete)
require.NotNil(t, operation, "The DELETE operation on the path /pet could not be found")

// this should be the raw operationId from the spec
assert.Equal(t, "this-is-a-kebabAndCamel_SNAKE", operation.OperationID)
}
8 changes: 8 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ type CompatibilityOptions struct {
//
// NOTE that this can be confusing to users of your OpenAPI specification, who may see a field present and therefore be expecting to see/use it in the request/response, without understanding the nuance of how `oapi-codegen` generates the code.
AllowUnexportedStructFieldNames bool `yaml:"allow-unexported-struct-field-names"`

// PreserveOriginalOperationIdCasingInEmbeddedSpec ensures that the `operationId` from the source spec is kept intact in case when embedding it into the Embedded Spec output.
// When `oapi-codegen` parses the original OpenAPI specification, it will apply the configured `output-options.name-normalizer` to each operation's `operationId` before that is used to generate code from.
// However, this is also applied to the copy of the `operationId`s in the `embedded-spec` generation, which means that the embedded OpenAPI specification is then out-of-sync with the input specificiation.
// To ensure that the `operationId` in the embedded spec is preserved as-is from the input specification, set this.
// NOTE that this will not impact generated code.
// NOTE that if you're using `include-operation-ids` or `exclude-operation-ids` you may want to ensure that the `operationId`s used are correct.
PreserveOriginalOperationIdCasingInEmbeddedSpec bool `yaml:"preserve-original-operation-id-casing-in-embedded-spec"`
}

func (co CompatibilityOptions) Validate() map[string]string {
Expand Down
28 changes: 19 additions & 9 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ func DescribeSecurityDefinition(securityRequirements openapi3.SecurityRequiremen

// OperationDefinition describes an Operation
type OperationDefinition struct {
OperationId string // The operation_id description from Swagger, used to generate function names
// OperationId is the `operationId` field from the OpenAPI Specification, after going through a `nameNormalizer`, and will be used to generate function names
OperationId string

PathParams []ParameterDefinition // Parameters in the path, eg, /path/:param
HeaderParams []ParameterDefinition // Parameters in HTTP headers
Expand Down Expand Up @@ -558,25 +559,34 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper
// Each path can have a number of operations, POST, GET, OPTIONS, etc.
pathOps := pathItem.Operations()
for _, opName := range SortedMapKeys(pathOps) {
// NOTE that this is a reference to the existing copy of the Operation, so any modifications will modify our shared copy of the spec
op := pathOps[opName]

if pathItem.Servers != nil {
op.Servers = &pathItem.Servers
}
// take a copy of operationId, so we don't modify the underlying spec
operationId := op.OperationID
// We rely on OperationID to generate function names, it's required
if op.OperationID == "" {
op.OperationID, err = generateDefaultOperationID(opName, requestPath, toCamelCaseFunc)
if operationId == "" {
operationId, err = generateDefaultOperationID(opName, requestPath, toCamelCaseFunc)
if err != nil {
return nil, fmt.Errorf("error generating default OperationID for %s/%s: %s",
opName, requestPath, err)
}
} else {
op.OperationID = nameNormalizer(op.OperationID)
operationId = nameNormalizer(operationId)
}
operationId = typeNamePrefix(operationId) + operationId

if !globalState.options.Compatibility.PreserveOriginalOperationIdCasingInEmbeddedSpec {
// update the existing, shared, copy of the spec if we're not wanting to preserve it
op.OperationID = operationId
}
op.OperationID = typeNamePrefix(op.OperationID) + op.OperationID

// These are parameters defined for the specific path method that
// we're iterating over.
localParams, err := DescribeParameters(op.Parameters, []string{op.OperationID + "Params"})
localParams, err := DescribeParameters(op.Parameters, []string{operationId + "Params"})
if err != nil {
return nil, fmt.Errorf("error describing global parameters for %s/%s: %s",
opName, requestPath, err)
Expand All @@ -599,14 +609,14 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper
return nil, err
}

bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(op.OperationID, op.RequestBody)
bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(operationId, op.RequestBody)
if err != nil {
return nil, fmt.Errorf("error generating body definitions: %w", err)
}

ensureExternalRefsInRequestBodyDefinitions(&bodyDefinitions, pathItem.Ref)

responseDefinitions, err := GenerateResponseDefinitions(op.OperationID, op.Responses.Map())
responseDefinitions, err := GenerateResponseDefinitions(operationId, op.Responses.Map())
if err != nil {
return nil, fmt.Errorf("error generating response definitions: %w", err)
}
Expand All @@ -618,7 +628,7 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper
HeaderParams: FilterParameterDefinitionByType(allParams, "header"),
QueryParams: FilterParameterDefinitionByType(allParams, "query"),
CookieParams: FilterParameterDefinitionByType(allParams, "cookie"),
OperationId: nameNormalizer(op.OperationID),
OperationId: nameNormalizer(operationId),
// Replace newlines in summary.
Summary: op.Summary,
Method: opName,
Expand Down