From ae7b464be89d803ad9d68c69d87a650e426f39ac Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 13 Jul 2026 08:27:17 -0700 Subject: [PATCH] Qualify external header schema refs in generated response headers Closes: #2060 When a response header is an external `$ref` (it lives in another file mapped via import-mapping) and that header's schema is itself a `$ref` to a named type, the generated response-headers struct referenced the type by a bare local name (e.g. `ETag ETagSchema`) instead of the imported one (`ETag externalRef0.ETagSchema`). Because the named type is only generated into the imported package, the referencing package failed to compile with an undefined `ETagSchema`. The header's schema `$ref` is written relative to the external file (e.g. `#/components/schemas/ETagSchema`), so `GenerateGoSchema` resolves it against the root spec as a bare local name. Since the header component carries its own `$ref` telling us which file it came from, qualify the schema with that external package via `ensureExternalRefsInSchema`, mirroring how response content schemas are already handled. This is guarded on the schema being a reference so an external header with an inline primitive schema (e.g. `type: string`) is not mangled into `externalRef0.string`. The fix corrects both the strict-server `ResponseHeaders` struct and the client response wrapper's header fields introduced in #2462. Adds internal/test/references/multipackage/header_ref exercising an external header ref with a ref'd schema across strict-server and client generation; the committed generated files must compile as part of the test module, which guards against the undefined-symbol regression. Co-Authored-By: Claude Fable 5 --- .../multipackage/header_ref/common/spec.yaml | 17 + .../multipackage/header_ref/config.api.yaml | 12 + .../header_ref/config.common.yaml | 7 + .../references/multipackage/header_ref/doc.go | 11 + .../header_ref/gen/api/api.gen.go | 567 ++++++++++++++++++ .../header_ref/gen/common/common.gen.go | 7 + .../multipackage/header_ref/issue_test.go | 29 + .../multipackage/header_ref/spec.yaml | 26 + pkg/codegen/operations.go | 14 + 9 files changed, 690 insertions(+) create mode 100644 internal/test/references/multipackage/header_ref/common/spec.yaml create mode 100644 internal/test/references/multipackage/header_ref/config.api.yaml create mode 100644 internal/test/references/multipackage/header_ref/config.common.yaml create mode 100644 internal/test/references/multipackage/header_ref/doc.go create mode 100644 internal/test/references/multipackage/header_ref/gen/api/api.gen.go create mode 100644 internal/test/references/multipackage/header_ref/gen/common/common.gen.go create mode 100644 internal/test/references/multipackage/header_ref/issue_test.go create mode 100644 internal/test/references/multipackage/header_ref/spec.yaml diff --git a/internal/test/references/multipackage/header_ref/common/spec.yaml b/internal/test/references/multipackage/header_ref/common/spec.yaml new file mode 100644 index 000000000..78257c40b --- /dev/null +++ b/internal/test/references/multipackage/header_ref/common/spec.yaml @@ -0,0 +1,17 @@ +# From issue-2060: common spec providing an externally referenced header whose +# schema is a $ref to a named type in this same package. +openapi: "3.0.4" +info: + title: Common + version: "0.0.1" +paths: {} +components: + schemas: + ETagSchema: + type: string + description: RFC 7232 entity tag + headers: + ETag: + description: Entity tag header + schema: + $ref: "#/components/schemas/ETagSchema" diff --git a/internal/test/references/multipackage/header_ref/config.api.yaml b/internal/test/references/multipackage/header_ref/config.api.yaml new file mode 100644 index 000000000..c4f82b6a1 --- /dev/null +++ b/internal/test/references/multipackage/header_ref/config.api.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +package: api +generate: + chi-server: true + strict-server: true + client: true + models: true +import-mapping: + ./common/spec.yaml: github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/header_ref/gen/common +output: gen/api/api.gen.go +output-options: + skip-prune: true diff --git a/internal/test/references/multipackage/header_ref/config.common.yaml b/internal/test/references/multipackage/header_ref/config.common.yaml new file mode 100644 index 000000000..9c51eaa7d --- /dev/null +++ b/internal/test/references/multipackage/header_ref/config.common.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +package: common +generate: + models: true +output: gen/common/common.gen.go +output-options: + skip-prune: true diff --git a/internal/test/references/multipackage/header_ref/doc.go b/internal/test/references/multipackage/header_ref/doc.go new file mode 100644 index 000000000..a28c9223f --- /dev/null +++ b/internal/test/references/multipackage/header_ref/doc.go @@ -0,0 +1,11 @@ +// Package headerref is a regression test for issue-2060: a response header +// that is an external `$ref` whose schema is itself a `$ref` to a named type +// must qualify that type with the imported package (e.g. +// externalRef0.ETagSchema), both in the strict-server response headers struct +// and in the typed client response wrappers. Before the fix the header field +// referenced an undefined local type and the generated package failed to +// compile. +package headerref + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.common.yaml common/spec.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.api.yaml spec.yaml diff --git a/internal/test/references/multipackage/header_ref/gen/api/api.gen.go b/internal/test/references/multipackage/header_ref/gen/api/api.gen.go new file mode 100644 index 000000000..9a8df76e5 --- /dev/null +++ b/internal/test/references/multipackage/header_ref/gen/api/api.gen.go @@ -0,0 +1,567 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/go-chi/chi/v5" + externalRef0 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/header_ref/gen/common" + "github.com/oapi-codegen/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + + // GetThing performs a GET /things/{id} (the `GetThing` operationId) request. + GetThing(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// GetThing performs a GET /things/{id} (the `GetThing` operationId) request. +func (c *Client) GetThing(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetThingRequest(c.Server, id) + 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) +} + +// NewGetThingRequest constructs an http.Request for the GetThing method +func NewGetThingRequest(server string, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/things/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + + // GetThingWithResponse performs a GET /things/{id} (the `GetThing` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetThingWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetThingResponse, error) +} + +// GetThingResponse200Headers the declared response headers of an HTTP 200 response for GetThing +type GetThingResponse200Headers struct { + ETag *externalRef0.ETagSchema +} + +type GetThingResponse struct { + Body []byte + HTTPResponse *http.Response + // JSON200 the response for an HTTP 200 `application/json` response + JSON200 *string + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *GetThingResponse200Headers +} + +// GetJSON200 returns the response for an HTTP 200 `application/json` response +func (r GetThingResponse) GetJSON200() *string { + return r.JSON200 +} + +// GetBody returns the raw response body bytes +func (r GetThingResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetThingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetThingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetThingResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// GetThingWithResponse performs a GET /things/{id} (the `GetThing` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetThingWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetThingResponse, error) { + rsp, err := c.GetThing(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetThingResponse(rsp) +} + +// ParseGetThingResponse parses an HTTP response from a GetThingWithResponse call +func ParseGetThingResponse(rsp *http.Response) (*GetThingResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetThingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest string + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + switch { + case rsp.StatusCode == 200: + var headers GetThingResponse200Headers + if values := rsp.Header.Values("ETag"); len(values) > 0 { + var value externalRef0.ETagSchema + if err := runtime.BindStyledParameterWithOptions("simple", "ETag", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.ETag = &value + } + response.Headers200 = &headers + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /things/{id}) + GetThing(w http.ResponseWriter, r *http.Request, id string) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct{} + +// (GET /things/{id}) +func (_ Unimplemented) GetThing(w http.ResponseWriter, r *http.Request, id string) { + w.WriteHeader(http.StatusNotImplemented) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetThing operation middleware +func (siw *ServerInterfaceWrapper) GetThing(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: r.URL.RawPath == ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetThing(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/things/{id}", wrapper.GetThing) + }) + + return r +} + +type GetThingRequestObject struct { + Id string `json:"id"` +} + +type GetThingResponseObject interface { + VisitGetThingResponse(w http.ResponseWriter) error +} + +type GetThing200ResponseHeaders struct { + ETag *externalRef0.ETagSchema +} + +type GetThing200JSONResponse struct { + Body string + Headers GetThing200ResponseHeaders +} + +func (response GetThing200JSONResponse) VisitGetThingResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response.Body); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + if response.Headers.ETag != nil { + w.Header().Set("ETag", fmt.Sprint(*response.Headers.ETag)) + } + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /things/{id}) + GetThing(ctx context.Context, request GetThingRequestObject) (GetThingResponseObject, error) +} + +type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + if options.RequestErrorHandlerFunc == nil { + options.RequestErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + if options.ResponseErrorHandlerFunc == nil { + options.ResponseErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetThing operation middleware +func (sh *strictHandler) GetThing(w http.ResponseWriter, r *http.Request, id string) { + var request GetThingRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetThing(ctx, request.(GetThingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetThing") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetThingResponseObject); ok { + if err := validResponse.VisitGetThingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/internal/test/references/multipackage/header_ref/gen/common/common.gen.go b/internal/test/references/multipackage/header_ref/gen/common/common.gen.go new file mode 100644 index 000000000..49cfb1b66 --- /dev/null +++ b/internal/test/references/multipackage/header_ref/gen/common/common.gen.go @@ -0,0 +1,7 @@ +// Package common provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package common + +// ETagSchema RFC 7232 entity tag +type ETagSchema = string diff --git a/internal/test/references/multipackage/header_ref/issue_test.go b/internal/test/references/multipackage/header_ref/issue_test.go new file mode 100644 index 000000000..97e36e6e7 --- /dev/null +++ b/internal/test/references/multipackage/header_ref/issue_test.go @@ -0,0 +1,29 @@ +package headerref + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/header_ref/gen/api" + "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/header_ref/gen/common" +) + +// TestExternalHeaderSchemaIsQualified asserts that a response header whose +// schema is an external $ref resolves to the imported package's type in both +// the strict-server response headers struct and the client response wrapper. +// The assignments below only compile if the ETag fields are typed as +// *common.ETagSchema (the imported type), which is the regression guard for +// issue-2060. The rest of the generated package failing to compile would also +// catch a regression, since these files are part of the test module. +func TestExternalHeaderSchemaIsQualified(t *testing.T) { + etag := common.ETagSchema("\"abc123\"") + + strictHeaders := api.GetThing200ResponseHeaders{ETag: &etag} + assert.Equal(t, &etag, strictHeaders.ETag) + + clientResp := api.GetThingResponse{ + Headers200: &api.GetThingResponse200Headers{ETag: &etag}, + } + assert.Equal(t, &etag, clientResp.Headers200.ETag) +} diff --git a/internal/test/references/multipackage/header_ref/spec.yaml b/internal/test/references/multipackage/header_ref/spec.yaml new file mode 100644 index 000000000..4ba4bf601 --- /dev/null +++ b/internal/test/references/multipackage/header_ref/spec.yaml @@ -0,0 +1,26 @@ +# From issue-2060: a response header referencing an external component header +# whose schema is a $ref must qualify the schema type with the imported package. +openapi: "3.0.4" +info: + title: API + version: "0.0.1" +paths: + /things/{id}: + get: + operationId: getThing + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + headers: + ETag: + $ref: "./common/spec.yaml#/components/headers/ETag" + content: + application/json: + schema: + type: string diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 90dca5f90..1c341ad0e 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -1701,6 +1701,20 @@ func GenerateResponseDefinitions(operationID string, responses map[string]*opena if err != nil { return nil, fmt.Errorf("error generating response header definition: %w", err) } + // When the header component itself is an external `$ref` (it lives + // in another file) and its schema references a named type, that + // type is generated into the imported package, not this one. The + // schema `$ref` is written relative to the external file (e.g. + // "#/components/schemas/ETagSchema"), so GenerateGoSchema resolved + // it as a bare local name; qualify it with the header's external + // package so we emit "externalRef0.ETagSchema" instead of an + // undefined local "ETagSchema" (issue #2060). Guard on the schema + // being a reference so an external header with an inline primitive + // schema (e.g. `type: string`) is not turned into + // "externalRef0.string". + if header.Value.Schema != nil && header.Value.Schema.Ref != "" { + ensureExternalRefsInSchema(&contentSchema, header.Ref) + } var nullable bool if header.Value.Schema != nil { nullable = schemaIsNullable(header.Value.Schema.Value)