diff --git a/experimental/internal/codegen/gather.go b/experimental/internal/codegen/gather.go index 6c2083768..227e9c2a7 100644 --- a/experimental/internal/codegen/gather.go +++ b/experimental/internal/codegen/gather.go @@ -362,9 +362,15 @@ func (g *gatherer) gatherFromSchemaProxy(proxy *base.SchemaProxy, path SchemaPat // Get the resolved schema schema := proxy.Schema() - // Check if schema has extensions that require type generation - hasTypeOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeOverride, legacyExtGoType) - hasTypeNameOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeNameOverride, legacyExtGoTypeName) + // Check if schema has extensions that require type generation. + // Skip extension checks for references — libopenapi copies extensions from the + // resolved target, but those extensions belong to the component schema, not to + // each reference site. Without this guard, a property like + // pagination: { $ref: '#/components/schemas/Pagination' } + // would inherit Pagination's x-go-type and be gathered as a separate type, + // producing duplicate declarations. (See oapi-codegen-exp#14.) + hasTypeOverride := !isRef && schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeOverride, legacyExtGoType) + hasTypeNameOverride := !isRef && schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeNameOverride, legacyExtGoTypeName) // Only gather schemas that need a generated type // References are always gathered (they point to real schemas) @@ -387,8 +393,11 @@ func (g *gatherer) gatherFromSchemaProxy(proxy *base.SchemaProxy, path SchemaPat ContentType: g.currentContentType, } - // Parse extensions from the schema - if schema != nil && schema.Extensions != nil { + // Parse extensions from the schema — but not for references. + // When libopenapi resolves a $ref, the resolved schema carries extensions + // from the target (e.g., x-go-type on a component schema). Those extensions + // belong to the component schema descriptor, not to every reference site. + if !isRef && schema != nil && schema.Extensions != nil { ext, err := ParseExtensions(schema.Extensions, path.String()) if err != nil { slog.Warn("failed to parse extensions", diff --git a/experimental/internal/codegen/name_conflict_resolution_test.go b/experimental/internal/codegen/name_conflict_resolution_test.go deleted file mode 100644 index a590f4c85..000000000 --- a/experimental/internal/codegen/name_conflict_resolution_test.go +++ /dev/null @@ -1,623 +0,0 @@ -package codegen - -import ( - "sort" - "testing" - - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// nameConflictSpec is the comprehensive collision spec from PR #2213. -// It exercises all documented name collision patterns: -// -// Pattern A: Cross-section collision (#200, #254, #407, #1881, PR #292) -// Pattern B: Schema vs client wrapper (#1474, #1713, #1450) -// Pattern C: Schema alias vs client wrapper (#1357) -// Pattern D: Operation name = schema response name (#255) -// Pattern E: Schema matches op+Response (#2097, #899) -// Pattern F: x-oapi-codegen-type-name extension + cross-section collision -// Pattern G: x-go-type extension + cross-section collision -// Pattern H: Multiple JSON content types in requestBody (TMF622 scenario, PR #2213) -// -// Note: The experimental code gathers schemas at path-level positions since -// libopenapi resolves $refs. Component-level requestBodies, parameters, responses, -// and headers are NOT gathered separately — they appear at their path-level -// positions instead (e.g., #/paths//foo/post/requestBody/content/application/json/schema). -// This inherently avoids many cross-section collision patterns that affected V2. -const nameConflictSpec = `openapi: "3.1.0" -info: - title: "Comprehensive name collision resolution test" - version: "0.0.0" -paths: - # Pattern A: Cross-section collision - # "Bar" appears in schemas, parameters, requestBodies, responses, and headers. - /foo: - post: - operationId: postFoo - parameters: - - $ref: '#/components/parameters/Bar' - requestBody: - $ref: '#/components/requestBodies/Bar' - responses: - "200": - $ref: '#/components/responses/Bar' - - # Pattern B: Schema vs client wrapper - # Schema "CreateItemResponse" collides with createItem wrapper. - /items: - post: - operationId: createItem - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/CreateItemResponse' - - # Pattern C: Schema alias vs client wrapper - # Schema "ListItemsResponse" (string alias) collides with listItems wrapper. - get: - operationId: listItems - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/ListItemsResponse' - - # Pattern D: Operation name = schema response name - # Schema "QueryResponse" collides with query wrapper. - /query: - post: - operationId: query - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - q: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResponse' - - # Pattern E: Schema matches op+Response - # Schema "GetStatusResponse" collides with getStatus wrapper. - /status: - get: - operationId: getStatus - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/GetStatusResponse' - - # Pattern F: x-oapi-codegen-type-name extension + cross-section collision - /qux: - get: - operationId: getQux - responses: - "200": - $ref: '#/components/responses/Qux' - post: - operationId: postQux - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Qux' - responses: - "200": - description: OK - - # Pattern G: x-go-type extension + cross-section collision - /zap: - get: - operationId: getZap - responses: - "200": - $ref: '#/components/responses/Zap' - post: - operationId: postZap - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Zap' - responses: - "200": - description: OK - - # Pattern H: Multiple JSON content types in requestBody (TMF622 scenario) - /orders: - post: - operationId: createOrder - requestBody: - $ref: '#/components/requestBodies/Order' - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - - # Cross-section: requestBody vs schema - /pets: - post: - operationId: createPet - requestBody: - $ref: '#/components/requestBodies/Pet' - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - -components: - schemas: - Bar: - type: object - properties: - value: - type: string - - Bar2: - type: object - properties: - value: - type: number - - CreateItemResponse: - type: object - properties: - id: - type: string - name: - type: string - - ListItemsResponse: - type: string - - QueryResponse: - type: object - properties: - results: - type: array - items: - type: string - - GetStatusResponse: - type: object - properties: - status: - type: string - timestamp: - type: string - - Order: - type: object - properties: - id: - type: string - product: - type: string - - Pet: - type: object - properties: - id: - type: integer - name: - type: string - - Qux: - type: object - x-oapi-codegen-type-name-override: CustomQux - properties: - label: - type: string - - Zap: - type: object - x-go-type: string - properties: - unused: - type: string - - parameters: - Bar: - name: bar - in: query - schema: - type: string - - requestBodies: - Bar: - content: - application/json: - schema: - type: object - properties: - value: - type: integer - - Order: - content: - application/json: - schema: - type: object - properties: - id: - type: string - product: - type: string - application/merge-patch+json: - schema: - type: object - properties: - product: - type: string - application/json-patch+json: - schema: - type: array - items: - type: object - properties: - op: - type: string - path: - type: string - value: - type: string - - Pet: - content: - application/json: - schema: - type: object - properties: - name: - type: string - species: - type: string - - headers: - Bar: - schema: - type: boolean - - responses: - Bar: - description: Bar response - headers: - X-Bar: - $ref: '#/components/headers/Bar' - content: - application/json: - schema: - type: object - properties: - value1: - $ref: '#/components/schemas/Bar' - value2: - $ref: '#/components/schemas/Bar2' - - Qux: - description: A Qux response - content: - application/json: - schema: - type: object - properties: - data: - type: string - - Zap: - description: A Zap response - content: - application/json: - schema: - type: object - properties: - result: - type: string -` - -// gatherAndComputeNames parses the spec, gathers schemas, and computes names. -// Returns a map of path string -> short name for easy assertions. -func gatherAndComputeNames(t *testing.T, spec string) map[string]string { - t.Helper() - - doc, err := libopenapi.NewDocument([]byte(spec)) - require.NoError(t, err) - - matcher := NewContentTypeMatcher(DefaultContentTypes()) - schemas, err := GatherSchemas(doc, matcher, OutputOptions{}) - require.NoError(t, err) - - converter := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) - contentTypeNamer := NewContentTypeShortNamer(DefaultContentTypeShortNames()) - ComputeSchemaNames(schemas, converter, contentTypeNamer) - - result := make(map[string]string) - for _, s := range schemas { - result[s.Path.String()] = s.ShortName - } - return result -} - -// assertUniqueShortNames verifies that all non-reference schemas have unique short names. -func assertUniqueShortNames(t *testing.T, spec string) map[string]string { - t.Helper() - - doc, err := libopenapi.NewDocument([]byte(spec)) - require.NoError(t, err) - - matcher := NewContentTypeMatcher(DefaultContentTypes()) - schemas, err := GatherSchemas(doc, matcher, OutputOptions{}) - require.NoError(t, err) - - converter := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) - contentTypeNamer := NewContentTypeShortNamer(DefaultContentTypeShortNames()) - ComputeSchemaNames(schemas, converter, contentTypeNamer) - - // Build map of short name -> paths, excluding references - nameToPath := make(map[string][]string) - result := make(map[string]string) - for _, s := range schemas { - result[s.Path.String()] = s.ShortName - if s.Ref == "" { - nameToPath[s.ShortName] = append(nameToPath[s.ShortName], s.Path.String()) - } - } - - for name, paths := range nameToPath { - if len(paths) > 1 { - t.Errorf("short name %q is not unique, used by: %v", name, paths) - } - } - - return result -} - -// TestNameConflictResolution_AllNamesUnique verifies that the collision resolver -// produces unique short names for all non-reference schemas in the comprehensive spec. -func TestNameConflictResolution_AllNamesUnique(t *testing.T) { - names := assertUniqueShortNames(t, nameConflictSpec) - - // Log all names sorted by path for readability - var paths []string - for path := range names { - paths = append(paths, path) - } - sort.Strings(paths) - - t.Log("All schema names:") - for _, path := range paths { - t.Logf(" %-80s -> %s", path, names[path]) - } -} - -// TestNameConflictResolution_PatternA_CrossSection verifies that when "Bar" appears in -// schemas, parameters, requestBodies, responses, and headers, each gets a unique name. -// -// In experimental, libopenapi resolves $refs so component requestBodies/parameters/ -// responses/headers appear at their path-level positions. The component schema keeps -// its bare name, and path-level schemas get operationId-based names. -// -// Covers issues: #200, #254, #407, #1881, PR #292 -func TestNameConflictResolution_PatternA_CrossSection(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - // Component schema keeps bare name - schemaName := names["#/components/schemas/Bar"] - assert.Equal(t, "Bar", schemaName, "component schema should keep bare name") - - // The $ref to components/requestBodies/Bar is resolved by libopenapi and - // gathered at the path level. The operationId "postFoo" gives it a distinct name. - reqBodyName := names["#/paths//foo/post/requestBody/content/application/json/schema"] - assert.NotEmpty(t, reqBodyName, "requestBody schema should be gathered") - assert.NotEqual(t, "Bar", reqBodyName, "requestBody should not collide with schema") - t.Logf("RequestBody Bar (via path) -> %s", reqBodyName) - - // The $ref to components/responses/Bar is resolved at the path level. - respName := names["#/paths//foo/post/responses/200/content/application/json/schema"] - assert.NotEmpty(t, respName, "response schema should be gathered") - assert.NotEqual(t, "Bar", respName, "response should not collide with schema") - t.Logf("Response Bar (via path) -> %s", respName) - - // All three should be distinct - assert.NotEqual(t, reqBodyName, respName, - "requestBody and response should have different names") -} - -// TestNameConflictResolution_PatternB_SchemaVsOperationResponse verifies that -// schema "CreateItemResponse" does not collide with the inline operation response -// type generated for createItem. In experimental, the path response is a $ref to -// the component schema, so it's a reference schema and doesn't generate a new type. -// -// Covers issues: #1474, #1713, #1450 -func TestNameConflictResolution_PatternB_SchemaVsOperationResponse(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - // Component schema keeps bare name - schemaName := names["#/components/schemas/CreateItemResponse"] - assert.Equal(t, "CreateItemResponse", schemaName, - "component schema should keep bare name 'CreateItemResponse'") - - // The inline request body for createItem should get a distinct name - reqBodyName := names["#/paths//items/post/requestBody/content/application/json/schema"] - assert.NotEmpty(t, reqBodyName, "inline request body should be gathered") - t.Logf("createItem requestBody -> %s", reqBodyName) - - // The response is a $ref to CreateItemResponse, so it's a reference and - // uses the component schema's name. Verify it's gathered as a reference. - opRespName := names["#/paths//items/post/responses/200/content/application/json/schema"] - t.Logf("createItem response (ref to schema) -> %s", opRespName) -} - -// TestNameConflictResolution_PatternD_OperationNameMatchesSchema verifies that -// schema "QueryResponse" does not collide with the response type generated -// for operation "query". In experimental, the path response is a $ref to the -// component schema, so no collision occurs. -// -// Covers issue: #255 -func TestNameConflictResolution_PatternD_OperationNameMatchesSchema(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - schemaName := names["#/components/schemas/QueryResponse"] - assert.Equal(t, "QueryResponse", schemaName, - "component schema should keep bare name 'QueryResponse'") - - // The inline request body for query should get a distinct name - reqBodyName := names["#/paths//query/post/requestBody/content/application/json/schema"] - assert.NotEmpty(t, reqBodyName, "inline request body should be gathered") - t.Logf("query requestBody -> %s", reqBodyName) - - // The response is a $ref, check its name - opRespName := names["#/paths//query/post/responses/200/content/application/json/schema"] - t.Logf("query response (ref to schema) -> %s", opRespName) -} - -// TestNameConflictResolution_PatternE_SchemaMatchesOpResponse verifies that -// schema "GetStatusResponse" does not collide with the response type generated -// for operation "getStatus". -// -// Covers issues: #2097, #899 -func TestNameConflictResolution_PatternE_SchemaMatchesOpResponse(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - schemaName := names["#/components/schemas/GetStatusResponse"] - assert.Equal(t, "GetStatusResponse", schemaName, - "component schema should keep bare name 'GetStatusResponse'") - - // The response is a $ref, check its name - opRespName := names["#/paths//status/get/responses/200/content/application/json/schema"] - t.Logf("getStatus response (ref to schema) -> %s", opRespName) -} - -// TestNameConflictResolution_PatternH_MultipleJsonContentTypes verifies that -// when a requestBody has 3 content types that all contain "json", the resolver -// produces unique names for each. The component $ref is resolved by libopenapi, -// so the schemas appear at the path level. -// -// Expected: all 3 content type schemas get unique names despite all mapping to -// the "JSON" content type short name. -// -// Covers: PR #2213 (TMF622 scenario) -func TestNameConflictResolution_PatternH_MultipleJsonContentTypes(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - // Schema "Order" keeps bare name - schemaName := names["#/components/schemas/Order"] - assert.Equal(t, "Order", schemaName, "component schema should keep bare name") - - // The $ref to components/requestBodies/Order is resolved at path level. - // The 3 content types should each get unique names. - jsonName := names["#/paths//orders/post/requestBody/content/application/json/schema"] - mergePatchName := names["#/paths//orders/post/requestBody/content/application/merge-patch+json/schema"] - jsonPatchName := names["#/paths//orders/post/requestBody/content/application/json-patch+json/schema"] - - t.Logf("Order schema -> %s", schemaName) - t.Logf("Order reqBody application/json -> %s", jsonName) - t.Logf("Order reqBody merge-patch+json -> %s", mergePatchName) - t.Logf("Order reqBody json-patch+json -> %s", jsonPatchName) - - // All should be non-empty - assert.NotEmpty(t, jsonName, "application/json requestBody should have a name") - assert.NotEmpty(t, mergePatchName, "application/merge-patch+json requestBody should have a name") - assert.NotEmpty(t, jsonPatchName, "application/json-patch+json requestBody should have a name") - - // All should be different from each other - assert.NotEqual(t, jsonName, mergePatchName, "json and merge-patch+json should have different names") - assert.NotEqual(t, jsonName, jsonPatchName, "json and json-patch+json should have different names") - assert.NotEqual(t, mergePatchName, jsonPatchName, "merge-patch+json and json-patch+json should have different names") - - // None should collide with the schema name - assert.NotEqual(t, schemaName, jsonName, "requestBody json should not collide with schema") - assert.NotEqual(t, schemaName, mergePatchName, "requestBody merge-patch should not collide with schema") - assert.NotEqual(t, schemaName, jsonPatchName, "requestBody json-patch should not collide with schema") -} - -// TestNameConflictResolution_RequestBodyVsSchema verifies that "Pet" in schemas -// and requestBodies resolves correctly: the schema keeps "Pet", the requestBody -// (resolved via $ref at path level) gets a different name. -// -// Covers issues: #254, #407 -func TestNameConflictResolution_RequestBodyVsSchema(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - schemaName := names["#/components/schemas/Pet"] - assert.Equal(t, "Pet", schemaName, "component schema should keep bare name") - - // The $ref to components/requestBodies/Pet is resolved at path level - reqBodyName := names["#/paths//pets/post/requestBody/content/application/json/schema"] - assert.NotEmpty(t, reqBodyName, "requestBody schema should be gathered") - t.Logf("Pet requestBody (via path) -> %s", reqBodyName) - assert.NotEqual(t, "Pet", reqBodyName, "requestBody should not collide with schema") -} - -// TestNameConflictResolution_PatternF_TypeNameOverride verifies that x-oapi-codegen-type-name -// interacts correctly with collision resolution. -func TestNameConflictResolution_PatternF_TypeNameOverride(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - // Schema Qux has x-oapi-codegen-type-name: CustomQux - schemaName := names["#/components/schemas/Qux"] - assert.Equal(t, "CustomQux", schemaName, - "schema with x-oapi-codegen-type-name should use override name") - - // Response Qux (resolved at path level from $ref to components/responses/Qux) - respName := names["#/paths//qux/get/responses/200/content/application/json/schema"] - assert.NotEmpty(t, respName, "response schema should be gathered") - t.Logf("Qux schema -> %s", schemaName) - t.Logf("Qux response (via path) -> %s", respName) - - // They should not collide - assert.NotEqual(t, schemaName, respName, - "schema Qux (CustomQux) and response Qux should not collide") -} - -// TestNameConflictResolution_PatternG_GoTypeOverride verifies that x-go-type -// interacts correctly with collision resolution. -func TestNameConflictResolution_PatternG_GoTypeOverride(t *testing.T) { - names := gatherAndComputeNames(t, nameConflictSpec) - - schemaName := names["#/components/schemas/Zap"] - t.Logf("Zap schema -> %s", schemaName) - - // Response Zap (resolved at path level from $ref to components/responses/Zap) - respName := names["#/paths//zap/get/responses/200/content/application/json/schema"] - assert.NotEmpty(t, respName, "response schema should be gathered") - t.Logf("Zap response (via path) -> %s", respName) - - // They should not collide - assert.NotEqual(t, schemaName, respName, - "schema Zap and response Zap should not collide") -} diff --git a/experimental/internal/codegen/test/name_conflict_resolution/output/types.gen.go b/experimental/internal/codegen/test/name_conflict_resolution/output/types.gen.go index bd9ebbef4..9ce84d414 100644 --- a/experimental/internal/codegen/test/name_conflict_resolution/output/types.gen.go +++ b/experimental/internal/codegen/test/name_conflict_resolution/output/types.gen.go @@ -86,6 +86,10 @@ type Pet struct { func (s *Pet) ApplyDefaults() { } +type Widget = string + +type Metadata = string + // #/components/schemas/Qux type CustomQux struct { Label *string `json:"label,omitempty" form:"label,omitempty"` @@ -158,8 +162,6 @@ type GetZapJSONResponse struct { func (s *GetZapJSONResponse) ApplyDefaults() { } -type PostZapJSONRequest = string - // #/paths//orders/post/requestBody/content/application/json/schema type CreateOrderJSONRequest1 struct { ID *string `json:"id,omitempty" form:"id,omitempty"` @@ -193,6 +195,19 @@ type PostOrdersRequest struct { func (s *PostOrdersRequest) ApplyDefaults() { } +// #/paths//entities/get/responses/200/content/application/json/schema +type ListEntitiesJSONResponse struct { + Data []Widget `json:"data,omitempty" form:"data,omitempty"` + Metadata string `json:"metadata,omitempty" form:"metadata,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ListEntitiesJSONResponse) ApplyDefaults() { +} + +// #/paths//entities/get/responses/200/content/application/json/schema/properties/data +type GetEntities200Response = []Widget + // #/paths//pets/post/requestBody/content/application/json/schema type CreatePetJSONRequest struct { Name *string `json:"name,omitempty" form:"name,omitempty"` @@ -205,32 +220,37 @@ func (s *CreatePetJSONRequest) ApplyDefaults() { // Base64-encoded, gzip-compressed OpenAPI spec. var openAPISpecJSON = []string{ - "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", - "AP//w/EIckAgAAA=", + "H4sIAAAAAAAC/9xaW3PbuhF+16/YkTojeyLJkmw3MWf6ELvJOWmbxokz047fIHIt4gxJIACoSL389w5A", + "8ALeJMU+9fHJg23h8mEv3y52oTCOCeHUg/PZfLYYDGjywLwBgKIqQg+GNyzmAkNMJN0gJCRG8FkUUUlZ", + "AgIli1Kl/1Qo1XAAEKD0BeV6zIP/DAAA3m1R+FSiBBJFEDA/jTFRGNTROFEKRSKB+IJJCVTKVG9KArj9", + "Ij2DNVrO5xMYLS8vzM9L/fNqOYHRxfz1BEZvrq4mMFqcX+oPi4vLufn1Wi9evF6c619v3iz0pvmVXrJc", + "Ls4N7ttIMvDZBoUERjid+izANSZT3PLR4gJOaBLRBLXGnCUSga1+QV/Bd6pC+IPAB+CCcRSKojydDQA0", + "krHBfDafzQcDTlRodBjBbaYnvPXgRis6legbG5amOLG6O+pmKmYK3H4xip8awOE1EUMgnCMREmgC0g8x", + "JnICnAgSo0IhJyDwW4pSXbOAovmYaSInxsIhkgCF1KKfPTCWWZszqbK/ALR2RIv5IfDMxHvG7FSJvMtX", + "gzGKB+PRmc9izhJMlDxzRDi7JmJcIFhhyv3L+bz80AVnd2VQjnGvPbgzVoCNBD+imCj4LrSNRGndGjUu", + "LueZQe3O4Y1AovCDwviLPWqYOSlAmbneL1bk6MaCVGEs99iw3NpnRj1IBQYeKJFiMeyzRGGiqhYinEfU", + "N/Bnv0iWVOfAUsIdA1A7jp7lcm2q5HN9E5jAbY7mcFIJmqwP9qyTMT79tTLTouQ+NbsUbSeQjZOzpp8N", + "m6p8uin4RCJKZCersuxzanfnRPoblUrDy5JHJ5mZMrjTGq2ifH2VVQBrbKdSsfwlGb1hk1oE/9mDT7mS", + "2VXxJ3tOmYXNcG745eWlG76fUxS7rsj9piedoDUje4LWrHlx8frt9xSsjldrnHlXRGlMlB+iBMZf5Wur", + "t+qVrRZcwvyE6k4Rlcou0qzzBQ5xpBnyekO02PmSTN2wR83c7z3YTp1ySZNqqqNyqmspQQME3CpdPbIE", + "XoHfXu/UonY7hJBIQ9AswgssXam4HilSgdln43i71xef0223I8bL+Xx8ePHxOd2O95dL1RP/73mjP562", + "4yNsUaelQ4ifNCHWzNDgaMffE545voTo87debvz9L8L3+vue8Cfz9z3hB/i7euJvyt+F+E/g7589+JhG", + "ivII4S93n/6ey29i17QiFeXh5OvH939cLkH6mBBBmW1jlotz28d8EgG2djKGCE73MIOvITromjnnuQAG", + "LxNChUSZzlNPEZrAUFtxWHCLcImgGKgQQepsM9SaDEGGTKii1B21eGLijMQo1jjl+t551ZzVI5VJzVum", + "tT2sSzCGeVy3ZSDGL+n2sRI7hPvgwYfDO3Ht1jKZWFyDd9LS5J/mnLLAoeEd2M6/ekz1hJBkZDNHF1oU", + "xPWJEDtd5xdyzCCiK/vskr2gbAwOGghDdQPoM64PKLKonMD3kPr6dNxQlspoBz5JJQaAxC+E2oEWRDFY", + "IayJClFgAFZrAhI5EUShiY3pGhNDtWSdV9ZGvSISKkycroi0TzYm6WKiaFls9jUm7+zKR+ScX4l8PdV0", + "fz0NEBBF2sZzUCIE2bXOZ08D8O9e6v+DBmtUY/hvC0SMipjj92B8tOs0ivG+89jkOblzI3MClDVy/uZk", + "c/MtKiczr5gKe9Kz5ghHdVh6u0X1uOR2i+pFpTYj76Cc0LvtXAZ0TUSO2MLSNm5uSJQ6jzJOf5eDLp8G", + "NUnjFYoMtfl4cuQZNOgUu+vBqalb4zXBFaK61Gkgj5RVoEwjJZuy1AO+8gLYLXSjvzpSmmrj2WM9RWOU", + "isS834ROWZcX5bYkqxfhZerIV2R3U0ikxTqv1YJFGRYTnpdbjUore+YyiE9PIi5YkPqq3wi35U32iJNp", + "onBdVGyH8NcpcfK0aoxdFjDmricyKzUUEWtUJhXXv5sopZxZ7NZyx8xlN02PysXxNXse6QQznl9JT3Sc", + "YopE3cav2fUJHyt0Dtn2KrHnHA9uUqlYXD4KtOkXkRVGh9LmB1rv/uY7P+igJhzgnvAncmuaaKL36V1+", + "rdW4LU2kwYrksUcT99G2flvXoZ26ooHeKBD6ioO2wqCz5OwuNxtX8AFc/9lrz9HaeZ2J2e2PLeLeLjl9", + "eKBb2FCSw37dcbwzoyenLfn8OUzo5qWeK6LzoujZ0/cG8Cvo8uPC1Z4gDpOt2ci0VDY//MUE4wd+M1GB", + "Iyo8elNHDO0pAJ6Dqu1frfYoJzn6rbatq2a/4W/ktPaMuGIsQpJkKdHpqCpbnU7qmohiqZ13jtT//jmt", + "7O5qkuyu6v8PeLa8uziur6uI7MAsj4ZZjgf1QsOx9ls9U7f3c5ip/SWkGVeV+qCmyD3hvwVFstZujyr/", + "CwAA//+Vvjq1LyUAAA==", } // decodeOpenAPISpec decodes and decompresses the embedded spec. @@ -386,6 +406,8 @@ func (c *Client) applyEditors(ctx context.Context, req *http.Request, additional // ClientInterface is the interface specification for the client. type ClientInterface interface { + // ListEntities makes a GET request to /entities + ListEntities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) // PostFooWithBody makes a POST request to /foo PostFooWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) PostFoo(ctx context.Context, body postFooJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -419,6 +441,20 @@ type ClientInterface interface { PostZap(ctx context.Context, body postZapJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) } +// ListEntities makes a GET request to /entities + +func (c *Client) ListEntities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListEntitiesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + // PostFooWithBody makes a POST request to /foo func (c *Client) PostFooWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -690,6 +726,33 @@ func (c *Client) PostZap(ctx context.Context, body postZapJSONRequestBody, reqEd return c.Client.Do(req) } +// NewListEntitiesRequest creates a GET request for /entities +func NewListEntitiesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/entities") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPostFooRequest creates a POST request for /foo with application/json body func NewPostFooRequest(server string, body postFooJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1128,6 +1191,36 @@ func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) return &SimpleClient{Client: client}, nil } +// ListEntities makes a GET request to /entities and returns the parsed response. + +// On success, returns the response body. On HTTP error, returns *ClientHttpError[struct{}]. +func (c *SimpleClient) ListEntities(ctx context.Context, reqEditors ...RequestEditorFn) (map[string]any, error) { + var result map[string]any + resp, err := c.Client.ListEntities(ctx, reqEditors...) + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // No typed error response defined + return result, &ClientHttpError[struct{}]{ + StatusCode: resp.StatusCode, + RawBody: rawBody, + } +} + // PostFoo makes a POST request to /foo and returns the parsed response. // On success, returns the response body. On HTTP error, returns *ClientHttpError[struct{}]. diff --git a/experimental/internal/codegen/test/name_conflict_resolution/output/types_test.go b/experimental/internal/codegen/test/name_conflict_resolution/output/types_test.go index 7b77c1ad7..c59275719 100644 --- a/experimental/internal/codegen/test/name_conflict_resolution/output/types_test.go +++ b/experimental/internal/codegen/test/name_conflict_resolution/output/types_test.go @@ -215,6 +215,34 @@ func TestExtGoTypeWithCollisionResolver(t *testing.T) { assertEqual(t, "response-result", *zapResp.Result) } +// TestInlineResponseWithRefProperties verifies Pattern I (oapi-codegen-exp#14): +// when a response has an inline object whose properties contain $refs to component +// schemas with x-go-type, the property-level refs must NOT produce duplicate type +// declarations. The component schemas keep their type aliases (Widget = string, +// Metadata = string), and the inline response object gets its own struct type +// (ListEntitiesJSONResponse). +// +// Covers: oapi-codegen-exp#14 +func TestInlineResponseWithRefProperties(t *testing.T) { + // Component schemas with x-go-type: string produce type aliases + var widget Widget = "widget-value" + assertEqual(t, "widget-value", widget) + + var metadata Metadata = "metadata-value" + assertEqual(t, "metadata-value", metadata) + + // Inline response object has struct fields typed by the component aliases + resp := ListEntitiesJSONResponse{ + Data: []Widget{"w1", "w2"}, + Metadata: "meta", + } + if len(resp.Data) != 2 { + t.Errorf("expected 2 data items, got %d", len(resp.Data)) + } + assertEqual(t, Widget("w1"), resp.Data[0]) + assertEqual(t, "meta", resp.Metadata) +} + // TestJSONRoundTrip verifies that the generated types marshal/unmarshal correctly. func TestJSONRoundTrip(t *testing.T) { // Bar diff --git a/experimental/internal/codegen/test/name_conflict_resolution/spec.yaml b/experimental/internal/codegen/test/name_conflict_resolution/spec.yaml index f57eb3dcc..9f667aad1 100644 --- a/experimental/internal/codegen/test/name_conflict_resolution/spec.yaml +++ b/experimental/internal/codegen/test/name_conflict_resolution/spec.yaml @@ -5,6 +5,7 @@ info: description: | Exercises all documented name collision patterns across issues and PRs: #200, #254, #255, #292, #407, #899, #1357, #1450, #1474, #1713, #1881, #2097, #2213 + Also covers oapi-codegen-exp#14 (inline response object with $ref properties). version: 0.0.0 paths: @@ -145,6 +146,27 @@ paths: schema: $ref: '#/components/schemas/Order' + # Pattern I: Inline response object with $ref properties to x-go-type schemas + # (oapi-codegen-exp#14). The response has an inline object with properties that + # $ref component schemas carrying x-go-type. libopenapi resolves the $refs and + # copies extensions, which previously caused each property ref to be gathered as + # a separate type-generating schema with the same operationId-based name. + /entities: + get: + operationId: listEntities + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: { $ref: '#/components/schemas/Widget' } + metadata: { $ref: '#/components/schemas/Metadata' } + # Cross-section: requestBody vs schema (issues #254, #407) # "Pet" appears in both schemas and requestBodies. /pets: @@ -219,6 +241,22 @@ components: name: type: string + # Pattern I: schemas with x-go-type used as $ref targets in inline response properties. + # (oapi-codegen-exp#14) + Widget: + type: object + x-go-type: string + properties: + id: + type: string + + Metadata: + type: object + x-go-type: string + properties: + total: + type: integer + # Pattern F: x-oapi-codegen-type-name-override extension + cross-section collision Qux: type: object diff --git a/experimental/internal/codegen/test/previous_version/issues/issue_1957/output/types.gen.go b/experimental/internal/codegen/test/previous_version/issues/issue_1957/output/types.gen.go index bad572a3d..629aedaed 100644 --- a/experimental/internal/codegen/test/previous_version/issues/issue_1957/output/types.gen.go +++ b/experimental/internal/codegen/test/previous_version/issues/issue_1957/output/types.gen.go @@ -46,8 +46,6 @@ type TypeWithAllOfID struct { func (s *TypeWithAllOfID) ApplyDefaults() { } -type TypeWithAllOfIDAllOf0 = googleuuid.UUID - type GetRootParameter = googleuuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec.