From bce67cf35bd95d50eb51be7c14a4f6eb7fb99c09 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 13 Jul 2026 16:15:03 -0700 Subject: [PATCH] Generate shared path-level parameter helper types once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #2090 A parameter declared at the path-item level is inherited by every method on the path. Its helper types — the member types of an inline anyOf/oneOf, named from the parameter alone (Id0, Id1, ...) — were generated once per operation, so a path with more than one method declared them several times and the output failed to compile ("Id0 redeclared in this block"). Fix the root cause: mark path-item-level parameters as shared and skip their helper types when collecting an operation's type definitions, emitting them once for the path item (attributed to its first operation) instead. Applied to the regular-path, webhook, and callback operation processors. A shared parameter used by several methods on one path now just works. Also fix referencing a shared parameter from #/components/parameters, which was independently broken for anyOf: the component's union member types were named from an empty path (N0/N1) and never declared, while each referencing operation redeclared its own inline members. Now GenerateTypesForParameters roots the schema path at the parameter's type name and declares its helper types once, and a $ref parameter drops the inline schema it would otherwise redeclare. Referencing a component parameter is a clean way to share one. Two different path items that each declare a shared parameter of the same name still collide (both emit the same type once). The pre-generation validation pass detects this and rejects it, naming the paths and the fixes: define the parameter under #/components/parameters and $ref it, rename it with x-go-type-name or a spec overlay, or move it into each operation. Adds internal/test/parameters/shared_anyof covering a shared anyOf parameter across multiple methods on a regular path, a webhook, a callback, and a component parameter referenced from two paths; plus a spec_validation testdata case for the cross-path rejection and a multi-method valid.yaml case for the accepted shared case. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/parameters/shared_anyof/config.yaml | 8 + internal/test/parameters/shared_anyof/doc.go | 8 + .../shared_anyof/shared_anyof.gen.go | 1639 +++++++++++++++++ .../shared_anyof/shared_anyof_test.go | 34 + .../test/parameters/shared_anyof/spec.yaml | 63 + .../spec_validation/spec_validation_test.go | 2 + .../shared_path_param_type_collision.yaml | 27 + .../test/spec_validation/testdata/valid.yaml | 19 + pkg/codegen/codegen.go | 19 +- pkg/codegen/operations.go | 85 +- pkg/codegen/validate_spec.go | 93 +- 11 files changed, 1983 insertions(+), 14 deletions(-) create mode 100644 internal/test/parameters/shared_anyof/config.yaml create mode 100644 internal/test/parameters/shared_anyof/doc.go create mode 100644 internal/test/parameters/shared_anyof/shared_anyof.gen.go create mode 100644 internal/test/parameters/shared_anyof/shared_anyof_test.go create mode 100644 internal/test/parameters/shared_anyof/spec.yaml create mode 100644 internal/test/spec_validation/testdata/shared_path_param_type_collision.yaml diff --git a/internal/test/parameters/shared_anyof/config.yaml b/internal/test/parameters/shared_anyof/config.yaml new file mode 100644 index 000000000..3b67ae177 --- /dev/null +++ b/internal/test/parameters/shared_anyof/config.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +# From issue-2090 +package: sharedanyof +generate: + echo-server: true + client: true + models: true +output: shared_anyof.gen.go diff --git a/internal/test/parameters/shared_anyof/doc.go b/internal/test/parameters/shared_anyof/doc.go new file mode 100644 index 000000000..b460b9fb8 --- /dev/null +++ b/internal/test/parameters/shared_anyof/doc.go @@ -0,0 +1,8 @@ +// Package sharedanyof is a regression test for issue-2090: a path-level anyOf +// parameter shared by multiple methods on a path (and on webhooks/callbacks) +// must have its union member types declared once for the path item, not once +// per method. The committed generated file compiling as part of the test +// module is the guard. +package sharedanyof + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/parameters/shared_anyof/shared_anyof.gen.go b/internal/test/parameters/shared_anyof/shared_anyof.gen.go new file mode 100644 index 000000000..75ed74d7a --- /dev/null +++ b/internal/test/parameters/shared_anyof/shared_anyof.gen.go @@ -0,0 +1,1639 @@ +// Package sharedanyof 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 sharedanyof + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/labstack/echo/v4" + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// WidgetId defines model for WidgetId. +type WidgetId struct { + union json.RawMessage +} + +// WidgetId0 defines model for WidgetId.0. +type WidgetId0 = openapi_types.UUID + +// WidgetId1 defines model for WidgetId.1. +type WidgetId1 = string + +// Id0 defines parameters for DeleteResource. +type Id0 = openapi_types.UUID + +// Id1 defines parameters for DeleteResource. +type Id1 = string + +// OnWebhookGetParams defines parameters for OnWebhookGet. +type OnWebhookGetParams struct { + Whid struct { + union json.RawMessage + } `form:"whid" json:"whid"` +} + +// Whid0 defines parameters for OnWebhookGet. +type Whid0 = openapi_types.UUID + +// Whid1 defines parameters for OnWebhookGet. +type Whid1 = string + +// OnWebhookPostParams defines parameters for OnWebhookPost. +type OnWebhookPostParams struct { + Whid struct { + union json.RawMessage + } `form:"whid" json:"whid"` +} + +// OnResourceGetParams defines parameters for OnResourceGet. +type OnResourceGetParams struct { + Cbid struct { + union json.RawMessage + } `form:"cbid" json:"cbid"` +} + +// Cbid0 defines parameters for OnResourceGet. +type Cbid0 = openapi_types.UUID + +// Cbid1 defines parameters for OnResourceGet. +type Cbid1 = string + +// OnResourcePostParams defines parameters for OnResourcePost. +type OnResourcePostParams struct { + Cbid struct { + union json.RawMessage + } `form:"cbid" json:"cbid"` +} + +// AsWidgetId0 returns the union data inside the WidgetId as a WidgetId0 +func (t WidgetId) AsWidgetId0() (WidgetId0, error) { + var body WidgetId0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromWidgetId0 overwrites any union data inside the WidgetId as the provided WidgetId0 +func (t *WidgetId) FromWidgetId0(v WidgetId0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeWidgetId0 performs a merge with any union data inside the WidgetId, using the provided WidgetId0 +func (t *WidgetId) MergeWidgetId0(v WidgetId0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsWidgetId1 returns the union data inside the WidgetId as a WidgetId1 +func (t WidgetId) AsWidgetId1() (WidgetId1, error) { + var body WidgetId1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromWidgetId1 overwrites any union data inside the WidgetId as the provided WidgetId1 +func (t *WidgetId) FromWidgetId1(v WidgetId1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeWidgetId1 performs a merge with any union data inside the WidgetId, using the provided WidgetId1 +func (t *WidgetId) MergeWidgetId1(v WidgetId1) 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 WidgetId) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *WidgetId) 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 { + + // GetGadget performs a GET /gadgets/{wid} (the `GetGadget` operationId) request. + GetGadget(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // Subscribe performs a POST /subscribe (the `Subscribe` operationId) request. + Subscribe(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteResource performs a DELETE /v1/resource/{id} (the `DeleteResource` operationId) request. + DeleteResource(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetResource performs a GET /v1/resource/{id} (the `GetResource` operationId) request. + GetResource(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostResource performs a POST /v1/resource/{id} (the `PostResource` operationId) request. + PostResource(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetWidget performs a GET /widgets/{wid} (the `GetWidget` operationId) request. + GetWidget(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// GetGadget performs a GET /gadgets/{wid} (the `GetGadget` operationId) request. +func (c *Client) GetGadget(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetGadgetRequest(c.Server, wid) + 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) +} + +// Subscribe performs a POST /subscribe (the `Subscribe` operationId) request. +func (c *Client) Subscribe(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSubscribeRequest(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) +} + +// DeleteResource performs a DELETE /v1/resource/{id} (the `DeleteResource` operationId) request. +func (c *Client) DeleteResource(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteResourceRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// GetResource performs a GET /v1/resource/{id} (the `GetResource` operationId) request. +func (c *Client) GetResource(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetResourceRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// PostResource performs a POST /v1/resource/{id} (the `PostResource` operationId) request. +func (c *Client) PostResource(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostResourceRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// GetWidget performs a GET /widgets/{wid} (the `GetWidget` operationId) request. +func (c *Client) GetWidget(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetWidgetRequest(c.Server, wid) + 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) +} + +// NewGetGadgetRequest constructs an http.Request for the GetGadget method +func NewGetGadgetRequest(server string, wid WidgetId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "wid", wid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/gadgets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewSubscribeRequest constructs an http.Request for the Subscribe method +func NewSubscribeRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/subscribe") + 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 +} + +// NewDeleteResourceRequest constructs an http.Request for the DeleteResource method +func NewDeleteResourceRequest(server string, id struct { + union json.RawMessage +}) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/resource/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetResourceRequest constructs an http.Request for the GetResource method +func NewGetResourceRequest(server string, id struct { + union json.RawMessage +}) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/resource/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostResourceRequest constructs an http.Request for the PostResource method +func NewPostResourceRequest(server string, id struct { + union json.RawMessage +}) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/resource/%s", pathParam0) + 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 +} + +// NewGetWidgetRequest constructs an http.Request for the GetWidget method +func NewGetWidgetRequest(server string, wid WidgetId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "wid", wid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/widgets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + + // GetGadgetWithResponse performs a GET /gadgets/{wid} (the `GetGadget` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetGadgetWithResponse(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*GetGadgetResponse, error) + + // SubscribeWithResponse performs a POST /subscribe (the `Subscribe` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + SubscribeWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*SubscribeResponse, error) + + // DeleteResourceWithResponse performs a DELETE /v1/resource/{id} (the `DeleteResource` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + DeleteResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*DeleteResourceResponse, error) + + // GetResourceWithResponse performs a GET /v1/resource/{id} (the `GetResource` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*GetResourceResponse, error) + + // PostResourceWithResponse performs a POST /v1/resource/{id} (the `PostResource` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + PostResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*PostResourceResponse, error) + + // GetWidgetWithResponse performs a GET /widgets/{wid} (the `GetWidget` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetWidgetWithResponse(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*GetWidgetResponse, error) +} + +type GetGadgetResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r GetGadgetResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetGadgetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetGadgetResponse) 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 GetGadgetResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type SubscribeResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r SubscribeResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r SubscribeResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SubscribeResponse) 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 SubscribeResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type DeleteResourceResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r DeleteResourceResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r DeleteResourceResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteResourceResponse) 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 DeleteResourceResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetResourceResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r GetResourceResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetResourceResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetResourceResponse) 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 GetResourceResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type PostResourceResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r PostResourceResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r PostResourceResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostResourceResponse) 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 PostResourceResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetWidgetResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r GetWidgetResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetWidgetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetWidgetResponse) 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 GetWidgetResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// GetGadgetWithResponse performs a GET /gadgets/{wid} (the `GetGadget` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetGadgetWithResponse(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*GetGadgetResponse, error) { + rsp, err := c.GetGadget(ctx, wid, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetGadgetResponse(rsp) +} + +// SubscribeWithResponse performs a POST /subscribe (the `Subscribe` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) SubscribeWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*SubscribeResponse, error) { + rsp, err := c.Subscribe(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseSubscribeResponse(rsp) +} + +// DeleteResourceWithResponse performs a DELETE /v1/resource/{id} (the `DeleteResource` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) DeleteResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*DeleteResourceResponse, error) { + rsp, err := c.DeleteResource(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteResourceResponse(rsp) +} + +// GetResourceWithResponse performs a GET /v1/resource/{id} (the `GetResource` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*GetResourceResponse, error) { + rsp, err := c.GetResource(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetResourceResponse(rsp) +} + +// PostResourceWithResponse performs a POST /v1/resource/{id} (the `PostResource` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) PostResourceWithResponse(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*PostResourceResponse, error) { + rsp, err := c.PostResource(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostResourceResponse(rsp) +} + +// GetWidgetWithResponse performs a GET /widgets/{wid} (the `GetWidget` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetWidgetWithResponse(ctx context.Context, wid WidgetId, reqEditors ...RequestEditorFn) (*GetWidgetResponse, error) { + rsp, err := c.GetWidget(ctx, wid, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetWidgetResponse(rsp) +} + +// ParseGetGadgetResponse parses an HTTP response from a GetGadgetWithResponse call +func ParseGetGadgetResponse(rsp *http.Response) (*GetGadgetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetGadgetResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseSubscribeResponse parses an HTTP response from a SubscribeWithResponse call +func ParseSubscribeResponse(rsp *http.Response) (*SubscribeResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SubscribeResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseDeleteResourceResponse parses an HTTP response from a DeleteResourceWithResponse call +func ParseDeleteResourceResponse(rsp *http.Response) (*DeleteResourceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteResourceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetResourceResponse parses an HTTP response from a GetResourceWithResponse call +func ParseGetResourceResponse(rsp *http.Response) (*GetResourceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetResourceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParsePostResourceResponse parses an HTTP response from a PostResourceWithResponse call +func ParsePostResourceResponse(rsp *http.Response) (*PostResourceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostResourceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetWidgetResponse parses an HTTP response from a GetWidgetWithResponse call +func ParseGetWidgetResponse(rsp *http.Response) (*GetWidgetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetWidgetResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// WebhookInitiator sends OpenAPI 3.1 webhook requests to target URLs. +// Modeled on the generated Client, but with no stored Server -- the full +// target URL is provided per-call by the caller (typically discovered +// from a subscription registration). +type WebhookInitiator struct { + // 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 +} + +// WebhookInitiatorOption allows setting custom parameters during construction. +type WebhookInitiatorOption func(*WebhookInitiator) error + +// NewWebhookInitiator creates a new WebhookInitiator with reasonable defaults. +func NewWebhookInitiator(opts ...WebhookInitiatorOption) (*WebhookInitiator, error) { + initiator := WebhookInitiator{} + for _, o := range opts { + if err := o(&initiator); err != nil { + return nil, err + } + } + if initiator.Client == nil { + initiator.Client = &http.Client{} + } + return &initiator, nil +} + +// WithWebhookHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithWebhookHTTPClient(doer HttpRequestDoer) WebhookInitiatorOption { + return func(p *WebhookInitiator) error { + p.Client = doer + return nil + } +} + +// WithWebhookRequestEditorFn allows setting up a callback function, which +// will be called right before sending the webhook request. This can be +// used to mutate the request, e.g. to add signature headers. +func WithWebhookRequestEditorFn(fn RequestEditorFn) WebhookInitiatorOption { + return func(p *WebhookInitiator) error { + p.RequestEditors = append(p.RequestEditors, fn) + return nil + } +} + +func (p *WebhookInitiator) applyWebhookEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range p.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 +} + +// WebhookInitiatorInterface is the interface specification for the webhook initiator. +type WebhookInitiatorInterface interface { + // OnWebhookGet fires the resourceWebhook webhook + OnWebhookGet(ctx context.Context, targetURL string, params *OnWebhookGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // OnWebhookPost fires the resourceWebhook webhook + OnWebhookPost(ctx context.Context, targetURL string, params *OnWebhookPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (p *WebhookInitiator) OnWebhookGet(ctx context.Context, targetURL string, params *OnWebhookGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnWebhookGetWebhookRequest(targetURL, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyWebhookEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +func (p *WebhookInitiator) OnWebhookPost(ctx context.Context, targetURL string, params *OnWebhookPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnWebhookPostWebhookRequest(targetURL, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyWebhookEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// NewOnWebhookGetWebhookRequest builds a GET request for the resourceWebhook webhook +func NewOnWebhookGetWebhookRequest(targetURL string, params *OnWebhookGetParams) (*http.Request, error) { + var err error + _ = err + + reqURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := reqURL.Query() + var rawQueryFragments []string + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "whid", params.Whid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + reqURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewOnWebhookPostWebhookRequest builds a POST request for the resourceWebhook webhook +func NewOnWebhookPostWebhookRequest(targetURL string, params *OnWebhookPostParams) (*http.Request, error) { + var err error + _ = err + + reqURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := reqURL.Query() + var rawQueryFragments []string + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "whid", params.Whid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + reqURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodPost, reqURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// CallbackInitiator sends OpenAPI callback requests to target URLs. +// Modeled on the generated Client, but with no stored Server -- the full +// target URL is provided per-call by the caller (typically discovered +// from a callback expression on the parent operation's request body, +// e.g. `{$request.body#/callbackUrl}`). +type CallbackInitiator struct { + // 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 +} + +// CallbackInitiatorOption allows setting custom parameters during construction. +type CallbackInitiatorOption func(*CallbackInitiator) error + +// NewCallbackInitiator creates a new CallbackInitiator with reasonable defaults. +func NewCallbackInitiator(opts ...CallbackInitiatorOption) (*CallbackInitiator, error) { + initiator := CallbackInitiator{} + for _, o := range opts { + if err := o(&initiator); err != nil { + return nil, err + } + } + if initiator.Client == nil { + initiator.Client = &http.Client{} + } + return &initiator, nil +} + +// WithCallbackHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithCallbackHTTPClient(doer HttpRequestDoer) CallbackInitiatorOption { + return func(p *CallbackInitiator) error { + p.Client = doer + return nil + } +} + +// WithCallbackRequestEditorFn allows setting up a callback function, which +// will be called right before sending the callback request. This can be +// used to mutate the request, e.g. to add signature headers. +func WithCallbackRequestEditorFn(fn RequestEditorFn) CallbackInitiatorOption { + return func(p *CallbackInitiator) error { + p.RequestEditors = append(p.RequestEditors, fn) + return nil + } +} + +func (p *CallbackInitiator) applyCallbackEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range p.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 +} + +// CallbackInitiatorInterface is the interface specification for the callback initiator. +type CallbackInitiatorInterface interface { + // OnResourceGet fires the resourceEvent callback + OnResourceGet(ctx context.Context, targetURL string, params *OnResourceGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // OnResourcePost fires the resourceEvent callback + OnResourcePost(ctx context.Context, targetURL string, params *OnResourcePostParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (p *CallbackInitiator) OnResourceGet(ctx context.Context, targetURL string, params *OnResourceGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnResourceGetCallbackRequest(targetURL, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyCallbackEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +func (p *CallbackInitiator) OnResourcePost(ctx context.Context, targetURL string, params *OnResourcePostParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnResourcePostCallbackRequest(targetURL, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyCallbackEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// NewOnResourceGetCallbackRequest builds a GET request for the resourceEvent callback +func NewOnResourceGetCallbackRequest(targetURL string, params *OnResourceGetParams) (*http.Request, error) { + var err error + _ = err + + reqURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := reqURL.Query() + var rawQueryFragments []string + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cbid", params.Cbid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + reqURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewOnResourcePostCallbackRequest builds a POST request for the resourceEvent callback +func NewOnResourcePostCallbackRequest(targetURL string, params *OnResourcePostParams) (*http.Request, error) { + var err error + _ = err + + reqURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := reqURL.Query() + var rawQueryFragments []string + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cbid", params.Cbid, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + reqURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodPost, reqURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /gadgets/{wid}) + GetGadget(ctx echo.Context, wid WidgetId) error + + // (POST /subscribe) + Subscribe(ctx echo.Context) error + + // (DELETE /v1/resource/{id}) + DeleteResource(ctx echo.Context, id struct { + union json.RawMessage + }) error + + // (GET /v1/resource/{id}) + GetResource(ctx echo.Context, id struct { + union json.RawMessage + }) error + + // (POST /v1/resource/{id}) + PostResource(ctx echo.Context, id struct { + union json.RawMessage + }) error + + // (GET /widgets/{wid}) + GetWidget(ctx echo.Context, wid WidgetId) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// GetGadget converts echo context to params. +func (w *ServerInterfaceWrapper) GetGadget(ctx echo.Context) error { + var err error + // ------------- Path parameter "wid" ------------- + var wid WidgetId + + err = runtime.BindStyledParameterWithOptions("simple", "wid", ctx.Param("wid"), &wid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "", Format: "", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter wid: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetGadget(ctx, wid) + return err +} + +// Subscribe converts echo context to params. +func (w *ServerInterfaceWrapper) Subscribe(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.Subscribe(ctx) + return err +} + +// DeleteResource converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteResource(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id struct { + union json.RawMessage + } + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "", Format: "", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteResource(ctx, id) + return err +} + +// GetResource converts echo context to params. +func (w *ServerInterfaceWrapper) GetResource(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id struct { + union json.RawMessage + } + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "", Format: "", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetResource(ctx, id) + return err +} + +// PostResource converts echo context to params. +func (w *ServerInterfaceWrapper) PostResource(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id struct { + union json.RawMessage + } + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "", Format: "", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostResource(ctx, id) + return err +} + +// GetWidget converts echo context to params. +func (w *ServerInterfaceWrapper) GetWidget(ctx echo.Context) error { + var err error + // ------------- Path parameter "wid" ------------- + var wid WidgetId + + err = runtime.BindStyledParameterWithOptions("simple", "wid", ctx.Param("wid"), &wid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "", Format: "", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter wid: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetWidget(ctx, wid) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlersOptions configures RegisterHandlersWithOptions. +type RegisterHandlersOptions struct { + // BaseURL is prepended to every registered path so the API can be served + // under a prefix. + BaseURL string + // OperationMiddlewares lets the caller attach per-operation middleware at + // registration time. The map key is the OpenAPI `operationId` value as it + // appears in the spec (the raw, un-normalized form). Operations that have + // no entry are registered with no extra middleware. A nil map disables + // per-operation middleware entirely. + OperationMiddlewares map[string][]echo.MiddlewareFunc +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithOptions(router, si, RegisterHandlersOptions{}) +} + +// RegisterHandlersWithBaseURL registers handlers and prepends BaseURL to the +// paths so the API can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + RegisterHandlersWithOptions(router, si, RegisterHandlersOptions{BaseURL: baseURL}) +} + +// RegisterHandlersWithOptions registers handlers using the supplied options, +// including any per-operation middleware. +func RegisterHandlersWithOptions(router EchoRouter, si ServerInterface, options RegisterHandlersOptions) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.DELETE(options.BaseURL+"/v1/resource/:id", wrapper.DeleteResource, options.OperationMiddlewares["deleteResource"]...) + router.GET(options.BaseURL+"/v1/resource/:id", wrapper.GetResource, options.OperationMiddlewares["getResource"]...) + router.POST(options.BaseURL+"/v1/resource/:id", wrapper.PostResource, options.OperationMiddlewares["postResource"]...) + router.POST(options.BaseURL+"/subscribe", wrapper.Subscribe, options.OperationMiddlewares["subscribe"]...) + router.GET(options.BaseURL+"/widgets/:wid", wrapper.GetWidget, options.OperationMiddlewares["getWidget"]...) + router.GET(options.BaseURL+"/gadgets/:wid", wrapper.GetGadget, options.OperationMiddlewares["getGadget"]...) + +} + +// WebhookReceiverInterface represents handlers for receiving inbound +// webhook requests. Each webhook becomes a Handle*Webhook +// method that the implementation fills in. The caller mounts the per- +// webhook echo.HandlerFunc returned by {Op}WebhookHandler at +// whatever URL path they advertise to senders. +type WebhookReceiverInterface interface { + + // HandleOnWebhookGetWebhook handles the GET webhook for resourceWebhook. + HandleOnWebhookGetWebhook(ctx echo.Context, params OnWebhookGetParams) error + + // HandleOnWebhookPostWebhook handles the POST webhook for resourceWebhook. + HandleOnWebhookPostWebhook(ctx echo.Context, params OnWebhookPostParams) error +} + +// OnWebhookGetWebhookHandler returns the echo.HandlerFunc for the resourceWebhook webhook. +// Mount this at the URL path advertised to webhook senders. Errors +// during parameter binding are returned via echo.NewHTTPError so echo's +// error-handling chain reports them as 400. Middlewares are applied in +// the order provided -- the last argument becomes the outermost wrapper. +func OnWebhookGetWebhookHandler(si WebhookReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { + h := echo.HandlerFunc(func(ctx echo.Context) error { + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context. + var params OnWebhookGetParams + + // ------------- Required query parameter "whid" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "whid", ctx.QueryParams(), ¶ms.Whid, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter whid: %s", err)) + } + + return si.HandleOnWebhookGetWebhook(ctx, params) + }) + for _, mw := range middlewares { + h = mw(h) + } + return h +} + +// OnWebhookPostWebhookHandler returns the echo.HandlerFunc for the resourceWebhook webhook. +// Mount this at the URL path advertised to webhook senders. Errors +// during parameter binding are returned via echo.NewHTTPError so echo's +// error-handling chain reports them as 400. Middlewares are applied in +// the order provided -- the last argument becomes the outermost wrapper. +func OnWebhookPostWebhookHandler(si WebhookReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { + h := echo.HandlerFunc(func(ctx echo.Context) error { + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context. + var params OnWebhookPostParams + + // ------------- Required query parameter "whid" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "whid", ctx.QueryParams(), ¶ms.Whid, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter whid: %s", err)) + } + + return si.HandleOnWebhookPostWebhook(ctx, params) + }) + for _, mw := range middlewares { + h = mw(h) + } + return h +} + +// CallbackReceiverInterface represents handlers for receiving inbound +// callback requests. Each callback becomes a Handle*Callback +// method that the implementation fills in. The caller mounts the per- +// callback echo.HandlerFunc returned by {Op}CallbackHandler at +// whatever URL path they advertise to senders. +type CallbackReceiverInterface interface { + + // HandleOnResourceGetCallback handles the GET callback for resourceEvent. + HandleOnResourceGetCallback(ctx echo.Context, params OnResourceGetParams) error + + // HandleOnResourcePostCallback handles the POST callback for resourceEvent. + HandleOnResourcePostCallback(ctx echo.Context, params OnResourcePostParams) error +} + +// OnResourceGetCallbackHandler returns the echo.HandlerFunc for the resourceEvent callback. +// Mount this at the URL path advertised to callback senders. Errors +// during parameter binding are returned via echo.NewHTTPError so echo's +// error-handling chain reports them as 400. Middlewares are applied in +// the order provided -- the last argument becomes the outermost wrapper. +func OnResourceGetCallbackHandler(si CallbackReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { + h := echo.HandlerFunc(func(ctx echo.Context) error { + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context. + var params OnResourceGetParams + + // ------------- Required query parameter "cbid" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "cbid", ctx.QueryParams(), ¶ms.Cbid, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter cbid: %s", err)) + } + + return si.HandleOnResourceGetCallback(ctx, params) + }) + for _, mw := range middlewares { + h = mw(h) + } + return h +} + +// OnResourcePostCallbackHandler returns the echo.HandlerFunc for the resourceEvent callback. +// Mount this at the URL path advertised to callback senders. Errors +// during parameter binding are returned via echo.NewHTTPError so echo's +// error-handling chain reports them as 400. Middlewares are applied in +// the order provided -- the last argument becomes the outermost wrapper. +func OnResourcePostCallbackHandler(si CallbackReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { + h := echo.HandlerFunc(func(ctx echo.Context) error { + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context. + var params OnResourcePostParams + + // ------------- Required query parameter "cbid" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "cbid", ctx.QueryParams(), ¶ms.Cbid, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter cbid: %s", err)) + } + + return si.HandleOnResourcePostCallback(ctx, params) + }) + for _, mw := range middlewares { + h = mw(h) + } + return h +} diff --git a/internal/test/parameters/shared_anyof/shared_anyof_test.go b/internal/test/parameters/shared_anyof/shared_anyof_test.go new file mode 100644 index 000000000..b11be615b --- /dev/null +++ b/internal/test/parameters/shared_anyof/shared_anyof_test.go @@ -0,0 +1,34 @@ +package sharedanyof + +import ( + "testing" + + openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSharedAnyOfParamTypesDeclaredOnce is the issue-2090 regression guard: a +// path-level anyOf parameter shared by several methods, and a component +// parameter referenced from several paths, each have their union member types +// declared exactly once. The blank declarations below only build because each +// type exists (and is not redeclared); the generated package failing to build +// as part of the test module would also catch a regression. +func TestSharedAnyOfParamTypesDeclaredOnce(t *testing.T) { + var ( + _ Id0 // path parameter, shared by get/post/delete + _ Id1 // + _ Whid0 // webhook query parameter + _ Cbid0 // callback query parameter + _ WidgetId0 // component parameter, $ref'd from two paths + _ WidgetId1 // + ) + + // The component parameter's union type carries its accessors once. + u := openapi_types.UUID{} + var w WidgetId + require.NoError(t, w.FromWidgetId0(u)) + got, err := w.AsWidgetId0() + require.NoError(t, err) + assert.Equal(t, u, got) +} diff --git a/internal/test/parameters/shared_anyof/spec.yaml b/internal/test/parameters/shared_anyof/spec.yaml new file mode 100644 index 000000000..62ef86497 --- /dev/null +++ b/internal/test/parameters/shared_anyof/spec.yaml @@ -0,0 +1,63 @@ +openapi: 3.1.0 +info: + title: shared-anyof-param + version: "1.0.0" +# Path-level anyOf parameters shared across multiple methods. The union member +# types (Id0/Id1, ...) are declared once for the path item rather than once per +# method, so this compiles (issue #2090). Distinct parameter names across the +# regular path, webhook, and callback avoid a cross-path collision. +paths: + /v1/resource/{id}: + parameters: + - name: id + in: path + required: true + schema: + anyOf: + - {type: string, format: uuid} + - {type: string, pattern: '^[a-z0-9_-]+$'} + get: {operationId: getResource, responses: {'200': {description: ok}}} + post: {operationId: postResource, responses: {'200': {description: ok}}} + delete: {operationId: deleteResource, responses: {'200': {description: ok}}} + /subscribe: + post: + operationId: subscribe + responses: {'200': {description: ok}} + callbacks: + resourceEvent: + '{$request.body#/callbackUrl}': + parameters: + - name: cbid + in: query + required: true + schema: + anyOf: [{type: string, format: uuid}, {type: string, pattern: '^[a-z0-9_-]+$'}] + post: {operationId: onResourcePost, responses: {'200': {description: ok}}} + get: {operationId: onResourceGet, responses: {'200': {description: ok}}} + # A component parameter with an anyOf schema, referenced from two different + # paths: its union member types are declared once with the component, so the + # two references do not collide (issue #2090). + /widgets/{wid}: + parameters: [{$ref: '#/components/parameters/WidgetId'}] + get: {operationId: getWidget, responses: {'200': {description: ok}}} + /gadgets/{wid}: + parameters: [{$ref: '#/components/parameters/WidgetId'}] + get: {operationId: getGadget, responses: {'200': {description: ok}}} +components: + parameters: + WidgetId: + name: wid + in: path + required: true + schema: + anyOf: [{type: string, format: uuid}, {type: string, pattern: '^[a-z0-9_-]+$'}] +webhooks: + resourceWebhook: + parameters: + - name: whid + in: query + required: true + schema: + anyOf: [{type: string, format: uuid}, {type: string, pattern: '^[a-z0-9_-]+$'}] + post: {operationId: onWebhookPost, responses: {'200': {description: ok}}} + get: {operationId: onWebhookGet, responses: {'200': {description: ok}}} diff --git a/internal/test/spec_validation/spec_validation_test.go b/internal/test/spec_validation/spec_validation_test.go index 3f5441639..f355d3192 100644 --- a/internal/test/spec_validation/spec_validation_test.go +++ b/internal/test/spec_validation/spec_validation_test.go @@ -31,6 +31,8 @@ func TestValidateSpecRejects(t *testing.T) { "x_go_type_semicolon.yaml": "x-go-type", "ref_sibling_x_go_type.yaml": "x-go-type", "security_scope_quote.yaml": "scope", + + "shared_path_param_type_collision.yaml": "path-level parameter", } for file, want := range cases { diff --git a/internal/test/spec_validation/testdata/shared_path_param_type_collision.yaml b/internal/test/spec_validation/testdata/shared_path_param_type_collision.yaml new file mode 100644 index 000000000..8392c683f --- /dev/null +++ b/internal/test/spec_validation/testdata/shared_path_param_type_collision.yaml @@ -0,0 +1,27 @@ +openapi: "3.0.0" +info: {title: t, version: "1.0.0"} +# Two different path items each declare a path-level anyOf parameter named +# "id". Each path declares the union member types (Id0, Id1) once, so across +# the two paths they are declared twice and collide (issue #2090). ValidateSpec +# should reject this before code generation. +paths: + /a/{id}: + parameters: + - name: id + in: path + required: true + schema: + anyOf: [{type: string, format: uuid}, {type: string, pattern: '^[a-z]+$'}] + get: + operationId: getA + responses: {'200': {description: ok}} + /b/{id}: + parameters: + - name: id + in: path + required: true + schema: + anyOf: [{type: string, format: uuid}, {type: string, pattern: '^[a-z]+$'}] + get: + operationId: getB + responses: {'200': {description: ok}} diff --git a/internal/test/spec_validation/testdata/valid.yaml b/internal/test/spec_validation/testdata/valid.yaml index 8724b4a0d..442a57fcb 100644 --- a/internal/test/spec_validation/testdata/valid.yaml +++ b/internal/test/spec_validation/testdata/valid.yaml @@ -10,6 +10,25 @@ paths: schema: {$ref: '#/components/schemas/Thing'} responses: '200': {description: ok} + # A path-level anyOf parameter shared by several methods on ONE path is + # declared once for the path item, so it is accepted (no collision). + /resource/{id}: + parameters: + - name: id + in: path + required: true + schema: + anyOf: + - {type: string, format: uuid} + - {type: string, pattern: '^[a-z]+$'} + get: + operationId: getResource + responses: + '200': {description: ok} + delete: + operationId: deleteResource + responses: + '200': {description: ok} components: schemas: Thing: diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 90ced55a3..54dee5e99 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -1144,11 +1144,6 @@ func GenerateTypesForParameters(t *template.Template, params map[string]*openapi for _, paramName := range SortedMapKeys(params) { paramOrRef := params[paramName] - goType, err := paramToGoType(paramOrRef.Value, nil) - if err != nil { - return nil, fmt.Errorf("error generating Go type for schema in parameter %s: %w", paramName, err) - } - goTypeName, err := renameParameter(paramName, paramOrRef) if err != nil { return nil, fmt.Errorf("error making name for components/parameters/%s: %w", paramName, err) @@ -1158,6 +1153,16 @@ func GenerateTypesForParameters(t *template.Template, params map[string]*openapi goTypeName = resolved } + // Root the schema path at the parameter's Go type name so any helper + // types the schema needs (e.g. anyOf union members) are named after the + // parameter — GoTypeName0, GoTypeName1 — matching the union accessors + // generated on the parameter type, rather than an unrelated + // numeric-derived name (issue #2090). + goType, err := paramToGoType(paramOrRef.Value, []string{goTypeName}) + if err != nil { + return nil, fmt.Errorf("error generating Go type for schema in parameter %s: %w", paramName, err) + } + typeDef := TypeDefinition{ JsonName: paramName, Schema: goType, @@ -1174,6 +1179,10 @@ func GenerateTypesForParameters(t *template.Template, params map[string]*openapi } types = append(types, typeDef) + // Declare the parameter's helper types (union members, ...) once here, + // with the component, so operations that reference the parameter do not + // each redeclare them (issue #2090). + types = append(types, goType.AdditionalTypes...) } return types, nil } diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 937012ef5..6d4f65d3d 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -36,6 +36,12 @@ type ParameterDefinition struct { Required bool // Is this a required parameter? Spec *openapi3.Parameter Schema Schema + + // Shared is true for a parameter declared at the path-item level, which + // is inherited by every method on the path. Its helper types are + // generated once for the path item rather than once per operation, so + // operation type collection skips them (issue #2090). + Shared bool } // TypeDef is here as an adapter after a large refactoring so that I don't @@ -284,7 +290,14 @@ func DescribeParameters(params openapi3.Parameters, path []string) ([]ParameterD return nil, fmt.Errorf("error dereferencing (%s) for param (%s): %s", paramOrRef.Ref, param.Name, err) } + // The parameter references a component parameter, whose type and + // any helper types (e.g. anyOf union members) are declared once + // with the component. Point at it and drop the inline schema this + // operation would otherwise redeclare per operation (issue #2090). pd.Schema.GoType = goType + pd.Schema.RefType = goType + pd.Schema.AdditionalTypes = nil + pd.Schema.UnionElements = nil } outParams = append(outParams, pd) } @@ -351,11 +364,11 @@ type OperationDefinition struct { // appear in the spec rather than sorted (issue #1887). Zero when the // source location is unavailable (e.g. a programmatically-built spec), // in which case route registration falls back to the default order. - SpecOrder int - Spec *openapi3.Operation - IsAlias bool // True when this path is a $ref alias of another path item - AliasTarget string // When IsAlias is true, this is the OperationId of the canonical operation (for route registration to reference the correct wrapper) - PathItemRef string // The path item's $ref (if any); used to qualify externally-loaded schemas referenced from this operation's responses + SpecOrder int + Spec *openapi3.Operation + IsAlias bool // True when this path is a $ref alias of another path item + AliasTarget string // When IsAlias is true, this is the OperationId of the canonical operation (for route registration to reference the correct wrapper) + PathItemRef string // The path item's $ref (if any); used to qualify externally-loaded schemas referenced from this operation's responses // IsWebhook is true when this OperationDefinition was sourced from // spec.Webhooks (OpenAPI 3.1+). Webhook operations have no path @@ -1105,12 +1118,17 @@ func OperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, error) { // spec declaration order (issue #1887). Zero when unavailable. pathSpecOrder := pathItemSourceLine(pathItem) // These are parameters defined for all methods on a given path. They - // are shared by all methods. + // are shared by all methods, so their helper types are declared once + // for the path item (on its first operation) rather than once per + // operation (issue #2090). globalParams, err := DescribeParameters(pathItem.Parameters, nil) if err != nil { return nil, fmt.Errorf("error describing global parameters for %s: %s", requestPath, err) } + markShared(globalParams) + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false // Each path can have a number of operations, POST, GET, OPTIONS, etc. pathOps := pathItem.Operations() @@ -1257,6 +1275,13 @@ func OperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, error) { // Generate all the type definitions needed for this operation opDef.TypeDefinitions = append(opDef.TypeDefinitions, GenerateTypeDefsForOperation(opDef)...) + // Declare the shared (path-item-level) parameter helper types once, + // on the first operation of the path item (issue #2090). + if !sharedTypeDefsEmitted { + opDef.TypeDefinitions = append(opDef.TypeDefinitions, sharedParamTypeDefs...) + sharedTypeDefsEmitted = true + } + operations = append(operations, opDef) } } @@ -1289,11 +1314,15 @@ func WebhookOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, er } // Path-item-level parameters apply to every method on the - // webhook (rare for webhooks, but honored defensively). + // webhook (rare for webhooks, but honored defensively). Their helper + // types are declared once for the path item (issue #2090). globalParams, err := DescribeParameters(pathItem.Parameters, nil) if err != nil { return nil, fmt.Errorf("error describing webhook %q parameters: %w", webhookName, err) } + markShared(globalParams) + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false pathOps := pathItem.Operations() for _, opName := range SortedMapKeys(pathOps) { @@ -1355,6 +1384,10 @@ func WebhookOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, er } opDef.TypeDefinitions = append(opDef.TypeDefinitions, GenerateTypeDefsForOperation(opDef)...) + if !sharedTypeDefsEmitted { + opDef.TypeDefinitions = append(opDef.TypeDefinitions, sharedParamTypeDefs...) + sharedTypeDefsEmitted = true + } operations = append(operations, opDef) } } @@ -1429,6 +1462,12 @@ func CallbackOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, e return nil, fmt.Errorf("error describing callback %q parameters: %w", callbackName, err) } + // Shared callback path-item parameters: declare helper + // types once for the path item (issue #2090). + markShared(globalParams) + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false + cbOps := cbPathItem.Operations() for _, opName := range SortedMapKeys(cbOps) { op := cbOps[opName] @@ -1486,6 +1525,10 @@ func CallbackOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, e } opDef.TypeDefinitions = append(opDef.TypeDefinitions, GenerateTypeDefsForOperation(opDef)...) + if !sharedTypeDefsEmitted { + opDef.TypeDefinitions = append(opDef.TypeDefinitions, sharedParamTypeDefs...) + sharedTypeDefsEmitted = true + } operations = append(operations, opDef) } } @@ -1809,6 +1852,26 @@ func GenerateResponseDefinitions(operationID string, responses map[string]*opena return responseDefinitions, nil } +// markShared flags every parameter as shared at the path-item level, so its +// helper types are declared once for the path item rather than once per +// operation (issue #2090). +func markShared(params []ParameterDefinition) { + for i := range params { + params[i].Shared = true + } +} + +// sharedParameterTypeDefs returns the helper TypeDefinitions produced by +// path-item-level parameters. These are emitted once for the path item +// (attributed to its first operation) instead of once per operation. +func sharedParameterTypeDefs(sharedParams []ParameterDefinition) []TypeDefinition { + var typeDefs []TypeDefinition + for _, param := range sharedParams { + typeDefs = append(typeDefs, param.Schema.AdditionalTypes...) + } + return typeDefs +} + func GenerateTypeDefsForOperation(op OperationDefinition) []TypeDefinition { var typeDefs []TypeDefinition // Start with the params object itself @@ -1816,8 +1879,14 @@ func GenerateTypeDefsForOperation(op OperationDefinition) []TypeDefinition { typeDefs = append(typeDefs, GenerateParamsTypes(op)...) } - // Now, go through all the additional types we need to declare. + // Now, go through all the additional types we need to declare. Skip + // parameters shared at the path-item level: their helper types are + // declared once for the path item (see sharedParameterTypeDefs), not once + // per operation, which would redeclare them (issue #2090). for _, param := range op.AllParams() { + if param.Shared { + continue + } typeDefs = append(typeDefs, param.Schema.AdditionalTypes...) } diff --git a/pkg/codegen/validate_spec.go b/pkg/codegen/validate_spec.go index d7f31e46c..ff9e6bf44 100644 --- a/pkg/codegen/validate_spec.go +++ b/pkg/codegen/validate_spec.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "go/token" + "slices" + "strings" "unicode" "github.com/getkin/kin-openapi/openapi3" @@ -30,13 +32,101 @@ func ValidateSpec(spec *openapi3.T) error { if spec == nil { return nil } - v := &specValidator{} + v := &specValidator{ + pathParamTypeDecls: map[string]int{}, + pathParamTypeWhere: map[string][]string{}, + } v.walkDocument(spec) + v.checkSharedPathParamTypeCollisions() return errors.Join(v.errs...) } type specValidator struct { errs []error + + // A path-item-level parameter whose inline schema needs a helper Go type + // (an anyOf/oneOf union, an enum, or an inline object) has that type + // declared once for the path item under a name derived only from the + // parameter. When two different path items declare a parameter of the + // same name, both emit the same type name and Go rejects the duplicate + // (issue #2090). These maps count, per generated type name, how many path + // items declare it and where, so checkSharedPathParamTypeCollisions can + // turn that into a clear pre-generation error. + pathParamTypeDecls map[string]int + pathParamTypeWhere map[string][]string +} + +// recordPathLevelParams accounts for the helper types generated by a path +// item's shared (path-level) parameters. Each type is declared once for the +// path item, so a name declared by more than one path item collides. +func (v *specValidator) recordPathLevelParams(item *openapi3.PathItem, where string) { + if item == nil || len(item.Operations()) == 0 { + return + } + for _, p := range item.Parameters { + name, ok := generatedParamTypeName(p) + if !ok { + continue + } + v.pathParamTypeDecls[name]++ + v.pathParamTypeWhere[name] = append(v.pathParamTypeWhere[name], where) + } +} + +// checkSharedPathParamTypeCollisions reports a generated helper type name that +// is produced by a path-level parameter on more than one path item, which +// would declare the same type twice. +func (v *specValidator) checkSharedPathParamTypeCollisions() { + for _, name := range SortedMapKeys(v.pathParamTypeDecls) { + if v.pathParamTypeDecls[name] <= 1 { + continue + } + wheres := dedupeStrings(v.pathParamTypeWhere[name]) + v.addf("the Go type %q is generated for a path-level parameter declared on more than one path (%s), so it would be declared more than once and the output will not compile; define the parameter once under #/components/parameters and $ref it from each path, give it a distinct name with the x-go-type-name extension or a spec overlay, or move it into each operation", + name, strings.Join(wheres, ", ")) + } +} + +// generatedParamTypeName returns the base Go type name a parameter's inline +// schema would generate, and whether it generates a named helper type that can +// collide across path items. An inline anyOf/oneOf produces union member types +// named from the parameter alone (Id0, Id1, ...); plain scalars, enums and +// objects used in a parameter generate no such shared named type, and a $ref +// schema references an already-declared one. +func generatedParamTypeName(ref *openapi3.ParameterRef) (string, bool) { + if ref == nil || ref.Ref != "" || ref.Value == nil { + return "", false + } + sr := ref.Value.Schema + if sr == nil || sr.Ref != "" || sr.Value == nil { + return "", false + } + s := sr.Value + if len(s.AnyOf) == 0 && len(s.OneOf) == 0 { + return "", false + } + // An x-go-type-name (on the schema) overrides the generated name, so + // collisions are keyed by that instead of the parameter name. + if raw, ok := s.Extensions[extGoTypeName]; ok { + if n, err := extTypeName(raw); err == nil { + return n, true + } + } + return SchemaNameToTypeName(ref.Value.Name), true +} + +func dedupeStrings(items []string) []string { + seen := map[string]struct{}{} + var uniq []string + for _, it := range items { + if _, ok := seen[it]; ok { + continue + } + seen[it] = struct{}{} + uniq = append(uniq, it) + } + slices.Sort(uniq) + return uniq } func (v *specValidator) addf(format string, args ...any) { @@ -230,6 +320,7 @@ func (v *specValidator) walkPathItem(item *openapi3.PathItem, where string) { if item == nil { return } + v.recordPathLevelParams(item, where) for _, p := range item.Parameters { v.walkParameterRef(p, where) }