From d24e195c9e75cd511026257f0b376701a7978d2f Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Fri, 10 Jul 2026 09:50:42 -0700 Subject: [PATCH] feat: content type handling customization Partially addresses #2368. Add an output-options.content-types setting: a map of short names (the tag spliced into generated type names, e.g. the JSON in FindPetsJSONRequestBody) to lists of regex patterns matched against request/response media types. Matching media types get models generated and named with the short name, so e.g. application/vnd.mycompany.v1+json can produce FindPetsV1RequestBody instead of a name derived from the media type. User entries are merged on top of the built-in defaults (JSON, Formdata, Multipart, Text) by key replacement; an empty pattern list disables a key. A media type matching patterns under two different short names is a generation-time error. Vendored +json naming and the client-side YAML/XML/HAL handling remain media-type-derived fallbacks, so an unset mapping produces identical output to before. To keep custom names from changing behavior, templates now branch on media-type-derived methods (IsFormdata/IsMultipart/IsText/IsSupported) instead of comparing NameTag strings, so wire-level handling (marshaling, binding) always follows the media type rather than the name. Co-Authored-By: Claude Fable 5 --- configuration-schema.json | 10 + docs/configuration.md | 12 + .../test/options/content_types/config.yaml | 16 + .../content_types/content_types.gen.go | 770 ++++++++++++++++++ .../content_types/content_types_test.go | 65 ++ internal/test/options/content_types/doc.go | 8 + internal/test/options/content_types/spec.yaml | 62 ++ pkg/codegen/codegen.go | 12 + pkg/codegen/configuration.go | 153 ++++ pkg/codegen/content_types_test.go | 289 +++++++ pkg/codegen/operations.go | 146 +++- pkg/codegen/templates/callback-initiator.tmpl | 4 +- pkg/codegen/templates/client.tmpl | 4 +- pkg/codegen/templates/request-bodies.tmpl | 2 +- pkg/codegen/templates/strict/strict-echo.tmpl | 8 +- .../templates/strict/strict-echo5.tmpl | 8 +- .../strict/strict-fiber-interface.tmpl | 20 +- .../strict/strict-fiber-v3-interface.tmpl | 20 +- .../templates/strict/strict-fiber-v3.tmpl | 8 +- .../templates/strict/strict-fiber.tmpl | 8 +- pkg/codegen/templates/strict/strict-gin.tmpl | 8 +- pkg/codegen/templates/strict/strict-http.tmpl | 8 +- .../templates/strict/strict-interface.tmpl | 20 +- .../strict/strict-iris-interface.tmpl | 20 +- pkg/codegen/templates/strict/strict-iris.tmpl | 8 +- .../templates/strict/strict-responses.tmpl | 4 +- pkg/codegen/templates/webhook-initiator.tmpl | 4 +- 27 files changed, 1580 insertions(+), 117 deletions(-) create mode 100644 internal/test/options/content_types/config.yaml create mode 100644 internal/test/options/content_types/content_types.gen.go create mode 100644 internal/test/options/content_types/content_types_test.go create mode 100644 internal/test/options/content_types/doc.go create mode 100644 internal/test/options/content_types/spec.yaml create mode 100644 pkg/codegen/content_types_test.go diff --git a/configuration-schema.json b/configuration-schema.json index b80019e64f..ea4b76f83e 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -223,6 +223,16 @@ "type": "string" } }, + "content-types": { + "type": "object", + "description": "Maps a short name (used as the tag in generated type names, e.g. the `JSON` in `FindPetsJSONRequestBody`) to a list of regex patterns matched against request/response media types. Matching media types get models generated and named with the short name. Note that typed fields on client response structs are only generated for media types the client knows how to deserialize (JSON-like, YAML and XML); mapping any other media type (e.g. `CSV: ['^text/csv$']`) generates models and named request bodies, but client responses for it expose only the raw body. Entries are merged on top of the built-in defaults by key replacement (`JSON: ['^application/json$']`, `Formdata: ['^application/x-www-form-urlencoded$']`, `Multipart: ['^multipart/']`, `Text: ['^text/plain$']`); an empty list disables a key. A media type matching patterns under two different short names is a generation-time error; unmatched media types keep the remaining built-in behavior.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, "nullable-type": { "type": "boolean", "description": "Whether to generate nullable type for nullable fields" diff --git a/docs/configuration.md b/docs/configuration.md index 2bd0d30752..e4518a87b1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -129,6 +129,18 @@ output-options: skip-client-response-content-type: false skip-response-body-getters: false streaming-content-types: [] + # Short names for media types, used in generated type names. Keys are the + # tag spliced into type names (e.g. the JSON in FindPetsJSONRequestBody), + # values are regex patterns matched against request/response media types. + # Matching media types get models generated and named with the short name. + # User entries are merged on top of these defaults by key replacement + # (an empty list disables a key); unmatched media types keep the + # remaining built-in behavior. + content-types: + JSON: ['^application/json$'] + Formdata: ['^application/x-www-form-urlencoded$'] + Multipart: ['^multipart/'] + Text: ['^text/plain$'] user-templates: {} # OpenAPI Overlay applied to the spec before generation overlay: diff --git a/internal/test/options/content_types/config.yaml b/internal/test/options/content_types/config.yaml new file mode 100644 index 0000000000..f32ba54304 --- /dev/null +++ b/internal/test/options/content_types/config.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: optionscontenttypes +output: content_types.gen.go +generate: + models: true + client: true + std-http-server: true + strict-server: true +output-options: + skip-prune: true + # content-types: short names mapped to media-type regex patterns. V1 renames + # a vendored JSON type in generated identifiers; CSV opts an otherwise + # unsupported media type into model generation. + content-types: + V1: ['^application/vnd\.mycompany\.v1\+json$'] + CSV: ['^text/csv$'] diff --git a/internal/test/options/content_types/content_types.gen.go b/internal/test/options/content_types/content_types.gen.go new file mode 100644 index 0000000000..d0ee955bab --- /dev/null +++ b/internal/test/options/content_types/content_types.gen.go @@ -0,0 +1,770 @@ +//go:build go1.22 + +// Package optionscontenttypes 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 optionscontenttypes + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Pet defines model for Pet. +type Pet struct { + Name string `json:"name"` +} + +// UploadReportCSVBody defines parameters for UploadReport. +type UploadReportCSVBody = string + +// AddPetV1RequestBody defines body for AddPet for application/vnd.mycompany.v1+json ContentType. +type AddPetV1RequestBody = Pet + +// UploadReportCSVRequestBody defines body for UploadReport for text/csv ContentType. +type UploadReportCSVRequestBody = UploadReportCSVBody + +// 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 { + + // AddPetWithBody performs a POST /pets (the `AddPet` operationId) request, + // with any type of body and a specified content type. + AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + // AddPetWithV1Body performs a POST /pets (the `AddPet` operationId) request. + // Takes a body of the `application/vnd.mycompany.v1+json` content type. + AddPetWithV1Body(ctx context.Context, body AddPetV1RequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UploadReportWithBody performs a POST /report (the `UploadReport` operationId) request, + // with any type of body and a specified content type. + UploadReportWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// AddPetWithBody performs a POST /pets (the `AddPet` operationId) request, +// with any type of body and a specified content type. +func (c *Client) AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPetRequestWithBody(c.Server, contentType, body) + 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) +} + +// AddPetWithV1Body performs a POST /pets (the `AddPet` operationId) request. +// Takes a body of the `application/vnd.mycompany.v1+json` content type. +func (c *Client) AddPetWithV1Body(ctx context.Context, body AddPetV1RequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPetRequestWithV1Body(c.Server, body) + 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) +} + +// UploadReportWithBody performs a POST /report (the `UploadReport` operationId) request, +// with any type of body and a specified content type. +func (c *Client) UploadReportWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUploadReportRequestWithBody(c.Server, contentType, body) + 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) +} + +// NewAddPetRequestWithV1Body calls the generic AddPet builder with application/vnd.mycompany.v1+json body +func NewAddPetRequestWithV1Body(server string, body AddPetV1RequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewAddPetRequestWithBody(server, "application/vnd.mycompany.v1+json", bodyReader) +} + +// NewAddPetRequestWithBody constructs an http.Request for the AddPet method, with any body, and a specified content type +func NewAddPetRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pets") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewUploadReportRequestWithBody constructs an http.Request for the UploadReport method, with any body, and a specified content type +func NewUploadReportRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/report") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + 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 { + + // AddPetWithBodyWithResponse performs a POST /pets (the `AddPet` operationId) request, + // with any type of body and a specified content type. + // + // Returns a wrapper object for the known response body format(s). + AddPetWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddPetResponse, error) + + // AddPetWithV1BodyWithResponse performs a POST /pets (the `AddPet` operationId) request. + // Takes a body of the `application/vnd.mycompany.v1+json` content type, and returns a wrapper object for the known response body format(s). + AddPetWithV1BodyWithResponse(ctx context.Context, body AddPetV1RequestBody, reqEditors ...RequestEditorFn) (*AddPetResponse, error) + + // UploadReportWithBodyWithResponse performs a POST /report (the `UploadReport` operationId) request, + // with any type of body and a specified content type. + // + // Returns a wrapper object for the known response body format(s). + UploadReportWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadReportResponse, error) +} + +type AddPetResponse struct { + Body []byte + HTTPResponse *http.Response + // V1200 the response for an HTTP 200 `application/vnd.mycompany.v1+json` response + V1200 *Pet +} + +// GetV1200 returns the response for an HTTP 200 `application/vnd.mycompany.v1+json` response +func (r AddPetResponse) GetV1200() *Pet { + return r.V1200 +} + +// GetBody returns the raw response body bytes +func (r AddPetResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r AddPetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r AddPetResponse) 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 AddPetResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type UploadReportResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r UploadReportResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r UploadReportResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UploadReportResponse) 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 UploadReportResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// AddPetWithBodyWithResponse performs a POST /pets (the `AddPet` operationId) request, +// with any type of body and a specified content type. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) AddPetWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddPetResponse, error) { + rsp, err := c.AddPetWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseAddPetResponse(rsp) +} + +// AddPetWithV1BodyWithResponse performs a POST /pets (the `AddPet` operationId) request. +// Takes a body of the `application/vnd.mycompany.v1+json` content type, and returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) AddPetWithV1BodyWithResponse(ctx context.Context, body AddPetV1RequestBody, reqEditors ...RequestEditorFn) (*AddPetResponse, error) { + rsp, err := c.AddPetWithV1Body(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseAddPetResponse(rsp) +} + +// UploadReportWithBodyWithResponse performs a POST /report (the `UploadReport` operationId) request, +// with any type of body and a specified content type. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) UploadReportWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadReportResponse, error) { + rsp, err := c.UploadReportWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUploadReportResponse(rsp) +} + +// ParseAddPetResponse parses an HTTP response from a AddPetWithResponse call +func ParseAddPetResponse(rsp *http.Response) (*AddPetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &AddPetResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Pet + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.V1200 = &dest + + } + + return response, nil +} + +// ParseUploadReportResponse parses an HTTP response from a UploadReportWithResponse call +func ParseUploadReportResponse(rsp *http.Response) (*UploadReportResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UploadReportResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (POST /pets) + AddPet(w http.ResponseWriter, r *http.Request) + + // (POST /report) + UploadReport(w http.ResponseWriter, r *http.Request) +} + +// 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 + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.AddPet(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UploadReport operation middleware +func (siw *ServerInterfaceWrapper) UploadReport(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UploadReport(w, r) + })) + + 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, StdHTTPServerOptions{}) +} + +// ServeMux is an abstraction of [http.ServeMux]. +type ServeMux interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + http.Handler +} + +type StdHTTPServerOptions struct { + BaseURL string + BaseRouter ServeMux + 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, m ServeMux) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseRouter: m, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseURL: baseURL, + BaseRouter: m, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { + m := options.BaseRouter + + if m == nil { + m = http.NewServeMux() + } + 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, + } + + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/pets", wrapper.AddPet) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/report", wrapper.UploadReport) + + return m +} + +type AddPetRequestObject struct { + Body *AddPetV1RequestBody +} + +type AddPetResponseObject interface { + VisitAddPetResponse(w http.ResponseWriter) error +} + +type AddPet200V1Response Pet + +func (response AddPet200V1Response) VisitAddPetResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/vnd.mycompany.v1+json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type UploadReportRequestObject struct { + Body io.Reader +} + +type UploadReportResponseObject interface { + VisitUploadReportResponse(w http.ResponseWriter) error +} + +type UploadReport200CSVResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response UploadReport200CSVResponse) VisitUploadReportResponse(w http.ResponseWriter) error { + + w.Header().Set("Content-Type", "text/csv") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (POST /pets) + AddPet(ctx context.Context, request AddPetRequestObject) (AddPetResponseObject, error) + + // (POST /report) + UploadReport(ctx context.Context, request UploadReportRequestObject) (UploadReportResponseObject, 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 +} + +// AddPet operation middleware +func (sh *strictHandler) AddPet(w http.ResponseWriter, r *http.Request) { + var request AddPetRequestObject + + var body AddPetV1RequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.AddPet(ctx, request.(AddPetRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "AddPet") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(AddPetResponseObject); ok { + if err := validResponse.VisitAddPetResponse(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)) + } +} + +// UploadReport operation middleware +func (sh *strictHandler) UploadReport(w http.ResponseWriter, r *http.Request) { + var request UploadReportRequestObject + + request.Body = r.Body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UploadReport(ctx, request.(UploadReportRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UploadReport") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UploadReportResponseObject); ok { + if err := validResponse.VisitUploadReportResponse(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/options/content_types/content_types_test.go b/internal/test/options/content_types/content_types_test.go new file mode 100644 index 0000000000..342b201da8 --- /dev/null +++ b/internal/test/options/content_types/content_types_test.go @@ -0,0 +1,65 @@ +package optionscontenttypes + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Compile-time checks: the V1 short name drives the vendored-JSON type names, +// and the CSV mapping produces a model for text/csv even though its +// wire-level handling stays the untyped io.Reader passthrough. +var ( + _ AddPetV1RequestBody = Pet{} + _ UploadReportCSVBody = "" + _ UploadReportCSVRequestBody = "" + _ AddPetResponseObject = AddPet200V1Response{} + _ UploadReportResponseObject = UploadReport200CSVResponse{} + _ io.Reader = UploadReportRequestObject{}.Body +) + +// The client deserializes the V1-mapped vendored JSON response into a typed +// field named after the short name. +func TestClientParsesV1MappedResponse(t *testing.T) { + rsp := &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/vnd.mycompany.v1+json"}}, + Body: io.NopCloser(strings.NewReader(`{"name":"fido"}`)), + } + + parsed, err := ParseAddPetResponse(rsp) + require.NoError(t, err) + require.NotNil(t, parsed.V1200) + assert.Equal(t, "fido", parsed.V1200.Name) +} + +// The CSV-mapped response gets no typed client field — only the raw body — +// but the raw bytes are still captured. +func TestClientKeepsCSVMappedResponseRaw(t *testing.T) { + rsp := &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/csv"}}, + Body: io.NopCloser(strings.NewReader("a,b,c\n")), + } + + parsed, err := ParseUploadReportResponse(rsp) + require.NoError(t, err) + assert.Equal(t, []byte("a,b,c\n"), parsed.GetBody()) +} + +// The strict envelope for the CSV-mapped response streams the body with the +// original media type. +func TestStrictCSVResponseVisit(t *testing.T) { + w := httptest.NewRecorder() + response := UploadReport200CSVResponse{Body: strings.NewReader("a,b,c\n")} + require.NoError(t, response.VisitUploadReportResponse(w)) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "text/csv", w.Header().Get("Content-Type")) + assert.Equal(t, "a,b,c\n", w.Body.String()) +} diff --git a/internal/test/options/content_types/doc.go b/internal/test/options/content_types/doc.go new file mode 100644 index 0000000000..d123c79910 --- /dev/null +++ b/internal/test/options/content_types/doc.go @@ -0,0 +1,8 @@ +// Package optionscontenttypes exercises the content-types output option: a +// short name mapped to media-type regex patterns is used as the tag in +// generated type names (e.g. V1 instead of ApplicationVndMycompanyV1JSON), +// and matched media types get models generated even when they are not one of +// the built-in supported types (e.g. text/csv). +package optionscontenttypes + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/options/content_types/spec.yaml b/internal/test/options/content_types/spec.yaml new file mode 100644 index 0000000000..a3ae020b2b --- /dev/null +++ b/internal/test/options/content_types/spec.yaml @@ -0,0 +1,62 @@ +# ========================================================================== +# Category: options/content_types +# Tests: content-types output option: custom short names in generated type +# names for vendored JSON, and model generation for a media type +# (text/csv) that is not built-in supported. +# +# Folds in (provenance): +# - pkg/codegen/content_types_test.go (unit-level generation assertions) +# ========================================================================== +openapi: "3.0.1" +info: + title: options/content_types + version: "1.0.0" + description: | + Exercises the content-types output option. The V1 mapping renames the + vendored application/vnd.mycompany.v1+json media type in all generated + identifiers (request bodies, client methods, response fields, strict + envelopes). The CSV mapping generates a model for text/csv, whose + wire-level handling remains the untyped io.Reader passthrough; client + responses for it expose only the raw body. +paths: + /pets: + post: + operationId: addPet + requestBody: + required: true + content: + application/vnd.mycompany.v1+json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: the created pet + content: + application/vnd.mycompany.v1+json: + schema: + $ref: '#/components/schemas/Pet' + /report: + post: + operationId: uploadReport + requestBody: + required: true + content: + text/csv: + schema: + type: string + responses: + '200': + description: the processed report + content: + text/csv: + schema: + type: string +components: + schemas: + Pet: + type: object + required: + - name + properties: + name: + type: string diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index ea29b723ee..533f54dd5b 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -74,6 +74,10 @@ var globalState struct { // streamingContentTypeRegexes are the compiled regexes (defaults + user) // used by ResponseContentDefinition.IsStreamingContentType. streamingContentTypeRegexes []*regexp.Regexp + // contentTypeNameTags is the compiled output-options.content-types + // mapping, resolving media types to user-configured short names for use + // in generated type names. Nil-safe: a nil matcher matches nothing. + contentTypeNameTags *contentTypeNameTags } // goImport represents a go package to be imported in the generated code @@ -210,6 +214,14 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { } globalState.streamingContentTypeRegexes = streamingRegexes + // Compile the content-types short name mapping. Validate() already caught + // syntax errors, but surface any regression here too. + contentTypeTags, err := compileContentTypeNameTags(opts.OutputOptions.ContentTypes) + if err != nil { + return "", err + } + globalState.contentTypeNameTags = contentTypeTags + // Multi-pass name resolution: gather all schemas, then resolve names globally. // Only enabled when resolve-type-name-collisions is set. if opts.OutputOptions.ResolveTypeNameCollisions { diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index 88c0a39c88..ee094154ee 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -5,6 +5,8 @@ import ( "fmt" "reflect" "regexp" + "sort" + "strings" ) // defaultStreamingContentTypes are the regex patterns matched against @@ -494,6 +496,41 @@ type OutputOptions struct { // TypeMapping allows customizing OpenAPI type/format to Go type mappings. // User-specified mappings are merged on top of the defaults. TypeMapping *TypeMapping `yaml:"type-mapping,omitempty"` + + // ContentTypes maps a short name to a list of regex patterns matched + // against request/response media types. When a media type matches a + // pattern, the short name is used as the tag in generated type names + // (e.g. mapping `V1: ['^application/vnd\.mycompany\.v1\+json$']` produces + // `FindPetsV1RequestBody` instead of the default name derived from the + // media type), and models are generated for that media type even when it + // isn't one of the built-in supported types. Wire-level handling + // (marshaling, binding) is still derived from the media type itself, so + // renaming e.g. `application/x-www-form-urlencoded` keeps form encoding. + // + // Typed fields on client response structs are only generated for media + // types the client knows how to deserialize (JSON-like, YAML and XML). + // Mapping any other media type (e.g. `CSV: ['^text/csv$']`) generates + // models and named request bodies, but client responses for it expose + // only the raw body. + // + // User entries are merged on top of the built-in defaults by key + // replacement (pattern lists are not merged): + // + // JSON: ['^application/json$'] + // Formdata: ['^application/x-www-form-urlencoded$'] + // Multipart: ['^multipart/'] + // Text: ['^text/plain$'] + // + // An empty pattern list disables a key, so renaming a built-in means + // adding the new key and emptying the old one, e.g. + // `Form: ['^application/x-www-form-urlencoded$']` plus `Formdata: []` — + // otherwise the media type matches under both short names, which is a + // generation-time error. Media types that match no pattern keep the + // remaining built-in behavior (vendored `+json` types get a name derived + // from the media type; anything else is passed through untyped). + // NOTE that mapping two media types that appear on the same request or + // response to the same short name produces colliding type names. + ContentTypes map[string][]string `yaml:"content-types,omitempty"` } func (oo OutputOptions) Validate() map[string]string { @@ -509,6 +546,12 @@ func (oo OutputOptions) Validate() map[string]string { } } + if _, err := compileContentTypeNameTags(oo.ContentTypes); err != nil { + return map[string]string{ + "content-types": err.Error(), + } + } + return nil } @@ -530,6 +573,116 @@ func compileStreamingContentTypes(user []string) ([]*regexp.Regexp, error) { return out, nil } +// contentTypeTagPattern constrains the short names usable as keys in +// output-options.content-types: they're spliced into Go type names, so they +// must be valid exported-identifier fragments. +var contentTypeTagPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*$`) + +// defaultContentTypes is the built-in short name to media-type-pattern +// mapping. User entries in output-options.content-types are merged on top by +// key replacement: a user key with the same name replaces the whole default +// entry (patterns are not merged), and an empty pattern list disables a key. +// +// Vendored JSON media types (e.g. application/vnd.api+json) aren't listed +// here because their default tag is derived from the media type itself; +// they're handled as a fallback after this mapping. Likewise YAML/XML/HAL +// tags only apply to client response deserialization and stay out of this +// mapping so they don't cause request-body models to be generated by default. +func defaultContentTypes() map[string][]string { + return map[string][]string{ + "JSON": {`^application/json$`}, + "Formdata": {`^application/x-www-form-urlencoded$`}, + "Multipart": {`^multipart/`}, + "Text": {`^text/plain$`}, + } +} + +// contentTypeNameTags resolves media types to the short name (tag) used in +// generated type names, from the defaults merged with +// output-options.content-types. +type contentTypeNameTags struct { + tags []string + patterns [][]*regexp.Regexp +} + +// compileContentTypeNameTags validates and compiles the defaults merged with +// the user's output-options.content-types mapping (user keys replace default +// keys wholesale). Tags are stored sorted so matching (and ambiguity +// reporting) is deterministic. +func compileContentTypeNameTags(user map[string][]string) (*contentTypeNameTags, error) { + mapping := defaultContentTypes() + for tag, patterns := range user { + mapping[tag] = patterns + } + + m := &contentTypeNameTags{} + tags := make([]string, 0, len(mapping)) + for tag := range mapping { + tags = append(tags, tag) + } + sort.Strings(tags) + for _, tag := range tags { + if !contentTypeTagPattern.MatchString(tag) { + return nil, fmt.Errorf("content-types short name %q is not usable in Go type names: it must match %s", tag, contentTypeTagPattern) + } + regexes := make([]*regexp.Regexp, 0, len(mapping[tag])) + for _, pattern := range mapping[tag] { + re, err := regexp.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("invalid content-types pattern %q under %q: %w", pattern, tag, err) + } + regexes = append(regexes, re) + } + m.tags = append(m.tags, tag) + m.patterns = append(m.patterns, regexes) + } + return m, nil +} + +// defaultContentTypeNameTags is the compiled default mapping, used when +// Generate() hasn't populated globalState (e.g. helpers exercised directly +// in tests). +var defaultContentTypeNameTags = func() *contentTypeNameTags { + m, err := compileContentTypeNameTags(nil) + if err != nil { + panic(fmt.Sprintf("compiling default content-types: %v", err)) + } + return m +}() + +// activeContentTypeNameTags returns the matcher for the current generation +// run, falling back to the compiled defaults. +func activeContentTypeNameTags() *contentTypeNameTags { + if globalState.contentTypeNameTags != nil { + return globalState.contentTypeNameTags + } + return defaultContentTypeNameTags +} + +// TagFor returns the short name whose patterns match the given media type, +// or "" when no pattern matches (callers fall back to media-type-derived +// naming, or to untyped handling). Matching patterns under more than one +// short name is an error. +func (m *contentTypeNameTags) TagFor(contentType string) (string, error) { + var matched []string + for i, regexes := range m.patterns { + for _, re := range regexes { + if re.MatchString(contentType) { + matched = append(matched, m.tags[i]) + break + } + } + } + switch len(matched) { + case 0: + return "", nil + case 1: + return matched[0], nil + default: + return "", fmt.Errorf("content type %q matches content-types patterns under multiple short names: %s", contentType, strings.Join(matched, ", ")) + } +} + type OutputOptionsOverlay struct { Path string `yaml:"path"` diff --git a/pkg/codegen/content_types_test.go b/pkg/codegen/content_types_test.go new file mode 100644 index 0000000000..aed041547d --- /dev/null +++ b/pkg/codegen/content_types_test.go @@ -0,0 +1,289 @@ +package codegen + +import ( + "go/format" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const contentTypesVendoredSpec = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Content types test +paths: + /pets: + post: + operationId: addPet + requestBody: + required: true + content: + application/vnd.mycompany.v1+json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: ok + content: + application/vnd.mycompany.v1+json: + schema: + $ref: '#/components/schemas/Pet' +components: + schemas: + Pet: + type: object + properties: + name: + type: string +` + +func contentTypesTestConfiguration(contentTypes map[string][]string) Configuration { + return Configuration{ + PackageName: "api", + Generate: GenerateOptions{ + StdHTTPServer: true, + Strict: true, + Client: true, + Models: true, + }, + OutputOptions: OutputOptions{ + ContentTypes: contentTypes, + }, + } +} + +func TestContentTypesShortName(t *testing.T) { + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(contentTypesVendoredSpec)) + require.NoError(t, err) + + code, err := Generate(swagger, contentTypesTestConfiguration(map[string][]string{ + "V1": {`^application/vnd\.mycompany\.v1\+json$`}, + })) + require.NoError(t, err) + + _, err = format.Source([]byte(code)) + require.NoError(t, err) + + // Request body model and client method use the short name. + assert.Contains(t, code, "type AddPetV1RequestBody = Pet") + assert.Contains(t, code, "AddPetWithV1Body(") + // Client response wrapper field uses the short name and is deserialized + // as JSON (derived from the media type, not the name). + assert.Contains(t, code, "V1200 *Pet") + assert.Contains(t, code, "response.V1200 = &dest") + assert.Contains(t, code, "json.Unmarshal(bodyBytes, &dest)") + // Strict server envelope uses the short name. + assert.Contains(t, code, "type AddPet200V1Response Pet") +} + +func TestContentTypesUnsetKeepsDefaultNames(t *testing.T) { + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(contentTypesVendoredSpec)) + require.NoError(t, err) + + code, err := Generate(swagger, contentTypesTestConfiguration(nil)) + require.NoError(t, err) + + assert.NotContains(t, code, "V1200") + assert.NotContains(t, code, "AddPetV1RequestBody") +} + +func TestContentTypesRenamesBuiltinWithoutChangingBehavior(t *testing.T) { + spec := ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Content types form test +paths: + /login: + post: + operationId: login + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + user: + type: string + responses: + '204': + description: no content +` + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + // Renaming a built-in means adding the new key and disabling the default + // one; leaving both matching is an ambiguity error (checked below). + code, err := Generate(swagger, contentTypesTestConfiguration(map[string][]string{ + "Form": {`^application/x-www-form-urlencoded$`}, + "Formdata": {}, + })) + require.NoError(t, err) + + _, err = format.Source([]byte(code)) + require.NoError(t, err) + + // The type names use the custom short name instead of "Formdata"... + assert.Contains(t, code, "LoginFormRequestBody") + assert.NotContains(t, code, "LoginFormdataRequestBody") + // ...but form handling is still derived from the media type. + assert.Contains(t, code, "runtime.BindForm") + assert.Contains(t, code, "runtime.MarshalForm") + + // Without disabling the default key, the media type matches under both + // short names. + _, err = Generate(swagger, contentTypesTestConfiguration(map[string][]string{ + "Form": {`^application/x-www-form-urlencoded$`}, + })) + require.Error(t, err) + assert.Contains(t, err.Error(), "multiple short names") + assert.Contains(t, err.Error(), "Formdata") +} + +func TestContentTypesGeneratesModelForUnsupportedMediaType(t *testing.T) { + spec := ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Content types csv test +paths: + /report: + post: + operationId: uploadReport + requestBody: + required: true + content: + text/csv: + schema: + type: string + responses: + '200': + description: the processed report + content: + text/csv: + schema: + type: string +` + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + // Without a mapping, text/csv gets no model at all. + code, err := Generate(swagger, contentTypesTestConfiguration(nil)) + require.NoError(t, err) + assert.NotContains(t, code, "UploadReportCSVRequestBody") + + code, err = Generate(swagger, contentTypesTestConfiguration(map[string][]string{ + "CSV": {`^text/csv$`}, + })) + require.NoError(t, err) + + _, err = format.Source([]byte(code)) + require.NoError(t, err) + + // The model is generated with the short name, but the wire-level handling + // stays the generic io.Reader passthrough. + assert.Contains(t, code, "type UploadReportCSVBody = string") + assert.Contains(t, code, "type UploadReportCSVRequestBody = UploadReportCSVBody") + assert.Contains(t, code, "Body io.Reader") + assert.NotContains(t, code, "NewUploadReportRequestWithCSVBody") + + // On the response side the mapping affects naming only: typed client + // response fields are generated solely for media types we know how to + // deserialize (JSON, YAML, XML), so the mapped CSV response exposes just + // the raw body — no CSV200 field and no unmarshal case. + assert.Contains(t, code, "ParseUploadReportResponse") + assert.NotContains(t, code, "CSV200") + assert.NotContains(t, code, "response.CSV200") +} + +func TestContentTypesAmbiguousMatchErrors(t *testing.T) { + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(contentTypesVendoredSpec)) + require.NoError(t, err) + + _, err = Generate(swagger, contentTypesTestConfiguration(map[string][]string{ + "Mine": {`^application/vnd\.mycompany\.`}, + "AnyV1": {`\.v1\+json$`}, + })) + require.Error(t, err) + assert.Contains(t, err.Error(), "multiple short names") + assert.Contains(t, err.Error(), "AnyV1") + assert.Contains(t, err.Error(), "Mine") +} + +func TestContentTypesValidation(t *testing.T) { + badRegex := OutputOptions{ContentTypes: map[string][]string{ + "JSON": {`[`}, + }} + problems := badRegex.Validate() + require.Contains(t, problems, "content-types") + assert.Contains(t, problems["content-types"], "invalid content-types pattern") + + badName := OutputOptions{ContentTypes: map[string][]string{ + "not-a-go-name": {`^application/json$`}, + }} + problems = badName.Validate() + require.Contains(t, problems, "content-types") + assert.Contains(t, problems["content-types"], "not usable in Go type names") + + ok := OutputOptions{ContentTypes: map[string][]string{ + "V1": {`^application/vnd\.mycompany\.v1\+json$`}, + }} + assert.Empty(t, ok.Validate()) +} + +func TestContentTypeNameTagsTagFor(t *testing.T) { + // With no user entries, the defaults apply. + m, err := compileContentTypeNameTags(nil) + require.NoError(t, err) + + for contentType, want := range map[string]string{ + "application/json": "JSON", + "application/x-www-form-urlencoded": "Formdata", + "multipart/form-data": "Multipart", + "multipart/mixed": "Multipart", + "text/plain": "Text", + // Vendored JSON isn't part of the mapping; callers derive its name + // from the media type. + "application/vnd.api+json": "", + "text/csv": "", + } { + tag, err := m.TagFor(contentType) + require.NoError(t, err) + assert.Equal(t, want, tag, contentType) + } + + // User entries replace default keys wholesale. + m, err = compileContentTypeNameTags(map[string][]string{ + "JSON": {`^application/vnd\.foo\+json$`}, + "Text": {}, + }) + require.NoError(t, err) + + tag, err := m.TagFor("application/vnd.foo+json") + require.NoError(t, err) + assert.Equal(t, "JSON", tag) + + // application/json no longer matches the replaced JSON key... + tag, err = m.TagFor("application/json") + require.NoError(t, err) + assert.Equal(t, "", tag) + + // ...and an empty list disables a key. + tag, err = m.TagFor("text/plain") + require.NoError(t, err) + assert.Equal(t, "", tag) + + // Untouched defaults survive the merge. + tag, err = m.TagFor("multipart/form-data") + require.NoError(t, err) + assert.Equal(t, "Multipart", tag) +} diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 2235c5ceb9..9b98dd3079 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -617,17 +617,35 @@ func (o *OperationDefinition) GetResponseTypeDefinitions() ([]ResponseTypeDefini contentType := responseRef.Value.Content[contentTypeName] // We can only generate a type if we have a schema: if contentType.Schema != nil { + // The content-types mapping (defaults merged with + // output-options.content-types) is consulted first; media + // types it doesn't cover fall through to the cases below. + mappedTag, err := activeContentTypeNameTags().TagFor(contentTypeName) + if err != nil { + return nil, fmt.Errorf("resolving content-types short name for %s.%s: %w", o.OperationId, responseName, err) + } + var typeName, tag string switch { + // Mapped media types only get a typed response field when + // we know how to deserialize them; anything else is + // skipped (the raw body remains available). + case mappedTag != "": + if !util.IsMediaTypeJson(contentTypeName) && + !slices.Contains(contentTypesJSON, contentTypeName) && + !slices.Contains(contentTypesHalJSON, contentTypeName) && + !slices.Contains(contentTypesYAML, contentTypeName) && + !slices.Contains(contentTypesXML, contentTypeName) { + continue + } + typeName = fmt.Sprintf("%s%s", mappedTag, nameNormalizer(responseName)) + tag = mappedTag + // HAL+JSON: case slices.Contains(contentTypesHalJSON, contentTypeName): typeName = fmt.Sprintf("HALJSON%s", nameNormalizer(responseName)) tag = "HALJSON" - case contentTypeName == "application/json": - // if it's the standard application/json - typeName = fmt.Sprintf("JSON%s", nameNormalizer(responseName)) - tag = "JSON" // Vendored JSON case slices.Contains(contentTypesJSON, contentTypeName) || util.IsMediaTypeJson(contentTypeName): baseTypeName := fmt.Sprintf("%s%s", nameNormalizer(contentTypeName), nameNormalizer(responseName)) @@ -862,7 +880,7 @@ func (r RequestBodyDefinition) Suffix() string { // IsSupportedByClient returns true if we support this content type for client. Otherwise only generic method will ge generated func (r RequestBodyDefinition) IsSupportedByClient() bool { - return r.IsJSON() || r.NameTag == "Formdata" || r.NameTag == "Text" + return r.IsJSON() || r.IsFormdata() || r.IsText() } // IsJSON returns whether this is a JSON media type, for instance: @@ -873,8 +891,33 @@ func (r RequestBodyDefinition) IsJSON() bool { return util.IsMediaTypeJson(r.ContentType) } -// IsSupported returns true if we support this content type for server. Otherwise io.Reader will be generated +// IsFormdata returns whether this body is form-urlencoded +func (r RequestBodyDefinition) IsFormdata() bool { + return r.ContentType == "application/x-www-form-urlencoded" +} + +// IsMultipart returns whether this body is a multipart media type +func (r RequestBodyDefinition) IsMultipart() bool { + return strings.HasPrefix(r.ContentType, "multipart/") +} + +// IsText returns whether this body is plain text +func (r RequestBodyDefinition) IsText() bool { + return r.ContentType == "text/plain" +} + +// IsSupported returns true if we support this content type for server. Otherwise io.Reader will be generated. +// Wire-level support is derived from the media type, not from NameTag, so +// user-configured short names (output-options.content-types) don't change +// how a body is bound. func (r RequestBodyDefinition) IsSupported() bool { + return r.IsJSON() || r.IsFormdata() || r.IsMultipart() || r.IsText() +} + +// HasModel returns true when a Go model type is generated for this body — +// either a built-in supported media type or one matched by +// output-options.content-types. +func (r RequestBodyDefinition) HasModel() bool { return r.NameTag != "" } @@ -937,8 +980,12 @@ func (r ResponseContentDefinition) TypeDef(opID string, statusCode int) *TypeDef } } +// IsSupported returns true if we know how to serialize this content type. +// Otherwise io.Reader will be generated. Wire-level support is derived from +// the media type, not from NameTag, so user-configured short names +// (output-options.content-types) don't change how a response is written. func (r ResponseContentDefinition) IsSupported() bool { - return r.NameTag != "" + return r.IsJSON() || r.IsFormdata() || r.IsMultipart() || r.IsText() } // HasFixedContentType returns true if content type has fixed content type, i.e. contains no "*" symbol @@ -961,6 +1008,21 @@ func (r ResponseContentDefinition) IsJSON() bool { return util.IsMediaTypeJson(r.ContentType) } +// IsFormdata returns whether this content is form-urlencoded +func (r ResponseContentDefinition) IsFormdata() bool { + return r.ContentType == "application/x-www-form-urlencoded" +} + +// IsMultipart returns whether this content is a multipart media type +func (r ResponseContentDefinition) IsMultipart() bool { + return strings.HasPrefix(r.ContentType, "multipart/") +} + +// IsText returns whether this content is plain text +func (r ResponseContentDefinition) IsText() bool { + return r.ContentType == "text/plain" +} + // IsStreamingContentType reports whether this response's media type matches // any configured streaming-content-types pattern (defaults merged with // OutputOptions.StreamingContentTypes). Templates use this to emit a @@ -1492,28 +1554,30 @@ func GenerateBodyDefinitions(operationID string, bodyOrRef *openapi3.RequestBody for _, contentType := range SortedMapKeys(body.Content) { content := body.Content[contentType] - var tag string - var defaultBody bool - - switch { - case contentType == "application/json": - tag = "JSON" - defaultBody = true - case util.IsMediaTypeJson(contentType): - tag = mediaTypeToCamelCase(contentType) - case strings.HasPrefix(contentType, "multipart/"): - tag = "Multipart" - case contentType == "application/x-www-form-urlencoded": - tag = "Formdata" - case contentType == "text/plain": - tag = "Text" - default: - bd := RequestBodyDefinition{ - Required: body.Required, - ContentType: contentType, + + // application/json stays the unsuffixed default body even when + // renamed via output-options.content-types. + defaultBody := contentType == "application/json" + + // The content-types mapping (defaults merged with + // output-options.content-types) names the body type; vendored JSON + // media types it doesn't cover get a name derived from the media + // type, and anything else is passed through untyped. + tag, err := activeContentTypeNameTags().TagFor(contentType) + if err != nil { + return nil, nil, fmt.Errorf("resolving content-types short name for %s: %w", operationID, err) + } + if tag == "" { + if util.IsMediaTypeJson(contentType) { + tag = mediaTypeToCamelCase(contentType) + } else { + bd := RequestBodyDefinition{ + Required: body.Required, + ContentType: contentType, + } + bodyDefinitions = append(bodyDefinitions, bd) + continue } - bodyDefinitions = append(bodyDefinitions, bd) - continue } bodyTypeName := operationID + tag + "Body" @@ -1605,19 +1669,21 @@ func GenerateResponseDefinitions(operationID string, responses map[string]*opena for _, contentType := range SortedMapKeys(response.Content) { content := response.Content[contentType] - var tag string - switch { - case contentType == "application/json": - tag = "JSON" - case util.IsMediaTypeJson(contentType): + + // The content-types mapping (defaults merged with + // output-options.content-types) names the response content; + // vendored JSON media types it doesn't cover get a name derived + // from the media type, and anything else is passed through + // untyped. Wire-level handling always follows from the media + // type itself, not the name. + tag, err := activeContentTypeNameTags().TagFor(contentType) + if err != nil { + return nil, fmt.Errorf("resolving content-types short name for %s.%s: %w", operationID, statusCode, err) + } + if tag == "" && util.IsMediaTypeJson(contentType) { tag = mediaTypeToCamelCase(contentType) - case contentType == "application/x-www-form-urlencoded": - tag = "Formdata" - case strings.HasPrefix(contentType, "multipart/"): - tag = "Multipart" - case contentType == "text/plain": - tag = "Text" - default: + } + if tag == "" { rcd := ResponseContentDefinition{ ContentType: contentType, } diff --git a/pkg/codegen/templates/callback-initiator.tmpl b/pkg/codegen/templates/callback-initiator.tmpl index 908695bf37..8a37a9acb1 100644 --- a/pkg/codegen/templates/callback-initiator.tmpl +++ b/pkg/codegen/templates/callback-initiator.tmpl @@ -131,13 +131,13 @@ func New{{$opid}}CallbackRequest{{.Suffix}}(targetURL string{{if $hasParams}}, p return nil, err } bodyReader = bytes.NewReader(buf) - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} bodyStr, err := runtime.MarshalForm(body, nil) if err != nil { return nil, err } bodyReader = strings.NewReader(bodyStr.Encode()) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} if stringer, ok := interface{}(body).(fmt.Stringer); ok { bodyReader = strings.NewReader(stringer.String()) } else { diff --git a/pkg/codegen/templates/client.tmpl b/pkg/codegen/templates/client.tmpl index 55f298a4cb..6845f1c937 100644 --- a/pkg/codegen/templates/client.tmpl +++ b/pkg/codegen/templates/client.tmpl @@ -170,13 +170,13 @@ func New{{$opid}}Request{{.Suffix}}(server string{{genParamArgs $pathParams}}{{i return nil, err } bodyReader = bytes.NewReader(buf) - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} bodyStr, err := runtime.MarshalForm(body, nil) if err != nil { return nil, err } bodyReader = strings.NewReader(bodyStr.Encode()) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} if stringer, ok := interface{}(body).(fmt.Stringer); ok { bodyReader = strings.NewReader(stringer.String()) } else { diff --git a/pkg/codegen/templates/request-bodies.tmpl b/pkg/codegen/templates/request-bodies.tmpl index 46415191e1..8fa588728a 100644 --- a/pkg/codegen/templates/request-bodies.tmpl +++ b/pkg/codegen/templates/request-bodies.tmpl @@ -1,6 +1,6 @@ {{range .}}{{$opid := .OperationId}} {{range .Bodies}} -{{if .IsSupported -}} +{{if .HasModel -}} {{$contentType := .ContentType -}} {{with .TypeDef $opid}} // {{.TypeName}} defines body for {{$opid}} for {{$contentType}} ContentType. diff --git a/pkg/codegen/templates/strict/strict-echo.tmpl b/pkg/codegen/templates/strict/strict-echo.tmpl index 404c0b7265..2ba441bef8 100644 --- a/pkg/codegen/templates/strict/strict-echo.tmpl +++ b/pkg/codegen/templates/strict/strict-echo.tmpl @@ -55,7 +55,7 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if form, err := ctx.FormParams(); err == nil { var body {{$opid}}{{.NameTag}}RequestBody if err := runtime.BindForm(&body, form, nil, nil); err != nil { @@ -65,7 +65,7 @@ type strictHandler struct { } else { return err } - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} if reader, err := ctx.Request().MultipartReader(); err != nil { return err @@ -81,7 +81,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request().Body, boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data, err := io.ReadAll(ctx.Request().Body) if err != nil { return err @@ -96,7 +96,7 @@ type strictHandler struct { {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-echo5.tmpl b/pkg/codegen/templates/strict/strict-echo5.tmpl index 551759c9e8..0dddca268d 100644 --- a/pkg/codegen/templates/strict/strict-echo5.tmpl +++ b/pkg/codegen/templates/strict/strict-echo5.tmpl @@ -55,7 +55,7 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if form, err := ctx.FormValues(); err == nil { var body {{$opid}}{{.NameTag}}RequestBody if err := runtime.BindForm(&body, form, nil, nil); err != nil { @@ -65,7 +65,7 @@ type strictHandler struct { } else { return err } - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} if reader, err := ctx.Request().MultipartReader(); err != nil { return err @@ -81,7 +81,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request().Body, boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data, err := io.ReadAll(ctx.Request().Body) if err != nil { return err @@ -90,7 +90,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl index 4deabb4863..c821aed9d1 100644 --- a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl @@ -12,7 +12,7 @@ {{end -}} {{$multipleBodies := gt (len .Bodies) 1 -}} {{range .Bodies -}} - {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} + {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if .IsMultipart}}*multipart.Reader{{else if .IsSupported}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} {{end -}} } @@ -41,13 +41,13 @@ {{range .Contents}} {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} + {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (.IsMultipart) (.IsText)) -}} type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response {{else -}} type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response } {{end}} {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}} - type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + type {{$receiverTypeName}} {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} func (t {{$receiverTypeName}}) MarshalJSON() ([]byte, error) { @@ -60,7 +60,7 @@ {{- end}} {{else -}} type {{$receiverTypeName}} struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + Body {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{if $hasHeaders -}} Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders {{end -}} @@ -93,10 +93,10 @@ ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) {{end -}} {{end -}} - {{if eq .NameTag "Multipart" -}} + {{if .IsMultipart -}} writer := multipart.NewWriter(ctx.Response().BodyWriter()) {{end -}} - ctx.Response().Header.Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) + ctx.Response().Header.Set("Content-Type", {{if .IsMultipart}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) @@ -107,17 +107,17 @@ {{if .IsJSON }} {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { return err } else { _, err := ctx.WriteString(form.Encode()) return err } - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} defer writer.Close() return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer); {{else -}} @@ -153,7 +153,7 @@ _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) return err {{end}}{{/* if .IsStreamingContentType */ -}} - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} } {{end}} diff --git a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl index 6806c0ea86..2d5c8ed47b 100644 --- a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl @@ -12,7 +12,7 @@ {{end -}} {{$multipleBodies := gt (len .Bodies) 1 -}} {{range .Bodies -}} - {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} + {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if .IsMultipart}}*multipart.Reader{{else if .IsSupported}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} {{end -}} } @@ -41,13 +41,13 @@ {{range .Contents}} {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} + {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (.IsMultipart) (.IsText)) -}} type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response {{else -}} type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response } {{end}} {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}} - type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + type {{$receiverTypeName}} {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} func (t {{$receiverTypeName}}) MarshalJSON() ([]byte, error) { @@ -60,7 +60,7 @@ {{- end}} {{else -}} type {{$receiverTypeName}} struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + Body {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{if $hasHeaders -}} Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders {{end -}} @@ -93,10 +93,10 @@ ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) {{end -}} {{end -}} - {{if eq .NameTag "Multipart" -}} + {{if .IsMultipart -}} writer := multipart.NewWriter(ctx.Response().BodyWriter()) {{end -}} - ctx.Response().Header.Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) + ctx.Response().Header.Set("Content-Type", {{if .IsMultipart}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) @@ -107,17 +107,17 @@ {{if .IsJSON }} {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { return err } else { _, err := ctx.WriteString(form.Encode()) return err } - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} defer writer.Close() return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer); {{else -}} @@ -153,7 +153,7 @@ _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) return err {{end}}{{/* if .IsStreamingContentType */ -}} - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} } {{end}} diff --git a/pkg/codegen/templates/strict/strict-fiber-v3.tmpl b/pkg/codegen/templates/strict/strict-fiber-v3.tmpl index d4f1542586..a670a797df 100644 --- a/pkg/codegen/templates/strict/strict-fiber-v3.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-v3.tmpl @@ -40,13 +40,13 @@ type strictHandler struct { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.Bind().Body(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) {{else -}} @@ -58,13 +58,13 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data := ctx.Request().Body() body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = bytes.NewReader(ctx.Request().Body()) - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-fiber.tmpl b/pkg/codegen/templates/strict/strict-fiber.tmpl index 2bd4e6bf7a..2a298666aa 100644 --- a/pkg/codegen/templates/strict/strict-fiber.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber.tmpl @@ -46,13 +46,13 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.BodyParser(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) {{else -}} @@ -64,7 +64,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data := ctx.Request().Body() {{if not .Required -}} if len(data) > 0 { @@ -76,7 +76,7 @@ type strictHandler struct { {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = bytes.NewReader(ctx.Request().Body()) - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-gin.tmpl b/pkg/codegen/templates/strict/strict-gin.tmpl index ed2e561a51..89342463d1 100644 --- a/pkg/codegen/templates/strict/strict-gin.tmpl +++ b/pkg/codegen/templates/strict/strict-gin.tmpl @@ -92,7 +92,7 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if err := ctx.Request.ParseForm(); err != nil { sh.options.RequestErrorHandlerFunc(ctx, err) return @@ -103,7 +103,7 @@ type strictHandler struct { return } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} if reader, err := ctx.Request.MultipartReader(); err != nil { sh.options.RequestErrorHandlerFunc(ctx, err) @@ -122,7 +122,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request.Body, boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data, err := io.ReadAll(ctx.Request.Body) if err != nil { sh.options.RequestErrorHandlerFunc(ctx, err) @@ -138,7 +138,7 @@ type strictHandler struct { {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request.Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-http.tmpl b/pkg/codegen/templates/strict/strict-http.tmpl index 8e6166cc99..5c54203093 100644 --- a/pkg/codegen/templates/strict/strict-http.tmpl +++ b/pkg/codegen/templates/strict/strict-http.tmpl @@ -74,7 +74,7 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if err := r.ParseForm(); err != nil { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) return @@ -85,7 +85,7 @@ type strictHandler struct { return } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} if reader, err := r.MultipartReader(); err != nil { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) @@ -104,7 +104,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(r.Body, boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data, err := io.ReadAll(r.Body) if err != nil { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) @@ -120,7 +120,7 @@ type strictHandler struct { {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = r.Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index 081f3fb1ae..95c2f12ece 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -12,7 +12,7 @@ {{end -}} {{$multipleBodies := gt (len .Bodies) 1 -}} {{range .Bodies -}} - {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} + {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if .IsMultipart}}*multipart.Reader{{else if .IsSupported}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} {{end -}} } @@ -41,13 +41,13 @@ {{range .Contents}} {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} + {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (.IsMultipart) (.IsText)) -}} type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response {{else -}} type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response } {{end}} {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}} - type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + type {{$receiverTypeName}} {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} func (t {{$receiverTypeName}}) MarshalJSON() ([]byte, error) { @@ -60,7 +60,7 @@ {{- end}} {{else -}} type {{$receiverTypeName}} struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + Body {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{if $hasHeaders -}} Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders {{end -}} @@ -80,7 +80,7 @@ {{end}} func (response {{$receiverTypeName}}) Visit{{$opid}}Response(w http.ResponseWriter) error { - {{if eq .NameTag "Multipart" -}} + {{if .IsMultipart -}} writer := multipart.NewWriter(w) {{end -}} {{$hasBodyVar := or ($hasHeaders) (not $fixedStatusCode) (not .IsSupported)}} @@ -107,7 +107,7 @@ w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) _, err := buf.WriteTo(w) return err - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil) if err != nil { return err @@ -130,7 +130,7 @@ _, err = w.Write([]byte(form.Encode())) return err {{else -}} - w.Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) + w.Header().Set("Content-Type", {{if .IsMultipart}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) @@ -151,10 +151,10 @@ {{end -}} w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) - {{if eq .NameTag "Text" -}} + {{if .IsText -}} _, err := w.Write([]byte({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} defer writer.Close() return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer); {{else -}} @@ -191,7 +191,7 @@ _, err := io.Copy(w, response.Body) return err {{end}}{{/* if .IsStreamingContentType */ -}} - {{end}}{{/* if eq .NameTag "Text" */ -}} + {{end}}{{/* if .IsText */ -}} {{end}}{{/* if .IsJSON */ -}} } {{end}} diff --git a/pkg/codegen/templates/strict/strict-iris-interface.tmpl b/pkg/codegen/templates/strict/strict-iris-interface.tmpl index 77e5c3488d..292d254e2f 100644 --- a/pkg/codegen/templates/strict/strict-iris-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-iris-interface.tmpl @@ -12,7 +12,7 @@ {{end -}} {{$multipleBodies := gt (len .Bodies) 1 -}} {{range .Bodies -}} - {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} + {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if .IsMultipart}}*multipart.Reader{{else if .IsSupported}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} {{end -}} } @@ -41,13 +41,13 @@ {{range .Contents}} {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} + {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (.IsMultipart) (.IsText)) -}} type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response {{else -}} type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response } {{end}} {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}} - type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + type {{$receiverTypeName}} {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} func (t {{$receiverTypeName}}) MarshalJSON() ([]byte, error) { @@ -60,7 +60,7 @@ {{- end}} {{else -}} type {{$receiverTypeName}} struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + Body {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{if $hasHeaders -}} Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders {{end -}} @@ -93,10 +93,10 @@ ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) {{end -}} {{end -}} - {{if eq .NameTag "Multipart" -}} + {{if .IsMultipart -}} writer := multipart.NewWriter(ctx.ResponseWriter()) {{end -}} - ctx.ResponseWriter().Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) + ctx.ResponseWriter().Header().Set("Content-Type", {{if .IsMultipart}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { ctx.ResponseWriter().Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) @@ -107,17 +107,17 @@ {{if .IsJSON -}} {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { return err } else { _, err := ctx.WriteString(form.Encode()) return err } - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} defer writer.Close() return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer); {{else -}} @@ -154,7 +154,7 @@ _, err := io.Copy(ctx.ResponseWriter(), response.Body) return err {{end}}{{/* if .IsStreamingContentType */ -}} - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} } {{end}} diff --git a/pkg/codegen/templates/strict/strict-iris.tmpl b/pkg/codegen/templates/strict/strict-iris.tmpl index 404db7e234..193c678787 100644 --- a/pkg/codegen/templates/strict/strict-iris.tmpl +++ b/pkg/codegen/templates/strict/strict-iris.tmpl @@ -47,7 +47,7 @@ type strictHandler struct { } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} if err := ctx.Request().ParseForm(); err != nil { ctx.StopWithError(http.StatusBadRequest, err) return @@ -58,7 +58,7 @@ type strictHandler struct { return } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} + {{else if .IsMultipart -}} {{if eq .ContentType "multipart/form-data" -}} if reader, err := ctx.Request().MultipartReader(); err == nil { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = reader @@ -77,7 +77,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request().Body, boundary) } {{end -}} - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} data, err := io.ReadAll(ctx.Request().Body) if err != nil { ctx.StopWithError(http.StatusBadRequest, err) @@ -93,7 +93,7 @@ type strictHandler struct { {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{end}}{{/* if .IsJSON */ -}} {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} diff --git a/pkg/codegen/templates/strict/strict-responses.tmpl b/pkg/codegen/templates/strict/strict-responses.tmpl index 77cb520a42..9d8685fd61 100644 --- a/pkg/codegen/templates/strict/strict-responses.tmpl +++ b/pkg/codegen/templates/strict/strict-responses.tmpl @@ -12,7 +12,7 @@ {{range .Contents -}} {{if and (not $hasHeaders) (.IsSupported) -}} - type {{$name}}{{.NameTagOrContentType}}Response {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if .Schema.IsRef}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + type {{$name}}{{.NameTagOrContentType}}Response {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if .Schema.IsRef}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} func (t {{$name}}{{.NameTagOrContentType}}Response) MarshalJSON() ([]byte, error) { @@ -25,7 +25,7 @@ {{- end}} {{else -}} type {{$name}}{{.NameTagOrContentType}}Response struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} + Body {{if .IsMultipart}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} {{if $hasHeaders -}} Headers {{$name}}ResponseHeaders diff --git a/pkg/codegen/templates/webhook-initiator.tmpl b/pkg/codegen/templates/webhook-initiator.tmpl index 032e3933ed..a2c842cb84 100644 --- a/pkg/codegen/templates/webhook-initiator.tmpl +++ b/pkg/codegen/templates/webhook-initiator.tmpl @@ -130,13 +130,13 @@ func New{{$opid}}WebhookRequest{{.Suffix}}(targetURL string{{if $hasParams}}, pa return nil, err } bodyReader = bytes.NewReader(buf) - {{else if eq .NameTag "Formdata" -}} + {{else if .IsFormdata -}} bodyStr, err := runtime.MarshalForm(body, nil) if err != nil { return nil, err } bodyReader = strings.NewReader(bodyStr.Encode()) - {{else if eq .NameTag "Text" -}} + {{else if .IsText -}} if stringer, ok := interface{}(body).(fmt.Stringer); ok { bodyReader = strings.NewReader(stringer.String()) } else {