From dfb401bbe75b2dafddd47b622be8def95e18a39c Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Thu, 16 Jul 2026 16:59:15 -0700 Subject: [PATCH 1/2] Revert "Generate shared path-level parameter helper types once (#2466)" This reverts commit a07731d8f3410162ecda0e12b5d1e178c7a1f7ef. --- .../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, 14 insertions(+), 1983 deletions(-) delete mode 100644 internal/test/parameters/shared_anyof/config.yaml delete mode 100644 internal/test/parameters/shared_anyof/doc.go delete mode 100644 internal/test/parameters/shared_anyof/shared_anyof.gen.go delete mode 100644 internal/test/parameters/shared_anyof/shared_anyof_test.go delete mode 100644 internal/test/parameters/shared_anyof/spec.yaml delete 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 deleted file mode 100644 index 3b67ae177..000000000 --- a/internal/test/parameters/shared_anyof/config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# 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 deleted file mode 100644 index b460b9fb8..000000000 --- a/internal/test/parameters/shared_anyof/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index 75ed74d7a..000000000 --- a/internal/test/parameters/shared_anyof/shared_anyof.gen.go +++ /dev/null @@ -1,1639 +0,0 @@ -// 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 deleted file mode 100644 index b11be615b..000000000 --- a/internal/test/parameters/shared_anyof/shared_anyof_test.go +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 62ef86497..000000000 --- a/internal/test/parameters/shared_anyof/spec.yaml +++ /dev/null @@ -1,63 +0,0 @@ -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 f355d3192..3f5441639 100644 --- a/internal/test/spec_validation/spec_validation_test.go +++ b/internal/test/spec_validation/spec_validation_test.go @@ -31,8 +31,6 @@ 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 deleted file mode 100644 index 8392c683f..000000000 --- a/internal/test/spec_validation/testdata/shared_path_param_type_collision.yaml +++ /dev/null @@ -1,27 +0,0 @@ -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 442a57fcb..8724b4a0d 100644 --- a/internal/test/spec_validation/testdata/valid.yaml +++ b/internal/test/spec_validation/testdata/valid.yaml @@ -10,25 +10,6 @@ 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 54dee5e99..90ced55a3 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -1144,6 +1144,11 @@ 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) @@ -1153,16 +1158,6 @@ 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, @@ -1179,10 +1174,6 @@ 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 6d4f65d3d..937012ef5 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -36,12 +36,6 @@ 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 @@ -290,14 +284,7 @@ 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) } @@ -364,11 +351,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 @@ -1118,17 +1105,12 @@ 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, so their helper types are declared once - // for the path item (on its first operation) rather than once per - // operation (issue #2090). + // are shared by all methods. 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() @@ -1275,13 +1257,6 @@ 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) } } @@ -1314,15 +1289,11 @@ func WebhookOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, er } // Path-item-level parameters apply to every method on the - // webhook (rare for webhooks, but honored defensively). Their helper - // types are declared once for the path item (issue #2090). + // webhook (rare for webhooks, but honored defensively). 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) { @@ -1384,10 +1355,6 @@ 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) } } @@ -1462,12 +1429,6 @@ 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] @@ -1525,10 +1486,6 @@ 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) } } @@ -1852,26 +1809,6 @@ 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 @@ -1879,14 +1816,8 @@ func GenerateTypeDefsForOperation(op OperationDefinition) []TypeDefinition { typeDefs = append(typeDefs, GenerateParamsTypes(op)...) } - // 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). + // Now, go through all the additional types we need to declare. 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 ff9e6bf44..d7f31e46c 100644 --- a/pkg/codegen/validate_spec.go +++ b/pkg/codegen/validate_spec.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "go/token" - "slices" - "strings" "unicode" "github.com/getkin/kin-openapi/openapi3" @@ -32,101 +30,13 @@ func ValidateSpec(spec *openapi3.T) error { if spec == nil { return nil } - v := &specValidator{ - pathParamTypeDecls: map[string]int{}, - pathParamTypeWhere: map[string][]string{}, - } + v := &specValidator{} 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) { @@ -320,7 +230,6 @@ 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) } From 4f69fd3d5690c53bd1ea8ed01fd06b918ba57862 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Thu, 16 Jul 2026 18:08:36 -0700 Subject: [PATCH 2/2] Name shared path parameter helper types once, hashing cross-path collisions Closes: #2090 A parameter declared at the path-item level is inherited by every method on the path. Its hoisted helper types -- the members of an inline anyOf/oneOf and similar -- were named from the parameter alone and declared once per operation, so a path with more than one method redeclared them ("Id0 redeclared in this block"). The same bare names also collided across different paths that reuse the same parameter, which is the common REST case of an {id} shared by sibling paths. Resolve path-item-level parameters in a single pre-pass (resolveSharedParameters): describe each scope's shared parameters once, count how many scopes hoist a helper type under each name, and emit each scope's helper types once for its path item rather than once per operation. A name produced by two or more scopes is disambiguated by prefixing that scope's parameters with a short, stable FNV hash of the scope, extended to the full hash only if two scopes' short hashes clash. A parameter that does not collide keeps its historical undecorated name, so existing generated code is unaffected -- the only outputs that change are specs that previously failed to compile. Applied uniformly to regular paths, webhooks, and callbacks, which share the one global Go type namespace. Disambiguated types carry a doc comment explaining the hash prefix and pointing back to the source path. Adds internal/test/parameters/shared_collision covering a shared anyOf parameter across multiple methods on a path (bare, emitted once), the same parameter reused across sibling paths (hash-disambiguated), and a non-colliding single-method parameter (unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../parameters/shared_collision/config.yaml | 6 + .../test/parameters/shared_collision/doc.go | 19 + .../shared_collision/shared_collision.gen.go | 1295 +++++++++++++++++ .../shared_collision/shared_collision_test.go | 56 + .../parameters/shared_collision/spec.yaml | 111 ++ pkg/codegen/operations.go | 330 ++++- pkg/codegen/schema.go | 7 + pkg/codegen/templates/param-types.tmpl | 2 +- 8 files changed, 1807 insertions(+), 19 deletions(-) create mode 100644 internal/test/parameters/shared_collision/config.yaml create mode 100644 internal/test/parameters/shared_collision/doc.go create mode 100644 internal/test/parameters/shared_collision/shared_collision.gen.go create mode 100644 internal/test/parameters/shared_collision/shared_collision_test.go create mode 100644 internal/test/parameters/shared_collision/spec.yaml diff --git a/internal/test/parameters/shared_collision/config.yaml b/internal/test/parameters/shared_collision/config.yaml new file mode 100644 index 000000000..61c9942fb --- /dev/null +++ b/internal/test/parameters/shared_collision/config.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: parameterssharedcollision +output: shared_collision.gen.go +generate: + models: true + client: true diff --git a/internal/test/parameters/shared_collision/doc.go b/internal/test/parameters/shared_collision/doc.go new file mode 100644 index 000000000..cc0d763b3 --- /dev/null +++ b/internal/test/parameters/shared_collision/doc.go @@ -0,0 +1,19 @@ +// Package parameterssharedcollision is a regression fixture for the naming of +// helper types hoisted by shared (path-item-level) parameters (issue #2090). +// +// A parameter declared at the path-item level is inherited by every method on +// the path, so a helper type it hoists (e.g. an anyOf member) was declared once +// per operation and redeclared — "Id0 redeclared in this block" — on any path +// with more than one method, and clashed again across sibling paths that reuse +// the same parameter. The spec exercises each case: a parameter shared across +// two methods of one path (declared once, bare), the same parameter reused by +// two sibling paths (hash-disambiguated per path), a non-colliding single-method +// parameter (unchanged), and the equivalent shared parameters on a webhook and a +// callback (the emit-once paths in WebhookOperationDefinitions and +// CallbackOperationDefinitions). +// +// The generated file compiling as part of this package is the primary guard: +// the redeclaration bug would fail the build. +package parameterssharedcollision + +//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_collision/shared_collision.gen.go b/internal/test/parameters/shared_collision/shared_collision.gen.go new file mode 100644 index 000000000..e237ca344 --- /dev/null +++ b/internal/test/parameters/shared_collision/shared_collision.gen.go @@ -0,0 +1,1295 @@ +// Package parameterssharedcollision 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 parameterssharedcollision + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// Hd147675Id0 is a helper type for the shared "id" parameter of "/gadget/{id}", prefixed with a per-path hash to disambiguate it from the same-named parameter on another path. +type Hd147675Id0 = openapi_types.UUID + +// Hd147675Id1 is a helper type for the shared "id" parameter of "/gadget/{id}", prefixed with a per-path hash to disambiguate it from the same-named parameter on another path. +type Hd147675Id1 = string + +// Hdf81445Id0 is a helper type for the shared "id" parameter of "/gadget/{id}/part", prefixed with a per-path hash to disambiguate it from the same-named parameter on another path. +type Hdf81445Id0 = openapi_types.UUID + +// Hdf81445Id1 is a helper type for the shared "id" parameter of "/gadget/{id}/part", prefixed with a per-path hash to disambiguate it from the same-named parameter on another path. +type Hdf81445Id1 = string + +// Ref0 defines parameters for GetSprocket. +type Ref0 = openapi_types.UUID + +// Ref1 defines parameters for GetSprocket. +type Ref1 = string + +// Wid0 defines parameters for DeleteWidget. +type Wid0 = openapi_types.UUID + +// Wid1 defines parameters for DeleteWidget. +type Wid1 = string + +// OnEventGetParams defines parameters for OnEventGet. +type OnEventGetParams struct { + Whid *struct { + union json.RawMessage + } `form:"whid,omitempty" json:"whid,omitempty"` +} + +// Whid0 defines parameters for OnEventGet. +type Whid0 = openapi_types.UUID + +// Whid1 defines parameters for OnEventGet. +type Whid1 = string + +// OnEventPostParams defines parameters for OnEventPost. +type OnEventPostParams struct { + Whid *struct { + union json.RawMessage + } `form:"whid,omitempty" json:"whid,omitempty"` +} + +// OnDataGetParams defines parameters for OnDataGet. +type OnDataGetParams struct { + Cbid *struct { + union json.RawMessage + } `form:"cbid,omitempty" json:"cbid,omitempty"` +} + +// Cbid0 defines parameters for OnDataGet. +type Cbid0 = openapi_types.UUID + +// Cbid1 defines parameters for OnDataGet. +type Cbid1 = string + +// OnDataPostParams defines parameters for OnDataPost. +type OnDataPostParams struct { + Cbid *struct { + union json.RawMessage + } `form:"cbid,omitempty" json:"cbid,omitempty"` +} + +// 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 { + + // DeleteGadget performs a DELETE /gadget/{id} (the `DeleteGadget` operationId) request. + DeleteGadget(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetGadgetPart performs a GET /gadget/{id}/part (the `GetGadgetPart` operationId) request. + GetGadgetPart(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetSprocket performs a GET /sprocket/{ref} (the `GetSprocket` operationId) request. + GetSprocket(ctx context.Context, ref struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // Subscribe performs a POST /subscribe (the `Subscribe` operationId) request. + Subscribe(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteWidget performs a DELETE /widget/{wid} (the `DeleteWidget` operationId) request. + DeleteWidget(ctx context.Context, wid struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetWidget performs a GET /widget/{wid} (the `GetWidget` operationId) request. + GetWidget(ctx context.Context, wid struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// DeleteGadget performs a DELETE /gadget/{id} (the `DeleteGadget` operationId) request. +func (c *Client) DeleteGadget(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteGadgetRequest(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) +} + +// GetGadgetPart performs a GET /gadget/{id}/part (the `GetGadgetPart` operationId) request. +func (c *Client) GetGadgetPart(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetGadgetPartRequest(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) +} + +// GetSprocket performs a GET /sprocket/{ref} (the `GetSprocket` operationId) request. +func (c *Client) GetSprocket(ctx context.Context, ref struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetSprocketRequest(c.Server, ref) + 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) +} + +// DeleteWidget performs a DELETE /widget/{wid} (the `DeleteWidget` operationId) request. +func (c *Client) DeleteWidget(ctx context.Context, wid struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteWidgetRequest(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) +} + +// GetWidget performs a GET /widget/{wid} (the `GetWidget` operationId) request. +func (c *Client) GetWidget(ctx context.Context, wid struct { + union json.RawMessage +}, 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) +} + +// NewDeleteGadgetRequest constructs an http.Request for the DeleteGadget method +func NewDeleteGadgetRequest(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("/gadget/%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 +} + +// NewGetGadgetPartRequest constructs an http.Request for the GetGadgetPart method +func NewGetGadgetPartRequest(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("/gadget/%s/part", 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 +} + +// NewGetSprocketRequest constructs an http.Request for the GetSprocket method +func NewGetSprocketRequest(server string, ref struct { + union json.RawMessage +}) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, 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("/sprocket/%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 +} + +// NewDeleteWidgetRequest constructs an http.Request for the DeleteWidget method +func NewDeleteWidgetRequest(server string, wid struct { + union json.RawMessage +}) (*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("/widget/%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 +} + +// NewGetWidgetRequest constructs an http.Request for the GetWidget method +func NewGetWidgetRequest(server string, wid struct { + union json.RawMessage +}) (*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("/widget/%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 { + + // DeleteGadgetWithResponse performs a DELETE /gadget/{id} (the `DeleteGadget` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + DeleteGadgetWithResponse(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*DeleteGadgetResponse, error) + + // GetGadgetPartWithResponse performs a GET /gadget/{id}/part (the `GetGadgetPart` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetGadgetPartWithResponse(ctx context.Context, id struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*GetGadgetPartResponse, error) + + // GetSprocketWithResponse performs a GET /sprocket/{ref} (the `GetSprocket` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetSprocketWithResponse(ctx context.Context, ref struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*GetSprocketResponse, 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) + + // DeleteWidgetWithResponse performs a DELETE /widget/{wid} (the `DeleteWidget` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + DeleteWidgetWithResponse(ctx context.Context, wid struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*DeleteWidgetResponse, error) + + // GetWidgetWithResponse performs a GET /widget/{wid} (the `GetWidget` operationId) request. + // + // Returns a wrapper object for the known response body format(s). + GetWidgetWithResponse(ctx context.Context, wid struct { + union json.RawMessage + }, reqEditors ...RequestEditorFn) (*GetWidgetResponse, error) +} + +type DeleteGadgetResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r DeleteGadgetResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r DeleteGadgetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteGadgetResponse) 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 DeleteGadgetResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetGadgetPartResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r GetGadgetPartResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetGadgetPartResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetGadgetPartResponse) 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 GetGadgetPartResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetSprocketResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r GetSprocketResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r GetSprocketResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetSprocketResponse) 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 GetSprocketResponse) 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 DeleteWidgetResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// GetBody returns the raw response body bytes +func (r DeleteWidgetResponse) GetBody() []byte { + return r.Body +} + +// Status returns HTTPResponse.Status +func (r DeleteWidgetResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteWidgetResponse) 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 DeleteWidgetResponse) 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 "" +} + +// DeleteGadgetWithResponse performs a DELETE /gadget/{id} (the `DeleteGadget` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) DeleteGadgetWithResponse(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*DeleteGadgetResponse, error) { + rsp, err := c.DeleteGadget(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteGadgetResponse(rsp) +} + +// GetGadgetPartWithResponse performs a GET /gadget/{id}/part (the `GetGadgetPart` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetGadgetPartWithResponse(ctx context.Context, id struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*GetGadgetPartResponse, error) { + rsp, err := c.GetGadgetPart(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetGadgetPartResponse(rsp) +} + +// GetSprocketWithResponse performs a GET /sprocket/{ref} (the `GetSprocket` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetSprocketWithResponse(ctx context.Context, ref struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*GetSprocketResponse, error) { + rsp, err := c.GetSprocket(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetSprocketResponse(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) +} + +// DeleteWidgetWithResponse performs a DELETE /widget/{wid} (the `DeleteWidget` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) DeleteWidgetWithResponse(ctx context.Context, wid struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*DeleteWidgetResponse, error) { + rsp, err := c.DeleteWidget(ctx, wid, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteWidgetResponse(rsp) +} + +// GetWidgetWithResponse performs a GET /widget/{wid} (the `GetWidget` operationId) request. +// +// Returns a wrapper object for the known response body format(s). +func (c *ClientWithResponses) GetWidgetWithResponse(ctx context.Context, wid struct { + union json.RawMessage +}, reqEditors ...RequestEditorFn) (*GetWidgetResponse, error) { + rsp, err := c.GetWidget(ctx, wid, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetWidgetResponse(rsp) +} + +// ParseDeleteGadgetResponse parses an HTTP response from a DeleteGadgetWithResponse call +func ParseDeleteGadgetResponse(rsp *http.Response) (*DeleteGadgetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteGadgetResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetGadgetPartResponse parses an HTTP response from a GetGadgetPartWithResponse call +func ParseGetGadgetPartResponse(rsp *http.Response) (*GetGadgetPartResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetGadgetPartResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetSprocketResponse parses an HTTP response from a GetSprocketWithResponse call +func ParseGetSprocketResponse(rsp *http.Response) (*GetSprocketResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetSprocketResponse{ + 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 +} + +// ParseDeleteWidgetResponse parses an HTTP response from a DeleteWidgetWithResponse call +func ParseDeleteWidgetResponse(rsp *http.Response) (*DeleteWidgetResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteWidgetResponse{ + 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 { + // OnEventGet fires the onEvent webhook + OnEventGet(ctx context.Context, targetURL string, params *OnEventGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // OnEventPost fires the onEvent webhook + OnEventPost(ctx context.Context, targetURL string, params *OnEventPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (p *WebhookInitiator) OnEventGet(ctx context.Context, targetURL string, params *OnEventGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnEventGetWebhookRequest(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) OnEventPost(ctx context.Context, targetURL string, params *OnEventPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnEventPostWebhookRequest(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) +} + +// NewOnEventGetWebhookRequest builds a GET request for the onEvent webhook +func NewOnEventGetWebhookRequest(targetURL string, params *OnEventGetParams) (*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 params.Whid != nil { + + 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 +} + +// NewOnEventPostWebhookRequest builds a POST request for the onEvent webhook +func NewOnEventPostWebhookRequest(targetURL string, params *OnEventPostParams) (*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 params.Whid != nil { + + 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 { + // OnDataGet fires the onData callback + OnDataGet(ctx context.Context, targetURL string, params *OnDataGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // OnDataPost fires the onData callback + OnDataPost(ctx context.Context, targetURL string, params *OnDataPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (p *CallbackInitiator) OnDataGet(ctx context.Context, targetURL string, params *OnDataGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnDataGetCallbackRequest(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) OnDataPost(ctx context.Context, targetURL string, params *OnDataPostParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewOnDataPostCallbackRequest(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) +} + +// NewOnDataGetCallbackRequest builds a GET request for the onData callback +func NewOnDataGetCallbackRequest(targetURL string, params *OnDataGetParams) (*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 params.Cbid != nil { + + 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 +} + +// NewOnDataPostCallbackRequest builds a POST request for the onData callback +func NewOnDataPostCallbackRequest(targetURL string, params *OnDataPostParams) (*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 params.Cbid != nil { + + 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 +} diff --git a/internal/test/parameters/shared_collision/shared_collision_test.go b/internal/test/parameters/shared_collision/shared_collision_test.go new file mode 100644 index 000000000..705740a63 --- /dev/null +++ b/internal/test/parameters/shared_collision/shared_collision_test.go @@ -0,0 +1,56 @@ +package parameterssharedcollision + +import ( + "os" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Referencing these names asserts, at compile time, that non-colliding shared +// (path-item-level) parameters keep their historical undecorated form, so +// existing code is unaffected (issue #2090). The generated file compiling is +// itself the primary guard — the "redeclared in this block" bug would fail the +// build. +// +// - Wid0/Wid1: shared across two methods of /widget/{wid}. Declared once for +// the path item (not once per method), and bare because the name is unique. +// - Ref0/Ref1: the single-method /sprocket/{ref}, also unique and bare. +// - Whid0/Whid1: shared across the two methods of the onEvent webhook, and +// Cbid0/Cbid1 across the two methods of the onData callback — the webhook +// and callback emit-once paths, declared once each rather than per method. +var ( + _ Wid0 + _ Wid1 + _ Ref0 + _ Ref1 + _ Whid0 + _ Whid1 + _ Cbid0 + _ Cbid1 +) + +// TestSharedParamCollisionHashed asserts that the same {id} parameter, reused by +// the two sibling paths /gadget/{id} and /gadget/{id}/part, is disambiguated by +// a per-path hash prefix rather than colliding on a bare name. We read the +// generated source because the hash values are derived from the paths and +// shouldn't be hard-coded into the test. +func TestSharedParamCollisionHashed(t *testing.T) { + src, err := os.ReadFile("shared_collision.gen.go") + require.NoError(t, err) + + // The bare, colliding names must NOT be declared. + assert.NotRegexp(t, regexp.MustCompile(`(?m)^type Id0 `), string(src)) + assert.NotRegexp(t, regexp.MustCompile(`(?m)^type Id1 `), string(src)) + + // Exactly two distinct hash-prefixed variants of the id union member must be + // declared — one per colliding path. + variants := regexp.MustCompile(`(?m)^type (H[0-9a-f]+Id0) `).FindAllStringSubmatch(string(src), -1) + seen := map[string]bool{} + for _, m := range variants { + seen[m[1]] = true + } + assert.Len(t, seen, 2, "expected two per-path hash-disambiguated id types, got %v", seen) +} diff --git a/internal/test/parameters/shared_collision/spec.yaml b/internal/test/parameters/shared_collision/spec.yaml new file mode 100644 index 000000000..68a0b8220 --- /dev/null +++ b/internal/test/parameters/shared_collision/spec.yaml @@ -0,0 +1,111 @@ +openapi: 3.1.0 +info: + title: shared-path-parameter-collision + version: "1.0" +paths: + # Within-path, unique name: a path-item-level `anyOf` parameter shared by two + # methods. Its helper types are declared once for the path item (not once per + # method, which was the "Wid0 redeclared" bug), and since the name is unique + # across the spec it keeps its bare, undecorated form. + /widget/{wid}: + parameters: + - name: wid + in: path + required: true + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + get: + operationId: getWidget + responses: { '200': { description: ok } } + delete: + operationId: deleteWidget + responses: { '200': { description: ok } } + + # Cross-path collision: two sibling single-method paths reuse the same `{id}` + # parameter. The bare names would collide across the two paths, so each path's + # parameter is prefixed with a short, stable hash of the path to disambiguate. + /gadget/{id}: + parameters: + - name: id + in: path + required: true + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + delete: + operationId: deleteGadget + responses: { '200': { description: ok } } + /gadget/{id}/part: + parameters: + - name: id + in: path + required: true + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + get: + operationId: getGadgetPart + responses: { '200': { description: ok } } + + # No collision: a single-method path whose shared parameter hoists a name used + # nowhere else keeps its historical undecorated names (Ref0/Ref1), so existing + # generated code is unaffected. + /sprocket/{ref}: + parameters: + - name: ref + in: path + required: true + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + get: + operationId: getSprocket + responses: { '200': { description: ok } } + + # Callback with a parameter shared across the callback path item's methods. + # Exercises the shared-parameter emit-once path in CallbackOperationDefinitions + # (cbid produces Cbid0/Cbid1 once, not once per callback method). + /subscribe: + post: + operationId: subscribe + responses: { '200': { description: ok } } + callbacks: + onData: + '{$request.body#/callbackUrl}': + parameters: + - name: cbid + in: query + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + get: + operationId: onDataGet + responses: { '200': { description: ok } } + post: + operationId: onDataPost + responses: { '200': { description: ok } } + +# Webhook with a parameter shared across the webhook path item's methods. +# Exercises the shared-parameter emit-once path in WebhookOperationDefinitions +# (whid produces Whid0/Whid1 once, not once per webhook method). +webhooks: + onEvent: + parameters: + - name: whid + in: query + schema: + anyOf: + - { type: string, format: uuid } + - { type: string } + get: + operationId: onEventGet + responses: { '200': { description: ok } } + post: + operationId: onEventPost + responses: { '200': { description: ok } } diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 937012ef5..ccb9c39d7 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -18,6 +18,7 @@ import ( "bytes" "cmp" "fmt" + "hash/fnv" "maps" "slices" "strconv" @@ -36,6 +37,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 declared + // 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 @@ -291,6 +298,253 @@ func DescribeParameters(params openapi3.Parameters, path []string) ([]ParameterD return outParams, nil } +// paramNeedsHoisting reports whether a parameter's schema produces a named +// helper type — an anyOf/oneOf union member, an inline object, etc. — as +// opposed to a bare primitive. Only such parameters can cause the redeclaration +// collision fixed for issue #2090, so only they participate in collision +// detection. The check is exact: it describes the parameter and asks whether it +// hoisted anything, rather than second-guessing which schema shapes hoist. +func paramNeedsHoisting(paramRef *openapi3.ParameterRef) (bool, error) { + if paramRef == nil || paramRef.Value == nil { + return false, nil + } + described, err := DescribeParameters(openapi3.Parameters{paramRef}, nil) + if err != nil { + return false, err + } + for _, pd := range described { + if len(pd.Schema.AdditionalTypes) > 0 { + return true, nil + } + } + return false, nil +} + +// sharedParamScope is one path item whose path-item-level parameters are shared +// by every method on it. hashKey is a string unique to the scope, hashed to +// disambiguate colliding names; source is a human-readable identifier for the +// scope used in generated doc comments. +type sharedParamScope struct { + item *openapi3.PathItem + hashKey string + source string +} + +// enumerateSharedParamScopes returns every path item in the spec that can +// declare path-item-level (shared) parameters: regular paths, webhooks, and +// callback path items. All three share the one global Go type namespace, so +// they must be considered together when detecting and resolving collisions. +func enumerateSharedParamScopes(swagger *openapi3.T) []sharedParamScope { + var scopes []sharedParamScope + + if swagger.Paths != nil { + for _, requestPath := range SortedMapKeys(swagger.Paths.Map()) { + item := swagger.Paths.Value(requestPath) + if item != nil { + scopes = append(scopes, sharedParamScope{item: item, hashKey: requestPath, source: requestPath}) + } + } + } + for _, webhookName := range SortedMapKeys(swagger.Webhooks) { + if item := swagger.Webhooks[webhookName]; item != nil { + scopes = append(scopes, sharedParamScope{item: item, hashKey: "webhook:" + webhookName, source: "webhook " + webhookName}) + } + } + if swagger.Paths != nil { + for _, requestPath := range SortedMapKeys(swagger.Paths.Map()) { + pathItem := swagger.Paths.Value(requestPath) + if pathItem == nil { + continue + } + for _, parentMethod := range SortedMapKeys(pathItem.Operations()) { + parentOp := pathItem.Operations()[parentMethod] + for _, cbName := range SortedMapKeys(parentOp.Callbacks) { + cbRef := parentOp.Callbacks[cbName] + if cbRef == nil || cbRef.Value == nil { + continue + } + cb := cbRef.Value + cbKeys := append([]string(nil), cb.Keys()...) + slices.Sort(cbKeys) + for _, urlExpr := range cbKeys { + if item := cb.Value(urlExpr); item != nil { + scopes = append(scopes, sharedParamScope{ + item: item, + hashKey: "callback:" + requestPath + ":" + parentMethod + ":" + cbName + ":" + urlExpr, + source: "callback " + cbName + " " + urlExpr, + }) + } + } + } + } + } + } + return scopes +} + +// resolveSharedParameters is the pre-pass for shared (path-item-level) +// parameter naming (issue #2090). It describes each scope's shared parameters +// once and returns them keyed by path item, ready for the operation loops to +// attach. +// +// A shared parameter's helper types are declared once for its path item, so +// two methods on the same path never collide. Names still collide *across* +// path items, though — the same `{id}` reused by sibling paths is the common +// case — so a helper-type name produced by more than one scope is disambiguated +// by prefixing every colliding scope's parameters with a short, stable hash of +// the scope (git-style: extended to the full hash only if two scopes' short +// hashes clash). A parameter that doesn't collide keeps its historical +// undecorated name, so existing generated code is unaffected. +func resolveSharedParameters(swagger *openapi3.T) (map[*openapi3.PathItem][]ParameterDefinition, error) { + scopes := enumerateSharedParamScopes(swagger) + + // Count, per shared-parameter name, how many scopes hoist a helper type + // under it. A name produced by two or more scopes collides. + nameCounts := map[string]int{} + for _, scope := range scopes { + if len(scope.item.Operations()) == 0 { + continue + } + for _, paramRef := range scope.item.Parameters { + hoists, err := paramNeedsHoisting(paramRef) + if err != nil { + return nil, err + } + if hoists { + nameCounts[paramRef.Value.Name]++ + } + } + } + colliding := map[string]bool{} + for name, n := range nameCounts { + if n >= 2 { + colliding[name] = true + } + } + + // Assign a disambiguating token to every scope that carries a colliding + // parameter. Tokens are hashes of the scope key; a short prefix is used + // unless two scopes' short prefixes clash, in which case both fall back to + // the full hash. + tokenScopeKeys := map[*openapi3.PathItem]string{} + var needToken []string + for _, scope := range scopes { + if len(scope.item.Operations()) == 0 { + continue + } + for _, paramRef := range scope.item.Parameters { + if paramRef.Value != nil && colliding[paramRef.Value.Name] { + tokenScopeKeys[scope.item] = scope.hashKey + needToken = append(needToken, scope.hashKey) + break + } + } + } + tokens := assignScopeTokens(needToken) + + result := map[*openapi3.PathItem][]ParameterDefinition{} + for _, scope := range scopes { + if _, done := result[scope.item]; done { + continue + } + token := "" + if key, ok := tokenScopeKeys[scope.item]; ok { + token = tokens[key] + } + described, err := describeSharedParameters(scope.item.Parameters, scope.source, token, colliding) + if err != nil { + return nil, err + } + markShared(described) + result[scope.item] = described + } + return result, nil +} + +// assignScopeTokens maps each scope key to a stable identifier token derived +// from an FNV hash of the key. It uses a short prefix of the hash, extending +// any keys whose short prefixes collide to the full hash (git-style). The +// tokens are lowercase so they camel-case cleanly when threaded through type +// naming (e.g. "a1b2c3d" becomes the "A1b2c3d" prefix of "A1b2c3dId0"). +func assignScopeTokens(keys []string) map[string]string { + const shortLen = 7 + full := map[string]string{} + short := map[string]string{} + for _, key := range keys { + h := fnv.New64a() + _, _ = h.Write([]byte(key)) + sum := fmt.Sprintf("h%x", h.Sum64()) + full[key] = sum + short[key] = sum[:min(shortLen+1, len(sum))] + } + shortCounts := map[string]int{} + for _, key := range keys { + shortCounts[short[key]]++ + } + tokens := map[string]string{} + for _, key := range keys { + if shortCounts[short[key]] > 1 { + tokens[key] = full[key] + } else { + tokens[key] = short[key] + } + } + return tokens +} + +// describeSharedParameters describes a scope's shared parameters once. A +// parameter whose name collides across scopes is prefixed with the scope's +// disambiguating token so its helper types are uniquely named; a parameter +// that does not collide keeps its historical undecorated name (issue #2090). +// For the disambiguated types, a doc comment is set explaining the otherwise +// opaque hash prefix and pointing back to the source path. +func describeSharedParameters(params openapi3.Parameters, source, token string, colliding map[string]bool) ([]ParameterDefinition, error) { + out := make([]ParameterDefinition, 0, len(params)) + for _, paramRef := range params { + collides := token != "" && paramRef.Value != nil && colliding[paramRef.Value.Name] + var path []string + if collides { + path = []string{token} + } + described, err := DescribeParameters(openapi3.Parameters{paramRef}, path) + if err != nil { + return nil, err + } + if collides { + for i := range described { + for j := range described[i].Schema.AdditionalTypes { + td := &described[i].Schema.AdditionalTypes[j] + td.Comment = fmt.Sprintf( + "// %s is a helper type for the shared %q parameter of %q, prefixed with a per-path hash to disambiguate it from the same-named parameter on another path.", + td.TypeName, paramRef.Value.Name, source) + } + } + } + out = append(out, described...) + } + return out, 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 +} + type SecurityDefinition struct { ProviderName string Scopes []string @@ -1099,18 +1353,25 @@ func OperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, error) { // when multiple paths $ref the same path item. aliasCounters := map[string]int{} + // Resolve path-item-level (shared) parameters once for the whole spec, so + // their helper types are declared once per path item and colliding names + // across paths are disambiguated (issue #2090). + sharedParams, err := resolveSharedParameters(swagger) + if err != nil { + return nil, err + } + for _, requestPath := range SortedMapKeys(swagger.Paths.Map()) { pathItem := swagger.Paths.Value(requestPath) // Source line of this path's key, so route registration can follow // 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. - globalParams, err := DescribeParameters(pathItem.Parameters, nil) - if err != nil { - return nil, fmt.Errorf("error describing global parameters for %s: %s", - requestPath, err) - } + // Parameters defined for all methods on this path, resolved by the + // pre-pass. Their helper types are emitted once for the path item (on + // its first operation) rather than once per operation (issue #2090). + globalParams := sharedParams[pathItem] + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false // Each path can have a number of operations, POST, GET, OPTIONS, etc. pathOps := pathItem.Operations() @@ -1257,6 +1518,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) } } @@ -1282,18 +1550,23 @@ func WebhookOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, er return operations, nil } + sharedParams, err := resolveSharedParameters(swagger) + if err != nil { + return nil, err + } + for _, webhookName := range SortedMapKeys(swagger.Webhooks) { pathItem := swagger.Webhooks[webhookName] if pathItem == nil { continue } - // Path-item-level parameters apply to every method on the - // webhook (rare for webhooks, but honored defensively). - globalParams, err := DescribeParameters(pathItem.Parameters, nil) - if err != nil { - return nil, fmt.Errorf("error describing webhook %q parameters: %w", webhookName, err) - } + // Path-item-level parameters apply to every method on the webhook + // (rare for webhooks, but honored defensively). Their helper types are + // declared once for the path item (issue #2090). + globalParams := sharedParams[pathItem] + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false pathOps := pathItem.Operations() for _, opName := range SortedMapKeys(pathOps) { @@ -1355,6 +1628,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) } } @@ -1394,6 +1671,11 @@ func CallbackOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, e return operations, nil } + sharedParams, err := resolveSharedParameters(swagger) + if err != nil { + return nil, err + } + for _, requestPath := range SortedMapKeys(swagger.Paths.Map()) { pathItem := swagger.Paths.Value(requestPath) if pathItem == nil { @@ -1424,10 +1706,12 @@ func CallbackOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, e if cbPathItem == nil { continue } - globalParams, err := DescribeParameters(cbPathItem.Parameters, nil) - if err != nil { - return nil, fmt.Errorf("error describing callback %q parameters: %w", callbackName, err) - } + // Path-item-level parameters shared by every method on + // the callback path item; helper types declared once + // (issue #2090). + globalParams := sharedParams[cbPathItem] + sharedParamTypeDefs := sharedParameterTypeDefs(globalParams) + sharedTypeDefsEmitted := false cbOps := cbPathItem.Operations() for _, opName := range SortedMapKeys(cbOps) { @@ -1486,6 +1770,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) } } @@ -1816,8 +2104,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/schema.go b/pkg/codegen/schema.go index e8d6500db..e5959d2d3 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -303,6 +303,13 @@ type TypeDefinition struct { // DeprecationReason is the human-readable reason for deprecation, used // when Deprecated is true. If empty, a generic message is emitted. DeprecationReason string + + // Comment, when set, is the full doc comment (including the leading "//") + // emitted above the type in the operation parameter template, replacing the + // default " defines parameters for ." line. Used to explain + // non-obvious generated names, e.g. the per-path hash prefix on a shared + // parameter disambiguated across paths (issue #2090). + Comment string } // DeprecationComment returns a Go-style deprecation comment if the type is diff --git a/pkg/codegen/templates/param-types.tmpl b/pkg/codegen/templates/param-types.tmpl index bb107b8f0..bccf29102 100644 --- a/pkg/codegen/templates/param-types.tmpl +++ b/pkg/codegen/templates/param-types.tmpl @@ -1,6 +1,6 @@ {{range .}}{{$opid := .OperationId}} {{range .TypeDefinitions}} -// {{.TypeName}} defines parameters for {{$opid}}. +{{if .Comment}}{{.Comment}}{{else}}// {{.TypeName}} defines parameters for {{$opid}}.{{end}} type {{.TypeName}} {{if .IsAlias}}={{end}} {{.Schema.TypeDecl}} {{end}} {{end}}