diff --git a/internal/test/clients/responseheaders/config.yaml b/internal/test/clients/responseheaders/config.yaml new file mode 100644 index 000000000..a91444800 --- /dev/null +++ b/internal/test/clients/responseheaders/config.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +# From issue-2011 +package: responseheaders +output: headers.gen.go +generate: + models: true + client: true +output-options: + # Exercises the nullable.Nullable[T] header-binding path (IsNullable). + nullable-type: true diff --git a/internal/test/clients/responseheaders/doc.go b/internal/test/clients/responseheaders/doc.go new file mode 100644 index 000000000..c15695574 --- /dev/null +++ b/internal/test/clients/responseheaders/doc.go @@ -0,0 +1,11 @@ +// Package responseheaders verifies that response headers declared in the +// spec are parsed into typed Headers fields on the generated +// client response wrappers. The header structs are client-local types named +// after the response wrapper (e.g. GetFooResponse200Headers), emitted and +// consumed entirely within the client output; specs without response +// headers generate no additional code. +// +// From issue-2011. +package responseheaders + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/clients/responseheaders/headers.gen.go b/internal/test/clients/responseheaders/headers.gen.go new file mode 100644 index 000000000..74c6a80b9 --- /dev/null +++ b/internal/test/clients/responseheaders/headers.gen.go @@ -0,0 +1,355 @@ +// Package responseheaders 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 responseheaders + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/nullable" + "github.com/oapi-codegen/runtime" +) + +// Data defines model for data. +type Data struct { + Foo *string `json:"foo,omitempty"` +} + +// NotFound defines model for NotFound. +type NotFound = Data + +// 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 { + + // GetFoo performs a GET /foo (the `GetFoo` operationId) request. + GetFoo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// GetFoo performs a GET /foo (the `GetFoo` operationId) request. +func (c *Client) GetFoo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetFooRequest(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) +} + +// NewGetFooRequest constructs an http.Request for the GetFoo method +func NewGetFooRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/foo") + 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 { + + // GetFooWithResponse performs a GET /foo (the `GetFoo` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetFooWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetFooResponse, error) +} + +// GetFooResponse200Headers the declared response headers of an HTTP 200 response for GetFoo +type GetFooResponse200Headers struct { + Bar *string + XNextCursor nullable.Nullable[string] + XRequestId int +} + +// GetFooResponse404Headers the declared response headers of an HTTP 404 response for GetFoo +type GetFooResponse404Headers struct { + TraceId string +} + +// GetFooResponseDefaultHeaders the declared response headers of an HTTP default response for GetFoo +type GetFooResponseDefaultHeaders struct { + RetryAfter *int +} + +type GetFooResponse struct { + Body []byte + HTTPResponse *http.Response + // JSON200 the response for an HTTP 200 `application/json` response + JSON200 *Data + // JSON404 the response for an HTTP 404 `application/json` response + JSON404 *NotFound + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *GetFooResponse200Headers + // Headers404 the parsed response headers for an HTTP 404 response + Headers404 *GetFooResponse404Headers + // HeadersDefault the parsed response headers for an HTTP default response + HeadersDefault *GetFooResponseDefaultHeaders +} + +// GetJSON200 returns the response for an HTTP 200 `application/json` response +func (r GetFooResponse) GetJSON200() *Data { + return r.JSON200 +} + +// GetJSON404 returns the response for an HTTP 404 `application/json` response +func (r GetFooResponse) GetJSON404() *NotFound { + return r.JSON404 +} + +// GetBody returns the raw response body bytes +func (r GetFooResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetFooResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetFooResponse) 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 GetFooResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// GetFooWithResponse performs a GET /foo (the `GetFoo` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetFooWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetFooResponse, error) { + rsp, err := c.GetFoo(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetFooResponse(rsp) +} + +// ParseGetFooResponse parses an HTTP response from a GetFooWithResponse call +func ParseGetFooResponse(rsp *http.Response) (*GetFooResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetFooResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Data + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + switch { + case rsp.StatusCode == 200: + var headers GetFooResponse200Headers + if values := rsp.Header.Values("bar"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "bar", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.Bar = &value + } + if values := rsp.Header.Values("x-next-cursor"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "x-next-cursor", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.XNextCursor.Set(value) + } + if values := rsp.Header.Values("x-request-id"); len(values) > 0 { + var value int + if err := runtime.BindStyledParameterWithOptions("simple", "x-request-id", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "integer", Format: ""}); err != nil { + return nil, err + } + headers.XRequestId = value + } + response.Headers200 = &headers + case rsp.StatusCode == 404: + var headers GetFooResponse404Headers + if values := rsp.Header.Values("trace-id"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "trace-id", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.TraceId = value + } + response.Headers404 = &headers + case true: + var headers GetFooResponseDefaultHeaders + if values := rsp.Header.Values("retry-after"); len(values) > 0 { + var value int + if err := runtime.BindStyledParameterWithOptions("simple", "retry-after", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "integer", Format: ""}); err != nil { + return nil, err + } + headers.RetryAfter = &value + } + response.HeadersDefault = &headers + } + + return response, nil +} diff --git a/internal/test/clients/responseheaders/headers_test.go b/internal/test/clients/responseheaders/headers_test.go new file mode 100644 index 000000000..e5318e78e --- /dev/null +++ b/internal/test/clients/responseheaders/headers_test.go @@ -0,0 +1,99 @@ +package responseheaders + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeResponse(status int, headers map[string]string, body string) *http.Response { + h := http.Header{} + h.Set("Content-Type", "application/json") + for k, v := range headers { + h.Set(k, v) + } + return &http.Response{ + StatusCode: status, + Header: h, + Body: io.NopCloser(bytes.NewBufferString(body)), + } +} + +func TestParsesDeclaredResponseHeaders(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(200, map[string]string{ + "bar": "bar-value", + "x-request-id": "42", + }, `{"foo":"hello"}`)) + require.NoError(t, err) + + require.NotNil(t, rsp.Headers200) + require.NotNil(t, rsp.Headers200.Bar) + assert.Equal(t, "bar-value", *rsp.Headers200.Bar) + assert.Equal(t, 42, rsp.Headers200.XRequestId) + assert.Nil(t, rsp.Headers404) + assert.Nil(t, rsp.HeadersDefault) +} + +func TestParsesNullableResponseHeader(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(200, map[string]string{ + "x-request-id": "42", + "x-next-cursor": "cursor-value", + }, `{"foo":"hello"}`)) + require.NoError(t, err) + + require.NotNil(t, rsp.Headers200) + require.True(t, rsp.Headers200.XNextCursor.IsSpecified()) + assert.Equal(t, "cursor-value", rsp.Headers200.XNextCursor.MustGet()) +} + +func TestAbsentNullableResponseHeaderIsUnspecified(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(200, nil, `{"foo":"hello"}`)) + require.NoError(t, err) + + require.NotNil(t, rsp.Headers200) + assert.False(t, rsp.Headers200.XNextCursor.IsSpecified()) +} + +func TestParsesComponentResponseHeaders(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(404, map[string]string{ + "trace-id": "abc", + }, `{}`)) + require.NoError(t, err) + + require.NotNil(t, rsp.Headers404) + assert.Equal(t, "abc", rsp.Headers404.TraceId) + assert.Nil(t, rsp.Headers200) +} + +func TestAbsentOptionalHeaderLeavesFieldNil(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(200, nil, `{"foo":"hello"}`)) + require.NoError(t, err) + + require.NotNil(t, rsp.Headers200) + assert.Nil(t, rsp.Headers200.Bar) + // Absent required headers are tolerated (spec-violating server); the + // field is left at its zero value rather than failing the parse. + assert.Equal(t, 0, rsp.Headers200.XRequestId) +} + +func TestParsesDefaultResponseHeaders(t *testing.T) { + rsp, err := ParseGetFooResponse(makeResponse(500, map[string]string{ + "retry-after": "3", + }, ``)) + require.NoError(t, err) + + require.NotNil(t, rsp.HeadersDefault) + require.NotNil(t, rsp.HeadersDefault.RetryAfter) + assert.Equal(t, 3, *rsp.HeadersDefault.RetryAfter) +} + +func TestMalformedHeaderValueErrors(t *testing.T) { + _, err := ParseGetFooResponse(makeResponse(200, map[string]string{ + "x-request-id": "not-a-number", + }, `{"foo":"hello"}`)) + assert.Error(t, err) +} diff --git a/internal/test/clients/responseheaders/spec.yaml b/internal/test/clients/responseheaders/spec.yaml new file mode 100644 index 000000000..777c3fca2 --- /dev/null +++ b/internal/test/clients/responseheaders/spec.yaml @@ -0,0 +1,54 @@ +openapi: 3.0.2 +info: + version: "0.0.1" + title: response-headers +paths: + /foo: + get: + operationId: getFoo + responses: + 200: + description: "endpoint" + headers: + "bar": + schema: + type: string + "x-next-cursor": + schema: + type: string + nullable: true + "x-request-id": + required: true + schema: + type: integer + content: + application/json: + schema: + $ref: "#/components/schemas/data" + 404: + $ref: "#/components/responses/NotFound" + default: + description: "error" + headers: + "retry-after": + schema: + type: integer +components: + responses: + NotFound: + description: "not found" + headers: + "trace-id": + required: true + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/data" + schemas: + data: + type: object + properties: + foo: + type: string diff --git a/internal/test/naming/conflicts/conflicts.gen.go b/internal/test/naming/conflicts/conflicts.gen.go index 736cc88bb..fcf5b2fd3 100644 --- a/internal/test/naming/conflicts/conflicts.gen.go +++ b/internal/test/naming/conflicts/conflicts.gen.go @@ -2106,11 +2106,18 @@ func (r ListEntitiesResponse) ContentType() string { return "" } +// PostFooResponse200Headers the declared response headers of an HTTP 200 response for PostFoo +type PostFooResponse200Headers struct { + XBar *bool +} + type PostFooResponse struct { Body []byte HTTPResponse *http.Response // JSON200 the response for an HTTP 200 `application/json` response JSON200 *BarResponse + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *PostFooResponse200Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -3207,6 +3214,19 @@ func ParsePostFooResponse(rsp *http.Response) (*PostFooResponse, error) { } + switch { + case rsp.StatusCode == 200: + var headers PostFooResponse200Headers + if values := rsp.Header.Values("X-Bar"); len(values) > 0 { + var value bool + if err := runtime.BindStyledParameterWithOptions("simple", "X-Bar", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "boolean", Format: ""}); err != nil { + return nil, err + } + headers.XBar = &value + } + response.Headers200 = &headers + } + return response, nil } diff --git a/internal/test/references/multipackage/pruned_deps/api.gen.go b/internal/test/references/multipackage/pruned_deps/api.gen.go index fd716c421..96a0e17c8 100644 --- a/internal/test/references/multipackage/pruned_deps/api.gen.go +++ b/internal/test/references/multipackage/pruned_deps/api.gen.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" externalRef0 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/pruned_deps/deps" + "github.com/oapi-codegen/runtime" ) // Thing defines model for Thing. @@ -213,6 +214,12 @@ type ClientWithResponsesInterface interface { GetThingsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetThingsResponse, error) } +// GetThingsResponse304Headers the declared response headers of an HTTP 304 response for GetThings +type GetThingsResponse304Headers struct { + CacheControl *string + ETag *string +} + type GetThingsResponse struct { Body []byte HTTPResponse *http.Response @@ -226,6 +233,8 @@ type GetThingsResponse struct { JSON404 *N404 // JSON500 the response for an HTTP 500 `application/json` response JSON500 *externalRef0.DefaultError + // Headers304 the parsed response headers for an HTTP 304 response + Headers304 *GetThingsResponse304Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -357,6 +366,26 @@ func ParseGetThingsResponse(rsp *http.Response) (*GetThingsResponse, error) { } + switch { + case rsp.StatusCode == 304: + var headers GetThingsResponse304Headers + if values := rsp.Header.Values("Cache-Control"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "Cache-Control", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.CacheControl = &value + } + if values := rsp.Header.Values("ETag"); len(values) > 0 { + var value string + 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.Headers304 = &headers + } + return response, nil } diff --git a/internal/test/schemas/deprecated/deprecated.gen.go b/internal/test/schemas/deprecated/deprecated.gen.go index 151316235..c3d6ec0da 100644 --- a/internal/test/schemas/deprecated/deprecated.gen.go +++ b/internal/test/schemas/deprecated/deprecated.gen.go @@ -948,6 +948,12 @@ func (r GetDeprecatedFieldResponse) ContentType() string { return "" } +// GetDeprecatedParamsOldIdResponse200Headers the declared response headers of an HTTP 200 response for GetDeprecatedParamsOldId +type GetDeprecatedParamsOldIdResponse200Headers struct { + // Deprecated: Use the Authorization header instead. + XOldToken *string +} + type GetDeprecatedParamsOldIdResponse struct { Body []byte HTTPResponse *http.Response @@ -955,6 +961,8 @@ type GetDeprecatedParamsOldIdResponse struct { JSON200 *struct { Result *string `json:"result,omitempty"` } + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *GetDeprecatedParamsOldIdResponse200Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -1034,11 +1042,20 @@ func (r PostDeprecatedRequestBodyResponse) ContentType() string { return "" } +// GetLegacyResponse200Headers the declared response headers of an HTTP 200 response for GetLegacy +type GetLegacyResponse200Headers struct { + XCorrelationId *string + // Deprecated: Use X-Trace-Id instead. + XOldTrace *string +} + type GetLegacyResponse struct { Body []byte HTTPResponse *http.Response // JSON200 the response for an HTTP 200 `application/json` response JSON200 *LegacyResponse + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *GetLegacyResponse200Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -1325,6 +1342,19 @@ func ParseGetDeprecatedParamsOldIdResponse(rsp *http.Response) (*GetDeprecatedPa } + switch { + case rsp.StatusCode == 200: + var headers GetDeprecatedParamsOldIdResponse200Headers + if values := rsp.Header.Values("X-Old-Token"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "X-Old-Token", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.XOldToken = &value + } + response.Headers200 = &headers + } + return response, nil } @@ -1377,6 +1407,26 @@ func ParseGetLegacyResponse(rsp *http.Response) (*GetLegacyResponse, error) { } + switch { + case rsp.StatusCode == 200: + var headers GetLegacyResponse200Headers + if values := rsp.Header.Values("X-Correlation-Id"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "X-Correlation-Id", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.XCorrelationId = &value + } + if values := rsp.Header.Values("X-Old-Trace"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "X-Old-Trace", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.XOldTrace = &value + } + response.Headers200 = &headers + } + return response, nil } diff --git a/internal/test/servers/strict/client/client.gen.go b/internal/test/servers/strict/client/client.gen.go index c12648584..d8166877e 100644 --- a/internal/test/servers/strict/client/client.gen.go +++ b/internal/test/servers/strict/client/client.gen.go @@ -1833,9 +1833,17 @@ func (r MultipleRequestAndResponseTypesResponse) ContentType() string { return "" } +// NoContentHeadersResponse204Headers the declared response headers of an HTTP 204 response for NoContentHeaders +type NoContentHeadersResponse204Headers struct { + NullableHeader *string + OptionalHeader *string +} + type NoContentHeadersResponse struct { Body []byte HTTPResponse *http.Response + // Headers204 the parsed response headers for an HTTP 204 response + Headers204 *NoContentHeadersResponse204Headers } // GetBody returns the raw response body bytes @@ -1976,11 +1984,19 @@ func (r ReservedGoKeywordParametersResponse) ContentType() string { return "" } +// ReusableResponsesResponse200Headers the declared response headers of an HTTP 200 response for ReusableResponses +type ReusableResponsesResponse200Headers struct { + Header1 string + Header2 int +} + type ReusableResponsesResponse struct { Body []byte HTTPResponse *http.Response // JSON200 the response for an HTTP 200 `application/json` response JSON200 *Reusableresponse + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *ReusableResponsesResponse200Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -2194,11 +2210,21 @@ func (r URLEncodedExampleResponse) ContentType() string { return "" } +// HeadersExampleResponse200Headers the declared response headers of an HTTP 200 response for HeadersExample +type HeadersExampleResponse200Headers struct { + Header1 string + Header2 int + NullableHeader *string + OptionalHeader *string +} + type HeadersExampleResponse struct { Body []byte HTTPResponse *http.Response // JSON200 the response for an HTTP 200 `application/json` response JSON200 *Example + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *HeadersExampleResponse200Headers } // GetJSON200 returns the response for an HTTP 200 `application/json` response @@ -2235,6 +2261,12 @@ func (r HeadersExampleResponse) ContentType() string { return "" } +// UnionExampleResponse200Headers the declared response headers of an HTTP 200 response for UnionExample +type UnionExampleResponse200Headers struct { + Header1 string + Header2 int +} + type UnionExampleResponse struct { Body []byte HTTPResponse *http.Response @@ -2242,6 +2274,8 @@ type UnionExampleResponse struct { ApplicationalternativeJSON200 *Example // JSON200 the response for an HTTP 200 `application/json` response JSON200 *UnionExample200JSONResponseBody + // Headers200 the parsed response headers for an HTTP 200 response + Headers200 *UnionExampleResponse200Headers } // GetApplicationalternativeJSON200 returns the response for an HTTP 200 `application/alternative+json` response @@ -2743,6 +2777,26 @@ func ParseNoContentHeadersResponse(rsp *http.Response) (*NoContentHeadersRespons HTTPResponse: rsp, } + switch { + case rsp.StatusCode == 204: + var headers NoContentHeadersResponse204Headers + if values := rsp.Header.Values("nullable-header"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "nullable-header", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.NullableHeader = &value + } + if values := rsp.Header.Values("optional-header"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "optional-header", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.OptionalHeader = &value + } + response.Headers204 = &headers + } + return response, nil } @@ -2833,6 +2887,26 @@ func ParseReusableResponsesResponse(rsp *http.Response) (*ReusableResponsesRespo } + switch { + case rsp.StatusCode == 200: + var headers ReusableResponsesResponse200Headers + if values := rsp.Header.Values("header1"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "header1", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.Header1 = value + } + if values := rsp.Header.Values("header2"); len(values) > 0 { + var value int + if err := runtime.BindStyledParameterWithOptions("simple", "header2", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "integer", Format: ""}); err != nil { + return nil, err + } + headers.Header2 = value + } + response.Headers200 = &headers + } + return response, nil } @@ -2952,6 +3026,40 @@ func ParseHeadersExampleResponse(rsp *http.Response) (*HeadersExampleResponse, e } + switch { + case rsp.StatusCode == 200: + var headers HeadersExampleResponse200Headers + if values := rsp.Header.Values("header1"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "header1", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.Header1 = value + } + if values := rsp.Header.Values("header2"); len(values) > 0 { + var value int + if err := runtime.BindStyledParameterWithOptions("simple", "header2", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "integer", Format: ""}); err != nil { + return nil, err + } + headers.Header2 = value + } + if values := rsp.Header.Values("nullable-header"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "nullable-header", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.NullableHeader = &value + } + if values := rsp.Header.Values("optional-header"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "optional-header", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.OptionalHeader = &value + } + response.Headers200 = &headers + } + return response, nil } @@ -2988,5 +3096,25 @@ func ParseUnionExampleResponse(rsp *http.Response) (*UnionExampleResponse, error } + switch { + case rsp.StatusCode == 200: + var headers UnionExampleResponse200Headers + if values := rsp.Header.Values("header1"); len(values) > 0 { + var value string + if err := runtime.BindStyledParameterWithOptions("simple", "header1", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}); err != nil { + return nil, err + } + headers.Header1 = value + } + if values := rsp.Header.Values("header2"); len(values) > 0 { + var value int + if err := runtime.BindStyledParameterWithOptions("simple", "header2", values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "integer", Format: ""}); err != nil { + return nil, err + } + headers.Header2 = value + } + response.Headers200 = &headers + } + return response, nil } diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 2235c5ceb..90dca5f90 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -1028,6 +1028,26 @@ func (h ResponseHeaderDefinition) IsNullable() bool { return globalState.options.OutputOptions.NullableType && h.Nullable } +// SchemaType returns the first OpenAPI type string for this header's schema +// (e.g. "string", "integer"), or empty string if unavailable. +func (h ResponseHeaderDefinition) SchemaType() string { + if h.Schema.OAPISchema != nil && h.Schema.OAPISchema.Type != nil { + if s := h.Schema.OAPISchema.Type.Slice(); len(s) > 0 { + return s[0] + } + } + return "" +} + +// SchemaFormat returns the OpenAPI format string for this header's schema +// (e.g. "date-time", "duration"), or empty string if unavailable. +func (h ResponseHeaderDefinition) SchemaFormat() string { + if h.Schema.OAPISchema != nil { + return h.Schema.OAPISchema.Format + } + return "" +} + // FilterParameterDefinitionByType returns the subset of the specified parameters which are of the // specified type. func FilterParameterDefinitionByType(params []ParameterDefinition, in string) []ParameterDefinition { diff --git a/pkg/codegen/template_helpers.go b/pkg/codegen/template_helpers.go index e080aac62..3a91cfdeb 100644 --- a/pkg/codegen/template_helpers.go +++ b/pkg/codegen/template_helpers.go @@ -338,6 +338,24 @@ func getConditionOfResponseName(statusCodeVar, responseName string) string { } } +// responsesWithHeaders returns the subset of responses that declare headers, +// ordered most-specific-first for emission as switch case clauses: exact +// status codes sort numerically, range wildcards after the exact codes they +// cover ("204" < "2XX" since 'X' > '9'), and "default" (which compiles to +// `case true:`) after everything ('d' > 'X'). +func responsesWithHeaders(responses []ResponseDefinition) []ResponseDefinition { + var out []ResponseDefinition + for _, response := range responses { + if len(response.Headers) > 0 { + out = append(out, response) + } + } + slices.SortFunc(out, func(a, b ResponseDefinition) int { + return strings.Compare(a.StatusCode, b.StatusCode) + }) + return out +} + // This outputs a string array func toStringArray(sarr []string) string { s := strings.Join(sarr, `","`) @@ -427,6 +445,8 @@ var TemplateFunctions = template.FuncMap{ "genResponsePayload": genResponsePayload, "genResponseTypeName": genResponseTypeName, "genResponseUnmarshal": genResponseUnmarshal, + "getConditionOfResponseName": getConditionOfResponseName, + "responsesWithHeaders": responsesWithHeaders, "getResponseTypeDefinitions": getResponseTypeDefinitions, "toStringArray": toStringArray, "lower": strings.ToLower, diff --git a/pkg/codegen/templates/client-with-responses.tmpl b/pkg/codegen/templates/client-with-responses.tmpl index 7e1b77ba4..37fdd83b6 100644 --- a/pkg/codegen/templates/client-with-responses.tmpl +++ b/pkg/codegen/templates/client-with-responses.tmpl @@ -60,6 +60,16 @@ type ClientWithResponsesInterface interface { {{range .}}{{$opid := .OperationId}}{{$op := .}} {{$responseTypeDefinitions := getResponseTypeDefinitions .}} +{{$headerResponses := responsesWithHeaders .Responses}} +{{- range $headerResponses}} +// {{genResponseTypeName $opid | ucFirst}}{{.StatusCode | ucFirst}}Headers the declared response headers of an HTTP {{.StatusCode}} response for {{$opid}} +type {{genResponseTypeName $opid | ucFirst}}{{.StatusCode | ucFirst}}Headers struct { + {{range .Headers -}} + {{with .DeprecationComment}}{{.}} + {{end -}}{{.GoName}} {{.GoTypeDef}} + {{end -}} +} +{{end}} type {{genResponseTypeName $opid | ucFirst}} struct { Body []byte HTTPResponse *http.Response @@ -67,6 +77,10 @@ type {{genResponseTypeName $opid | ucFirst}} struct { // {{.TypeName}} the response for an HTTP {{ .ResponseName }} `{{ .ContentTypeName }}` response {{.TypeName}} *{{.Schema.TypeDecl}} {{- end}} + {{- range $headerResponses}} + // Headers{{.StatusCode | ucFirst}} the parsed response headers for an HTTP {{.StatusCode}} response + Headers{{.StatusCode | ucFirst}} *{{genResponseTypeName $opid | ucFirst}}{{.StatusCode | ucFirst}}Headers + {{- end}} } {{ if not opts.OutputOptions.SkipResponseBodyGetters }} @@ -177,6 +191,36 @@ func Parse{{genResponseTypeName $opid | ucFirst}}(rsp *http.Response) (*{{genRes response := {{genResponsePayload $opid}} {{genResponseUnmarshal .}} + {{- /* Bind declared response headers into the typed Headers + fields. Binding is lenient: an absent header (even a required one) leaves + the field at its zero value rather than failing the parse, mirroring the + body unmarshal's tolerance of spec-violating servers. A present header + that fails to bind to its declared type is an error. */ -}} + {{$headerResponses := responsesWithHeaders .Responses}} + {{- if $headerResponses}} + switch { + {{- range $headerResponses}} + case {{getConditionOfResponseName "rsp.StatusCode" .StatusCode}}: + var headers {{genResponseTypeName $opid | ucFirst}}{{.StatusCode | ucFirst}}Headers + {{- range .Headers}} + if values := rsp.Header.Values({{.Name | toGoString}}); len(values) > 0 { + var value {{.Schema.TypeDecl}} + if err := runtime.BindStyledParameterWithOptions("simple", {{.Name | toGoString}}, values[0], &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}); err != nil { + return nil, err + } + {{- if .IsNullable}} + headers.{{.GoName}}.Set(value) + {{- else if .IsOptional}} + headers.{{.GoName}} = &value + {{- else}} + headers.{{.GoName}} = value + {{- end}} + } + {{- end}} + response.Headers{{.StatusCode | ucFirst}} = &headers + {{- end}} + } + {{- end}} return response, nil }