diff --git a/internal/test/issue-vnd-json/config.yaml b/internal/test/issue-vnd-json/config.yaml new file mode 100644 index 000000000..0739b2b4f --- /dev/null +++ b/internal/test/issue-vnd-json/config.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=../../../configuration-schema.json +package: issuevndjson +output: schema.gen.go +generate: + models: true + client: true diff --git a/internal/test/issue-vnd-json/doc.go b/internal/test/issue-vnd-json/doc.go new file mode 100644 index 000000000..fc16b3991 --- /dev/null +++ b/internal/test/issue-vnd-json/doc.go @@ -0,0 +1,3 @@ +package issuevndjson + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issue-vnd-json/schema.gen.go b/internal/test/issue-vnd-json/schema.gen.go new file mode 100644 index 000000000..e8433e97e --- /dev/null +++ b/internal/test/issue-vnd-json/schema.gen.go @@ -0,0 +1,358 @@ +// Package issuevndjson 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 issuevndjson + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" +) + +// ErrorA defines model for error_a. +type ErrorA struct { + Message *string `json:"message,omitempty"` +} + +// ErrorB defines model for error_b. +type ErrorB struct { + Code *int `json:"code,omitempty"` +} + +// Item defines model for item. +type Item struct { + Id *string `json:"id,omitempty"` +} + +// CreateItem422ApplicationVndAPIPlusJSONResponseBody defines parameters for CreateItem. +type CreateItem422ApplicationVndAPIPlusJSONResponseBody struct { + union json.RawMessage +} + +// AsErrorA returns the union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody as a ErrorA +func (t CreateItem422ApplicationVndAPIPlusJSONResponseBody) AsErrorA() (ErrorA, error) { + var body ErrorA + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorA overwrites any union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody as the provided ErrorA +func (t *CreateItem422ApplicationVndAPIPlusJSONResponseBody) FromErrorA(v ErrorA) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorA performs a merge with any union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody, using the provided ErrorA +func (t *CreateItem422ApplicationVndAPIPlusJSONResponseBody) MergeErrorA(v ErrorA) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorB returns the union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody as a ErrorB +func (t CreateItem422ApplicationVndAPIPlusJSONResponseBody) AsErrorB() (ErrorB, error) { + var body ErrorB + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorB overwrites any union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody as the provided ErrorB +func (t *CreateItem422ApplicationVndAPIPlusJSONResponseBody) FromErrorB(v ErrorB) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorB performs a merge with any union data inside the CreateItem422ApplicationVndAPIPlusJSONResponseBody, using the provided ErrorB +func (t *CreateItem422ApplicationVndAPIPlusJSONResponseBody) MergeErrorB(v ErrorB) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t CreateItem422ApplicationVndAPIPlusJSONResponseBody) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *CreateItem422ApplicationVndAPIPlusJSONResponseBody) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// 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 { + + // CreateItem performs a POST /items (the `CreateItem` operationId) request. + CreateItem(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// CreateItem performs a POST /items (the `CreateItem` operationId) request. +func (c *Client) CreateItem(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateItemRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewCreateItemRequest constructs an http.Request for the CreateItem method +func NewCreateItemRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/items") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + + // CreateItemWithResponse performs a POST /items (the `CreateItem` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + CreateItemWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*CreateItemResponse, error) +} + +type CreateItemResponse struct { + Body []byte + HTTPResponse *http.Response + // ApplicationvndApiJSON200 the response for an HTTP 200 `application/vnd.api+json` response + ApplicationvndApiJSON200 *Item + // ApplicationvndApiJSON422 the response for an HTTP 422 `application/vnd.api+json` response + ApplicationvndApiJSON422 *CreateItem422ApplicationVndAPIPlusJSONResponseBody +} + +// GetApplicationvndApiJSON200 returns the response for an HTTP 200 `application/vnd.api+json` response +func (r CreateItemResponse) GetApplicationvndApiJSON200() *Item { + return r.ApplicationvndApiJSON200 +} + +// GetApplicationvndApiJSON422 returns the response for an HTTP 422 `application/vnd.api+json` response +func (r CreateItemResponse) GetApplicationvndApiJSON422() *CreateItem422ApplicationVndAPIPlusJSONResponseBody { + return r.ApplicationvndApiJSON422 +} + +// GetBody returns the raw response body bytes +func (r CreateItemResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r CreateItemResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateItemResponse) 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 CreateItemResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// CreateItemWithResponse performs a POST /items (the `CreateItem` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) CreateItemWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*CreateItemResponse, error) { + rsp, err := c.CreateItem(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateItemResponse(rsp) +} + +// ParseCreateItemResponse parses an HTTP response from a CreateItemWithResponse call +func ParseCreateItemResponse(rsp *http.Response) (*CreateItemResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateItemResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Item + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationvndApiJSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest CreateItem422ApplicationVndAPIPlusJSONResponseBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationvndApiJSON422 = &dest + + } + + return response, nil +} diff --git a/internal/test/issue-vnd-json/spec.yaml b/internal/test/issue-vnd-json/spec.yaml new file mode 100644 index 000000000..b55ecb7d8 --- /dev/null +++ b/internal/test/issue-vnd-json/spec.yaml @@ -0,0 +1,38 @@ +openapi: "3.0.0" +info: + title: rootly-go issue with + version: "1.0.0" +paths: + /items: + post: + operationId: createItem + responses: + "200": + content: + application/vnd.api+json: + schema: + $ref: "#/components/schemas/item" + "422": + content: + application/vnd.api+json: + schema: + anyOf: + - $ref: "#/components/schemas/error_a" + - $ref: "#/components/schemas/error_b" +components: + schemas: + item: + type: object + properties: + id: + type: string + error_a: + type: object + properties: + message: + type: string + error_b: + type: object + properties: + code: + type: integer diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 2bea1e4a8..cf219c2f2 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -558,7 +558,7 @@ func (o *OperationDefinition) GetResponseTypeDefinitions() ([]ResponseTypeDefini baseTypeName := fmt.Sprintf("%s%s", nameNormalizer(contentTypeName), nameNormalizer(responseName)) typeName = strings.ReplaceAll(baseTypeName, "Json", "JSON") - tag = strings.ReplaceAll(nameNormalizer(contentTypeName), "Json", "JSON") + tag = mediaTypeToCamelCase(contentTypeName) // YAML: case slices.Contains(contentTypesYAML, contentTypeName): typeName = fmt.Sprintf("YAML%s", nameNormalizer(responseName)) @@ -860,13 +860,13 @@ func (r ResponseContentDefinition) IsStreamingContentType() bool { } type ResponseHeaderDefinition struct { - Name string - GoName string - Schema Schema - Required bool - Nullable bool - Deprecated bool - DeprecationReason string + Name string + GoName string + Schema Schema + Required bool + Nullable bool + Deprecated bool + DeprecationReason string } // DeprecationComment returns a Go-style deprecation comment if the header is deprecated, otherwise returns an empty string.