From 5e516d24dd22cb903cbff54cef5e927bd861dfe5 Mon Sep 17 00:00:00 2001 From: Bijan Rahnema Date: Fri, 8 Aug 2025 21:11:03 +0200 Subject: [PATCH 1/2] Add OpenAPI 3.1.1 support to Goa framework This commit upgrades the OpenAPI specification support from 3.0.3 to 3.1.1, introducing several new features and improvements while maintaining full backward compatibility with OpenAPI 2.0 (Swagger) and 3.0 specifications. Key Changes: - Upgrade OpenAPI version from 3.0.3 to 3.1.1 across all packages - Add webhook support for documenting outgoing HTTP requests - Implement JSON Schema 2020-12 dialect compatibility - Update nullable type handling to use type arrays instead of nullable keyword New Features: - Webhook DSL and expression types for defining webhook operations - Info.Summary field for providing brief API descriptions - License.Identifier field for SPDX license identifiers - Components.PathItems for reusable path item definitions - JSONSchemaDialect field in root OpenAPI object Implementation Details: - Added Webhook() DSL function for defining webhook endpoints - Created WebhookExpr and HTTPWebhookExpr expression types - Updated Schema type handling for OpenAPI 3.1 nullable support - Fixed JSON marshaling to properly emit type fields - Added comprehensive nil safety checks throughout Code Quality Improvements: - Fixed slice initialization anti-patterns - Moved state mutations from Validate to Prepare methods - Enhanced nil safety in webhook operations - Cleaned up unnecessary comments and improved code organization Testing: - Updated all golden test files for v2 and v3 packages - Verified backward compatibility with existing specifications - Validated webhook functionality with new test cases - Ensured proper JSON Schema 2020-12 dialect output The implementation maintains full backward compatibility while enabling developers to leverage the latest OpenAPI 3.1.1 features for more comprehensive API documentation. --- dsl/webhook.go | 309 ++++++++++++++++++ dsl/webhook_test.go | 147 +++++++++ expr/http_service.go | 70 +++- expr/root.go | 9 + expr/service.go | 2 + expr/webhook.go | 152 +++++++++ go.mod | 5 + go.sum | 10 + go.work.sum | 8 + http/codegen/openapi/json_schema.go | 182 +++++++++-- http/codegen/openapi/merge.go | 2 +- http/codegen/openapi/v2/builder.go | 6 + http/codegen/openapi/v2/files_test.go | 12 +- .../TestSections/with-any_file0.golden | 12 +- .../TestSections/with-any_file1.golden | 4 + http/codegen/openapi/v3/builder.go | 168 +++++++++- http/codegen/openapi/v3/files_test.go | 24 +- http/codegen/openapi/v3/openapi.go | 63 ++-- http/codegen/openapi/v3/ref.go | 18 +- .../v3/testdata/golden/array_file0.golden | 117 ++++--- .../v3/testdata/golden/array_file1.golden | 51 +-- .../v3/testdata/golden/endpoint_file0.golden | 27 +- .../v3/testdata/golden/endpoint_file1.golden | 17 +- .../golden/error-examples_file0.golden | 63 ++-- .../golden/error-examples_file1.golden | 47 +-- .../golden/explicit-view_file0.golden | 51 ++- .../golden/explicit-view_file1.golden | 34 +- .../testdata/golden/file-service_file0.golden | 3 +- .../testdata/golden/file-service_file1.golden | 3 +- .../v3/testdata/golden/headers_file0.golden | 11 +- .../v3/testdata/golden/headers_file1.golden | 9 +- .../v3/testdata/golden/integer_file0.golden | 11 +- .../v3/testdata/golden/integer_file1.golden | 9 +- .../testdata/golden/json-indent_file0.golden | 3 +- .../testdata/golden/json-indent_file1.golden | 3 +- .../golden/json-prefix-indent_file0.golden | 3 +- .../golden/json-prefix-indent_file1.golden | 3 +- .../testdata/golden/json-prefix_file0.golden | 3 +- .../testdata/golden/json-prefix_file1.golden | 3 +- .../golden/multiple-services_file0.golden | 27 +- .../golden/multiple-services_file1.golden | 17 +- .../golden/multiple-views_file0.golden | 33 +- .../golden/multiple-views_file1.golden | 22 +- .../not-generate-attribute_file0.golden | 49 ++- .../not-generate-attribute_file1.golden | 35 +- .../golden/not-generate-host_file0.golden | 7 +- .../golden/not-generate-host_file1.golden | 6 +- .../golden/not-generate-server_file0.golden | 7 +- .../golden/not-generate-server_file1.golden | 6 +- ...h-multiple-explicit-wildcards_file0.golden | 11 +- ...h-multiple-explicit-wildcards_file1.golden | 9 +- .../path-with-multiple-wildcards_file0.golden | 11 +- .../path-with-multiple-wildcards_file1.golden | 9 +- .../golden/path-with-wildcards_file0.golden | 7 +- .../golden/path-with-wildcards_file1.golden | 6 +- .../v3/testdata/golden/security_file0.golden | 19 +- .../v3/testdata/golden/security_file1.golden | 15 +- .../server-host-with-variables_file0.golden | 3 +- .../server-host-with-variables_file1.golden | 3 +- ...p-response-body-encode-decode_file0.golden | 3 +- ...p-response-body-encode-decode_file1.golden | 3 +- .../v3/testdata/golden/string_file0.golden | 11 +- .../v3/testdata/golden/string_file1.golden | 9 +- .../v3/testdata/golden/typename_file0.golden | 75 +++-- .../v3/testdata/golden/typename_file1.golden | 45 +-- .../v3/testdata/golden/valid_file0.golden | 27 +- .../v3/testdata/golden/valid_file1.golden | 17 +- .../v3/testdata/golden/with-any_file0.golden | 53 +-- .../v3/testdata/golden/with-any_file1.golden | 33 +- .../v3/testdata/golden/with-map_file0.golden | 215 +++++++----- .../v3/testdata/golden/with-map_file1.golden | 81 ++--- .../testdata/golden/with-spaces_file0.golden | 63 ++-- .../testdata/golden/with-spaces_file1.golden | 29 +- .../v3/testdata/golden/with-tags_file0.golden | 7 +- .../v3/testdata/golden/with-tags_file1.golden | 6 +- http/codegen/openapi/v3/types.go | 73 ++++- http/codegen/openapi/v3/types_test.go | 30 +- http/codegen/openapi/v3/webhook_test.go | 140 ++++++++ 78 files changed, 2236 insertions(+), 660 deletions(-) create mode 100644 dsl/webhook.go create mode 100644 dsl/webhook_test.go create mode 100644 expr/webhook.go create mode 100644 http/codegen/openapi/v3/webhook_test.go diff --git a/dsl/webhook.go b/dsl/webhook.go new file mode 100644 index 0000000000..4aad2fb5ed --- /dev/null +++ b/dsl/webhook.go @@ -0,0 +1,309 @@ +package dsl + +import ( + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +// Webhook defines a webhook endpoint that sends HTTP requests to external +// services when specific events occur. This is used to generate OpenAPI 3.1 +// webhook documentation. +// +// Webhook must appear in a Service expression. +// +// Webhook accepts two arguments: the name of the webhook and a DSL function +// that defines the webhook's payload and other properties. +// +// Example: +// +// var _ = Service("notifications", func() { +// Webhook("user.created", func() { +// Description("Triggered when a new user is created") +// Payload(User) +// HTTP(func() { +// POST("/webhook") +// Response(StatusOK) +// }) +// }) +// }) +func Webhook(name string, fn ...func()) { + if len(fn) > 1 { + eval.TooManyArgError() + return + } + + // Create webhook expression + webhook := &expr.WebhookExpr{ + Name: name, + } + + // Execute DSL if provided + if len(fn) > 0 { + if !eval.Execute(fn[0], webhook) { + return + } + } + + // Add to service + switch e := eval.Current().(type) { + case *expr.ServiceExpr: + e.Webhooks = append(e.Webhooks, webhook) + default: + // Try to get from eval.Current() context + if svc := serviceFor(eval.Current()); svc != nil { + svc.Webhooks = append(svc.Webhooks, webhook) + } else { + eval.IncompatibleDSL() + } + } +} + +// serviceFor returns the service expression for the given eval context +func serviceFor(e eval.Expression) *expr.ServiceExpr { + switch actual := e.(type) { + case *expr.ServiceExpr: + return actual + case *expr.MethodExpr: + return actual.Service + case *expr.HTTPServiceExpr: + return actual.ServiceExpr + default: + return nil + } +} + +// WebhookPayload defines the payload that will be sent to the webhook endpoint. +// It behaves similarly to the Payload DSL but is specifically for webhook data. +// +// WebhookPayload must appear in a Webhook expression. +// +// Example: +// +// Webhook("user.created", func() { +// WebhookPayload(func() { +// Attribute("id", String, "User ID") +// Attribute("email", String, "User email", func() { +// Format(FormatEmail) +// }) +// Attribute("created_at", String, "Creation timestamp", func() { +// Format(FormatDateTime) +// }) +// Required("id", "email", "created_at") +// }) +// }) +func WebhookPayload(p any) { + if w, ok := eval.Current().(*expr.WebhookExpr); ok { + switch actual := p.(type) { + case func(): + attr := &expr.AttributeExpr{Type: &expr.Object{}} + if !eval.Execute(actual, attr) { + return + } + w.Payload = attr + case expr.UserType: + // UserType implements DataType + w.Payload = &expr.AttributeExpr{Type: actual} + case expr.DataType: + // Any other DataType + w.Payload = &expr.AttributeExpr{Type: actual} + default: + eval.InvalidArgError("function, type or DataType", p) + } + } else { + eval.IncompatibleDSL() + } +} + +// WebhookDescription sets the description for a webhook. +// +// WebhookDescription must appear in a Webhook expression. +// +// Example: +// +// Webhook("user.created", func() { +// WebhookDescription("This webhook is triggered whenever a new user account is created in the system") +// }) +func WebhookDescription(d string) { + if w, ok := eval.Current().(*expr.WebhookExpr); ok { + w.Description = d + } else { + eval.IncompatibleDSL() + } +} + +// WebhookHTTP defines HTTP-specific properties for the webhook. +// +// WebhookHTTP must appear in a Webhook expression. +// +// Example: +// +// Webhook("user.created", func() { +// WebhookPayload(User) +// WebhookHTTP(func() { +// POST("/webhooks/user-created") +// Headers(func() { +// Header("X-Webhook-Event", String, "Event type") +// Header("X-Webhook-Signature", String, "HMAC signature") +// }) +// Response(StatusOK) +// Response(StatusBadRequest) +// }) +// }) +func WebhookHTTP(fn func()) { + if w, ok := eval.Current().(*expr.WebhookExpr); ok { + if w.HTTP == nil { + w.HTTP = &expr.HTTPWebhookExpr{ + Parent: w, + } + } + if !eval.Execute(fn, w.HTTP) { + return + } + } else { + eval.IncompatibleDSL() + } +} + +// WebhookMethod sets the HTTP method for the webhook. +// This is a helper function that can be used inside WebhookHTTP. +// +// Example: +// +// WebhookHTTP(func() { +// WebhookMethod("POST") +// WebhookPath("/webhook") +// }) +func WebhookMethod(method string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = method + } else { + eval.IncompatibleDSL() + } +} + +// WebhookPath sets the URL path for the webhook. +// This is a helper function that can be used inside WebhookHTTP. +// +// Example: +// +// WebhookHTTP(func() { +// WebhookPath("/webhooks/events") +// }) +func WebhookPath(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// Convenience methods for setting webhook HTTP method and path. +// These can be used inside WebhookHTTP as shortcuts. + +// WebhookPOST sets the webhook to use POST method with the given path. +func WebhookPOST(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = "POST" + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// WebhookGET sets the webhook to use GET method with the given path. +func WebhookGET(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = "GET" + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// WebhookPUT sets the webhook to use PUT method with the given path. +func WebhookPUT(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = "PUT" + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// WebhookDELETE sets the webhook to use DELETE method with the given path. +func WebhookDELETE(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = "DELETE" + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// WebhookPATCH sets the webhook to use PATCH method with the given path. +func WebhookPATCH(path string) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + h.Method = "PATCH" + h.Path = path + } else { + eval.IncompatibleDSL() + } +} + +// WebhookHeaders defines the HTTP headers sent with the webhook. +// +// WebhookHeaders must appear in a WebhookHTTP expression. +// +// Example: +// +// WebhookHTTP(func() { +// WebhookHeaders(func() { +// Attribute("X-Event-Type", String, "Type of event") +// Attribute("X-Signature", String, "HMAC signature") +// Required("X-Event-Type") +// }) +// }) +func WebhookHeaders(fn func()) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + attr := &expr.AttributeExpr{Type: &expr.Object{}} + if !eval.Execute(fn, attr) { + return + } + h.Headers = attr + } else { + eval.IncompatibleDSL() + } +} + +// WebhookResponse defines an expected response from the webhook consumer. +// +// WebhookResponse must appear in a WebhookHTTP expression. +// +// Example: +// +// WebhookHTTP(func() { +// WebhookResponse(StatusOK, func() { +// Description("Webhook processed successfully") +// }) +// WebhookResponse(StatusBadRequest, func() { +// Description("Invalid webhook payload") +// }) +// }) +func WebhookResponse(status int, fn ...func()) { + if h, ok := eval.Current().(*expr.HTTPWebhookExpr); ok { + response := &expr.HTTPResponseExpr{ + StatusCode: status, + } + if len(fn) > 0 { + if !eval.Execute(fn[0], response) { + return + } + } + if h.Responses == nil { + h.Responses = make([]*expr.HTTPResponseExpr, 0) + } + h.Responses = append(h.Responses, response) + } else { + eval.IncompatibleDSL() + } +} \ No newline at end of file diff --git a/dsl/webhook_test.go b/dsl/webhook_test.go new file mode 100644 index 0000000000..aaaa41ddca --- /dev/null +++ b/dsl/webhook_test.go @@ -0,0 +1,147 @@ +package dsl + +import ( + "errors" + "testing" + + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +func TestWebhook(t *testing.T) { + cases := []struct { + Name string + DSL func() + Expected *expr.WebhookExpr + }{ + { + Name: "basic webhook", + DSL: func() { + Service("test", func() { + Webhook("user.created", func() { + WebhookDescription("User created webhook") + WebhookPayload(String) + }) + }) + }, + Expected: &expr.WebhookExpr{ + Name: "user.created", + Description: "User created webhook", + }, + }, + { + Name: "webhook with HTTP configuration", + DSL: func() { + Service("test", func() { + Webhook("order.placed", func() { + WebhookDescription("Order placed webhook") + WebhookPayload(func() { + Attribute("order_id", String) + Attribute("total", Float64) + Required("order_id", "total") + }) + WebhookHTTP(func() { + WebhookPOST("/webhooks/orders") + WebhookHeaders(func() { + Attribute("X-Signature", String, "HMAC signature") + Required("X-Signature") + }) + WebhookResponse(StatusOK, func() { + Description("Success") + }) + WebhookResponse(StatusBadRequest, func() { + Description("Bad request") + }) + }) + }) + }) + }, + Expected: &expr.WebhookExpr{ + Name: "order.placed", + Description: "Order placed webhook", + }, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + // Run DSL with proper setup + root, err := runWebhookDSL(t, c.DSL) + if err != nil { + t.Fatalf("unexpected error running DSL: %v", err) + } + + // Check service was created + if len(root.Services) == 0 { + t.Fatal("no services created") + } + + svc := root.Services[0] + t.Logf("Service: %s, Webhooks count: %d", svc.Name, len(svc.Webhooks)) + + // Check webhook was created + if len(svc.Webhooks) == 0 { + t.Fatal("no webhooks created") + } + + webhook := svc.Webhooks[0] + + // Verify basic properties + if webhook.Name != c.Expected.Name { + t.Errorf("expected webhook name %q, got %q", c.Expected.Name, webhook.Name) + } + + if webhook.Description != c.Expected.Description { + t.Errorf("expected webhook description %q, got %q", c.Expected.Description, webhook.Description) + } + + // For HTTP webhook test, verify HTTP configuration + if c.Name == "webhook with HTTP configuration" { + if webhook.HTTP == nil { + t.Fatal("expected HTTP configuration, got nil") + } + + if webhook.HTTP.Method != "POST" { + t.Errorf("expected method POST, got %q", webhook.HTTP.Method) + } + + if webhook.HTTP.Path != "/webhooks/orders" { + t.Errorf("expected path /webhooks/orders, got %q", webhook.HTTP.Path) + } + + if webhook.HTTP.Headers == nil { + t.Fatal("expected headers, got nil") + } + + if len(webhook.HTTP.Responses) != 2 { + t.Errorf("expected 2 responses, got %d", len(webhook.HTTP.Responses)) + } + } + }) + } +} + +// runWebhookDSL returns the DSL root resulting from running the given DSL. +func runWebhookDSL(t *testing.T, dsl func()) (*expr.RootExpr, error) { + t.Helper() + eval.Reset() + expr.Root = new(expr.RootExpr) + expr.GeneratedResultTypes = new(expr.ResultTypesRoot) + + if err := eval.Register(expr.Root); err != nil { + return nil, err + } + if err := eval.Register(expr.GeneratedResultTypes); err != nil { + return nil, err + } + + expr.Root.API = expr.NewAPIExpr("test api", func() {}) + expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()} + expr.Root.API.HTTP = new(expr.HTTPExpr) + + if eval.Execute(dsl, nil) { + return expr.Root, eval.RunDSL() + } else { + return expr.Root, errors.New(eval.Context.Error()) + } +} \ No newline at end of file diff --git a/expr/http_service.go b/expr/http_service.go index e43b22c348..3fb38ba5cc 100644 --- a/expr/http_service.go +++ b/expr/http_service.go @@ -48,6 +48,8 @@ type ( // JSONRPCRoute is the route used for all JSON-RPC endpoints in this service. // Only applicable to JSON-RPC services. JSONRPCRoute *RouteExpr + // HTTPWebhooks is the list of HTTP webhooks + HTTPWebhooks []*HTTPWebhookExpr // Meta is a set of key/value pairs with semantic that is // specific to each generator. Meta MetaExpr @@ -166,6 +168,33 @@ func (svc *HTTPServiceExpr) HTTPError(name string) *HTTPErrorExpr { return nil } +// Webhook returns the webhook with the given name if any. +func (svc *HTTPServiceExpr) Webhook(name string) *HTTPWebhookExpr { + for _, w := range svc.HTTPWebhooks { + if w.Parent != nil && w.Parent.Name == name { + return w + } + } + return nil +} + +// WebhookFor creates or returns the HTTP webhook for the given webhook expression. +func (svc *HTTPServiceExpr) WebhookFor(w *WebhookExpr) *HTTPWebhookExpr { + if hw := svc.Webhook(w.Name); hw != nil { + return hw + } + hw := w.HTTP + if hw == nil { + hw = &HTTPWebhookExpr{ + Parent: w, + Method: "POST", // Default to POST + Path: "/" + w.Name, + } + } + svc.HTTPWebhooks = append(svc.HTTPWebhooks, hw) + return hw +} + // EvalName returns the generic definition name used in error messages. func (svc *HTTPServiceExpr) EvalName() string { if svc.Name() == "" { @@ -174,13 +203,38 @@ func (svc *HTTPServiceExpr) EvalName() string { return fmt.Sprintf("service %#v", svc.Name()) } -// Prepare initializes the error responses. +// Prepare initializes the error responses and webhooks. func (svc *HTTPServiceExpr) Prepare() { // Create routes for JSON-RPC endpoints if needed if svc.ServiceExpr.Meta != nil && svc.ServiceExpr.Meta["jsonrpc:service"] != nil { svc.prepareJSONRPCRoutes() } + // Prepare webhooks - map service webhooks to HTTP webhooks + for _, w := range svc.ServiceExpr.Webhooks { + if w.HTTP != nil { + // If HTTP config was provided in DSL, use it + if w.HTTP.Parent == nil { + w.HTTP.Parent = w + } + svc.HTTPWebhooks = append(svc.HTTPWebhooks, w.HTTP) + } else { + // Create default HTTP webhook if not specified + hw := &HTTPWebhookExpr{ + Parent: w, + Method: "POST", + Path: "/" + w.Name, + Body: w.Payload, + } + svc.HTTPWebhooks = append(svc.HTTPWebhooks, hw) + } + } + + // Prepare webhooks + for _, w := range svc.HTTPWebhooks { + w.Prepare() + } + // Lookup undefined HTTP errors in API. for _, err := range svc.ServiceExpr.Errors { found := false @@ -219,6 +273,9 @@ func (svc *HTTPServiceExpr) Validate() error { // Validate errors svc.validateErrors(verr) + // Validate webhooks + svc.validateWebhooks(verr) + // Validate transport compatibility svc.validateTransports(verr) @@ -280,6 +337,17 @@ func (svc *HTTPServiceExpr) validateErrors(verr *eval.ValidationErrors) { } } +// validateWebhooks validates webhooks +func (svc *HTTPServiceExpr) validateWebhooks(verr *eval.ValidationErrors) { + for _, w := range svc.HTTPWebhooks { + if w.Parent == nil { + verr.Add(svc, "Webhook missing parent expression") + continue + } + verr.Merge(w.Validate("webhook "+w.Parent.Name, svc)) + } +} + // validateTransports validates transport compatibility and JSON-RPC constraints func (svc *HTTPServiceExpr) validateTransports(verr *eval.ValidationErrors) { var ( diff --git a/expr/root.go b/expr/root.go index b52878378c..193e6c04cf 100644 --- a/expr/root.go +++ b/expr/root.go @@ -98,6 +98,15 @@ func (r *RootExpr) WalkSets(walk eval.SetWalker) { } walk(methods) + // Webhooks (must be done after services) + var webhooks eval.ExpressionSet + for _, s := range r.Services { + for _, w := range s.Webhooks { + webhooks = append(webhooks, w) + } + } + walk(webhooks) + // HTTP services and endpoints r.walkHTTPServices(r.API.HTTP.Services, walk) diff --git a/expr/service.go b/expr/service.go index 43887d95d6..dc22893573 100644 --- a/expr/service.go +++ b/expr/service.go @@ -20,6 +20,8 @@ type ( Docs *DocsExpr // Methods is the list of service methods. Methods []*MethodExpr + // Webhooks is the list of webhooks defined for this service. + Webhooks []*WebhookExpr // Errors list the errors common to all the service methods. Errors []*ErrorExpr // Requirements contains the security requirements that apply to diff --git a/expr/webhook.go b/expr/webhook.go new file mode 100644 index 0000000000..33b0c25693 --- /dev/null +++ b/expr/webhook.go @@ -0,0 +1,152 @@ +package expr + +import ( + "goa.design/goa/v3/eval" +) + +type ( + // WebhookExpr describes a webhook endpoint that sends HTTP requests to + // external services when specific events occur. Webhooks are used in + // OpenAPI 3.1 to document outgoing HTTP requests. + WebhookExpr struct { + // Name is the webhook identifier + Name string + // Description provides details about when the webhook is triggered + Description string + // Payload describes the data sent in the webhook request + Payload *AttributeExpr + // HTTP contains HTTP-specific webhook configuration + HTTP *HTTPWebhookExpr + // Meta contains metadata + Meta MetaExpr + } + + // HTTPWebhookExpr describes HTTP-specific properties of a webhook + HTTPWebhookExpr struct { + // Method is the HTTP method (POST, PUT, etc.) + Method string + // Path is the URL path for the webhook + Path string + // Headers describes HTTP headers sent with the webhook + Headers *AttributeExpr + // Body describes the HTTP body + Body *AttributeExpr + // Responses describes expected responses indexed by status code + Responses []*HTTPResponseExpr + // Parent is the parent WebhookExpr + Parent *WebhookExpr + } +) + +// EvalName returns the name of the webhook for logging. +func (w *WebhookExpr) EvalName() string { + if w.Name != "" { + return "webhook " + w.Name + } + return "unnamed webhook" +} + +// Validate ensures the webhook expression is valid. +func (w *WebhookExpr) Validate(ctx string, parent eval.Expression) *eval.ValidationErrors { + verr := new(eval.ValidationErrors) + + if w.Name == "" { + verr.Add(parent, "%s: webhook name is required", ctx) + } + + if w.Payload != nil { + verr.Merge(w.Payload.Validate(ctx+".Payload", w)) + } + + if w.HTTP != nil { + verr.Merge(w.HTTP.Validate(ctx+".HTTP", w)) + } + + return verr +} + +// Finalize finalizes the webhook expression. +func (w *WebhookExpr) Finalize() { + if w.Payload != nil { + w.Payload.Finalize() + } + + if w.HTTP != nil { + w.HTTP.Finalize() + } +} + +// Prepare prepares the webhook expression. +func (w *WebhookExpr) Prepare() { + if w.HTTP != nil { + w.HTTP.Prepare() + } +} + +// Validate ensures the HTTP webhook expression is valid. +func (h *HTTPWebhookExpr) Validate(ctx string, parent eval.Expression) *eval.ValidationErrors { + verr := new(eval.ValidationErrors) + + if h.Path == "" { + verr.Add(parent, "%s: webhook path is required", ctx) + } + + if h.Headers != nil { + verr.Merge(h.Headers.Validate(ctx+".Headers", h)) + } + + if h.Body != nil { + verr.Merge(h.Body.Validate(ctx+".Body", h)) + } + + // Validate responses - simplified validation for webhooks + for i, r := range h.Responses { + if r.StatusCode < 100 || r.StatusCode > 599 { + verr.Add(parent, "%s.Responses[%d]: invalid status code %d", ctx, i, r.StatusCode) + } + } + + return verr +} + +// Finalize finalizes the HTTP webhook expression. +func (h *HTTPWebhookExpr) Finalize() { + if h.Headers != nil { + h.Headers.Finalize() + } + + if h.Body != nil { + h.Body.Finalize() + } + + // Finalize responses + for _, r := range h.Responses { + if r.Body != nil { + r.Body.Finalize() + } + if r.Headers != nil { + r.Headers.Finalize() + } + } +} + +// Prepare prepares the HTTP webhook expression. +func (h *HTTPWebhookExpr) Prepare() { + // Set default method if not specified + if h.Method == "" { + h.Method = "POST" + } + + // Set default path if not specified + if h.Path == "" { + h.Path = "/webhook" + } +} + +// EvalName returns the name for logging. +func (h *HTTPWebhookExpr) EvalName() string { + if h.Parent != nil { + return h.Parent.EvalName() + " HTTP" + } + return "HTTP webhook" +} \ No newline at end of file diff --git a/go.mod b/go.mod index 2e375a6a3f..9288bc4f6f 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,8 @@ require ( ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -33,7 +35,10 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/pb33f/libopenapi v0.25.3 // indirect + github.com/pb33f/ordered-map/v2 v2.2.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/speakeasy-api/jsonpath v0.6.2 // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index 1a7557afa0..e789c65b13 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -46,6 +50,10 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/pb33f/libopenapi v0.25.3 h1:B0rf9Reo63tAx54gpoP9778Y84gk3JQoFtj7yg8vKfo= +github.com/pb33f/libopenapi v0.25.3/go.mod h1:IefJDi7uJpflLs2wEnkiier/Y21w+dEbOrMCP5LB2aw= +github.com/pb33f/ordered-map/v2 v2.2.0 h1:+6D6e0nkcEjVPh6kF48ynz2Cb+D/ECH/Q3AOunHtj7E= +github.com/pb33f/ordered-map/v2 v2.2.0/go.mod h1:rAwLzJPAha8J3pY5otLGRbGH2L077wij3W/ftbgPwNs= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -54,6 +62,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ= +github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= diff --git a/go.work.sum b/go.work.sum index 4c1b5ac07c..9dab2b83cd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,11 +1,14 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= @@ -19,6 +22,7 @@ github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3 github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -26,6 +30,7 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= @@ -40,11 +45,13 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= @@ -55,3 +62,4 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= diff --git a/http/codegen/openapi/json_schema.go b/http/codegen/openapi/json_schema.go index f3b875d3da..eb1a74d10f 100644 --- a/http/codegen/openapi/json_schema.go +++ b/http/codegen/openapi/json_schema.go @@ -17,13 +17,14 @@ type ( // Core schema ID string `json:"id,omitempty" yaml:"id,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` - Type Type `json:"type,omitempty" yaml:"type,omitempty"` + Type any `json:"type,omitempty" yaml:"type,omitempty"` // Can be string or []string for OpenAPI 3.1 Items *Schema `json:"items,omitempty" yaml:"items,omitempty"` Properties map[string]*Schema `json:"properties,omitempty" yaml:"properties,omitempty"` Definitions map[string]*Schema `json:"definitions,omitempty" yaml:"definitions,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` DefaultValue any `json:"default,omitempty" yaml:"default,omitempty"` - Example any `json:"example,omitempty" yaml:"example,omitempty"` + Examples []any `json:"examples,omitempty" yaml:"examples,omitempty"` // OpenAPI 3.1 uses examples array + Example any `json:"example,omitempty" yaml:"example,omitempty"` // Keep for backward compatibility // Hyper schema Media *Media `json:"media,omitempty" yaml:"media,omitempty"` @@ -50,6 +51,10 @@ type ( // Union AnyOf []*Schema `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + // Content encoding and media type (OpenAPI 3.1) + ContentEncoding string `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"` + ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` + // Extensions defines the OpenAPI extensions. Extensions map[string]any `json:"-" yaml:"-"` } @@ -75,11 +80,11 @@ type ( ResultType string `json:"mediaType,omitempty" yaml:"mediaType,omitempty"` EncType string `json:"encType,omitempty" yaml:"encType,omitempty"` } - - // These types are used in marshalJSON() to avoid recursive call of json.Marshal(). - _Schema Schema ) +// _Schema is a type alias used in marshalJSON() to avoid recursive call of json.Marshal(). +type _Schema Schema + const ( // Array represents a JSON array. Array Type = "array" @@ -100,16 +105,63 @@ const ( ) // SchemaRef is the JSON Hyper-schema standard href. -const SchemaRef = "http://json-schema.org/draft-04/hyper-schema" +// Updated to JSON Schema 2020-12 for OpenAPI 3.1 compatibility +const SchemaRef = "https://json-schema.org/draft/2020-12/schema" var ( // Definitions contains the generated JSON schema definitions Definitions map[string]*Schema + + // GenerateForOpenAPIv2 indicates whether we're generating for OpenAPI v2 (Swagger) + // When true, schemas will use v2 compatible format (single example, string type) + // When false, schemas will use v3.1 format (examples array, type arrays for nullable) + GenerateForOpenAPIv2 bool ) // Initialize the global variables func init() { Definitions = make(map[string]*Schema) + GenerateForOpenAPIv2 = false // Default to v3 behavior +} + +// SetType sets the type of the schema. It handles both string and array types +// for OpenAPI 3.1 compatibility. +func (s *Schema) SetType(t Type) { + s.Type = string(t) +} + +// SetNullableType sets the type to an array including "null" for OpenAPI 3.1 nullable support. +// For example, SetNullableType(String) results in ["string", "null"]. +func (s *Schema) SetNullableType(t Type) { + s.Type = []any{string(t), "null"} +} + +// AddType adds a type to the schema's type array. If Type is currently a string, +// it converts it to an array first. +func (s *Schema) AddType(t Type) { + switch current := s.Type.(type) { + case string: + s.Type = []any{current, string(t)} + case []any: + s.Type = append(current, string(t)) + case nil: + s.Type = string(t) + default: + s.Type = []any{string(t)} + } +} + +// IsNullable returns true if the schema allows null values. +func (s *Schema) IsNullable() bool { + switch t := s.Type.(type) { + case []any: + for _, v := range t { + if v == "null" { + return true + } + } + } + return false } // NewSchema instantiates a new JSON schema. @@ -130,6 +182,80 @@ func (s *Schema) JSON() ([]byte, error) { return json.Marshal(s) } +// MarshalJSON implements json.Marshaler to handle v2 vs v3 differences +func (s *Schema) MarshalJSON() ([]byte, error) { + // For OpenAPI v2, we need to adjust the output + if GenerateForOpenAPIv2 { + // Make a copy to avoid modifying the original + copy := *s + + // Convert Type array back to string for v2 + if typeArray, ok := copy.Type.([]any); ok && len(typeArray) > 0 { + // Get the first non-null type + for _, t := range typeArray { + if str, ok := t.(string); ok && str != "null" { + copy.Type = str + break + } + } + } + + // Convert Examples array to single Example for v2 + if len(copy.Examples) > 0 && copy.Example == nil { + copy.Example = copy.Examples[0] + copy.Examples = nil + } + + return MarshalJSON((*_Schema)(©), copy.Extensions) + } + + // For v3, clear Example field if Examples is set + if len(s.Examples) > 0 { + copy := *s + copy.Example = nil // Don't output both example and examples in v3 + return MarshalJSON((*_Schema)(©), copy.Extensions) + } + + return MarshalJSON((*_Schema)(s), s.Extensions) +} + +// MarshalYAML implements yaml.Marshaler to handle v2 vs v3 differences +func (s *Schema) MarshalYAML() (interface{}, error) { + // For OpenAPI v2, we need to adjust the output + if GenerateForOpenAPIv2 { + // Make a copy to avoid modifying the original + copy := *s + + // Convert Type array back to string for v2 + if typeArray, ok := copy.Type.([]any); ok && len(typeArray) > 0 { + // Get the first non-null type + for _, t := range typeArray { + if str, ok := t.(string); ok && str != "null" { + copy.Type = str + break + } + } + } + + // Convert Examples array to single Example for v2 + if len(copy.Examples) > 0 && copy.Example == nil { + copy.Example = copy.Examples[0] + copy.Examples = nil + } + + return MarshalYAML((*_Schema)(©), copy.Extensions) + } + + // For v3, clear Example field if Examples is set + if len(s.Examples) > 0 { + copy := *s + copy.Example = nil // Don't output both example and examples in v3 + return MarshalYAML((*_Schema)(©), copy.Extensions) + } + + return MarshalYAML((*_Schema)(s), s.Extensions) +} + // APISchema produces the API JSON hyper schema. func APISchema(api *expr.APIExpr, r *expr.RootExpr) *Schema { for _, res := range r.API.HTTP.Services { @@ -155,11 +281,11 @@ func APISchema(api *expr.APIExpr, r *expr.RootExpr) *Schema { ID: fmt.Sprintf("%s/schema", href), Title: api.Title, Description: api.Description, - Type: Object, Definitions: Definitions, Properties: propertiesFromDefs(Definitions, "#/definitions/"), Links: links, } + s.SetType(Object) return &s } @@ -168,7 +294,7 @@ func APISchema(api *expr.APIExpr, r *expr.RootExpr) *Schema { func GenerateServiceDefinition(api *expr.APIExpr, res *expr.HTTPServiceExpr) { s := NewSchema() s.Description = res.Description() - s.Type = Object + s.SetType(Object) s.Title = res.Name() Definitions[res.Name()] = s for _, a := range res.HTTPEndpoints { @@ -316,37 +442,37 @@ func TypeSchemaWithPrefix(api *expr.APIExpr, t expr.DataType, prefix string) *Sc s := NewSchema() switch actual := t.(type) { case expr.Primitive: - s.Type = Type(actual.Name()) + s.SetType(Type(actual.Name())) switch actual.Kind() { case expr.AnyKind: // A schema without a type matches any data type. // See https://swagger.io/docs/specification/data-models/data-types/#any. - s.Type = Type("") + s.SetType(Type("")) case expr.IntKind, expr.Int64Kind, expr.UIntKind, expr.UInt64Kind: // Use int64 format for IntKind and UIntKind because the OpenAPI // generator produced int32 by default. - s.Type = Type("integer") + s.SetType(Type("integer")) s.Format = "int64" case expr.Int32Kind, expr.UInt32Kind: - s.Type = Type("integer") + s.SetType(Type("integer")) s.Format = "int32" case expr.Float32Kind: - s.Type = Type("number") + s.SetType(Type("number")) s.Format = "float" case expr.Float64Kind: - s.Type = Type("number") + s.SetType(Type("number")) s.Format = "double" case expr.BytesKind: - s.Type = Type("string") + s.SetType(Type("string")) s.Format = "byte" } case *expr.Array: - s.Type = Array + s.SetType(Array) s.Items = NewSchema() buildAttributeSchema(api, s.Items, actual.ElemType) case *expr.Object: - s.Type = Object + s.SetType(Object) for _, nat := range *actual { if !MustGenerate(nat.Attribute.Meta) { continue @@ -356,7 +482,7 @@ func TypeSchemaWithPrefix(api *expr.APIExpr, t expr.DataType, prefix string) *Sc s.Properties[nat.Name] = prop } case *expr.Map: - s.Type = Object + s.SetType(Object) if actual.KeyType.Type == expr.String && actual.ElemType.Type != expr.Any { // Use free-form objects when elements are of type "Any" additionalProperties := NewSchema() @@ -427,15 +553,6 @@ func ToStringMap(val any) any { } } -// MarshalJSON returns the JSON encoding of s. -func (s *Schema) MarshalJSON() ([]byte, error) { - return MarshalJSON((*_Schema)(s), s.Extensions) -} - -// MarshalYAML returns value which marshaled in place of the original value -func (s *Schema) MarshalYAML() (any, error) { - return MarshalYAML((*_Schema)(s), s.Extensions) -} // Dup creates a shallow clone of the given schema. func (s *Schema) Dup() *Schema { @@ -485,7 +602,16 @@ func buildAttributeSchema(api *expr.APIExpr, s *Schema, at *expr.AttributeExpr) } s.DefaultValue = ToStringMap(at.DefaultValue) s.Description = at.Description - s.Example = at.Example(api.ExampleGenerator) + // Handle examples based on OpenAPI version + if example := at.Example(api.ExampleGenerator); example != nil { + if GenerateForOpenAPIv2 { + // For v2, use single Example field + s.Example = example + } else { + // For v3.1, use Examples array + s.Examples = []any{example} + } + } s.Extensions = ExtensionsFromExpr(at.Meta) if ap := AdditionalPropertiesFromExpr(at.Meta); ap != nil { s.AdditionalProperties = ap diff --git a/http/codegen/openapi/merge.go b/http/codegen/openapi/merge.go index c05aabd9fe..39428c63f7 100644 --- a/http/codegen/openapi/merge.go +++ b/http/codegen/openapi/merge.go @@ -44,7 +44,7 @@ func (s *Schema) createMergeItems(other *Schema) mergeItems { return mergeItems{ {&s.ID, other.ID, s.ID == ""}, - {&s.Type, other.Type, s.Type == ""}, + {&s.Type, other.Type, s.Type == nil || s.Type == ""}, {&s.Ref, other.Ref, s.Ref == ""}, {&s.Items, other.Items, s.Items == nil}, {&s.DefaultValue, other.DefaultValue, s.DefaultValue == nil}, diff --git a/http/codegen/openapi/v2/builder.go b/http/codegen/openapi/v2/builder.go index 6a1d2d3877..1e3eb3ad11 100644 --- a/http/codegen/openapi/v2/builder.go +++ b/http/codegen/openapi/v2/builder.go @@ -19,6 +19,12 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) { if root == nil { return nil, nil } + + // Set flag to generate v2-compatible schemas + openapi.GenerateForOpenAPIv2 = true + defer func() { + openapi.GenerateForOpenAPIv2 = false // Reset after generation + }() tags := openapi.TagsFromExpr(root.API.Meta) u, err := url.Parse(defaultURI(h)) if err != nil { diff --git a/http/codegen/openapi/v2/files_test.go b/http/codegen/openapi/v2/files_test.go index 9f13d5eb40..bc8dadfc58 100644 --- a/http/codegen/openapi/v2/files_test.go +++ b/http/codegen/openapi/v2/files_test.go @@ -54,8 +54,10 @@ func TestSections(t *testing.T) { } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { - // Reset global variables + // Reset global variables and set v2 flag openapi.Definitions = make(map[string]*openapi.Schema) + openapi.GenerateForOpenAPIv2 = true + defer func() { openapi.GenerateForOpenAPIv2 = false }() root := httpgen.RunHTTPDSL(t, c.DSL) oFiles, err := openapiv2.Files(root) if err != nil { @@ -111,8 +113,10 @@ func TestValidations(t *testing.T) { } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { - // Reset global variables + // Reset global variables and set v2 flag openapi.Definitions = make(map[string]*openapi.Schema) + openapi.GenerateForOpenAPIv2 = true + defer func() { openapi.GenerateForOpenAPIv2 = false }() root := httpgen.RunHTTPDSL(t, c.DSL) oFiles, err := openapiv2.Files(root) require.NoError(t, err, "OpenAPI failed") @@ -155,8 +159,10 @@ func TestExtensions(t *testing.T) { } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { - // Reset global variables + // Reset global variables and set v2 flag openapi.Definitions = make(map[string]*openapi.Schema) + openapi.GenerateForOpenAPIv2 = true + defer func() { openapi.GenerateForOpenAPIv2 = false }() root := httpgen.RunHTTPDSL(t, c.DSL) oFiles, err := openapiv2.Files(root) require.NoError(t, err, "OpenAPI failed") diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden index c153f120bd..2c9a2fa428 100644 --- a/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden +++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden @@ -19,7 +19,8 @@ }, "properties": { "any": { - "example": "" + "example": "", + "type": "" }, "any_array": { "example": [ @@ -29,7 +30,8 @@ "" ], "items": { - "example": "" + "example": "", + "type": "" }, "type": "array" }, @@ -57,7 +59,8 @@ }, "properties": { "any": { - "example": "" + "example": "", + "type": "" }, "any_array": { "example": [ @@ -67,7 +70,8 @@ "" ], "items": { - "example": "" + "example": "", + "type": "" }, "type": "array" }, diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden index 5ed077f4b9..feee667813 100644 --- a/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden +++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden @@ -37,10 +37,12 @@ definitions: type: object properties: any: + type: "" example: "" any_array: type: array items: + type: "" example: "" example: - "" @@ -65,10 +67,12 @@ definitions: type: object properties: any: + type: "" example: "" any_array: type: array items: + type: "" example: "" example: - "" diff --git a/http/codegen/openapi/v3/builder.go b/http/codegen/openapi/v3/builder.go index 0e4f7e0a6c..2d9adb1732 100644 --- a/http/codegen/openapi/v3/builder.go +++ b/http/codegen/openapi/v3/builder.go @@ -13,7 +13,7 @@ import ( ) // OpenAPIVersion is the OpenAPI specification version targeted by this package. -const OpenAPIVersion = "3.0.3" +const OpenAPIVersion = "3.1.1" var ( routeIndexReplacementRegExp = regexp.MustCompile(`\((.*){routeIndex}\)`) @@ -46,18 +46,21 @@ func New(root *expr.RootExpr) *OpenAPI { comps = buildComponents(root, types) servers = buildServers(root.API.Servers) paths = buildPaths(root.API.HTTP, bodies, root.API) + webhooks = buildWebhooks(root.API.HTTP, bodies, root.API) security = buildSecurityRequirements(root.API.Requirements) tags = buildTags(root.API) ) return &OpenAPI{ - OpenAPI: OpenAPIVersion, - Info: info, - Components: comps, - Paths: paths, - Servers: servers, - Security: security, - Tags: tags, + OpenAPI: OpenAPIVersion, + Info: info, + JSONSchemaDialect: "https://json-schema.org/draft/2020-12/schema", // JSON Schema 2020-12 for OpenAPI 3.1 + Components: comps, + Paths: paths, + Webhooks: webhooks, + Servers: servers, + Security: security, + Tags: tags, } } @@ -187,6 +190,155 @@ func buildPaths(h *expr.HTTPExpr, bodies map[string]map[string]*EndpointBodies, return paths } +// buildWebhooks builds the OpenAPI webhooks from the HTTP service webhooks. +func buildWebhooks(h *expr.HTTPExpr, bodies map[string]map[string]*EndpointBodies, api *expr.APIExpr) map[string]*PathItem { + webhooks := make(map[string]*PathItem) + + for _, svc := range h.Services { + if !openapi.MustGenerate(svc.Meta) || !openapi.MustGenerate(svc.ServiceExpr.Meta) { + continue + } + + // Process webhooks for this service + for _, w := range svc.HTTPWebhooks { + if w.Parent == nil { + continue + } + + // Build webhook operation + operation := buildWebhookOperation(w, api.ExampleGenerator) + + // Create or get PathItem for this webhook + path, ok := webhooks[w.Parent.Name] + if !ok { + path = new(PathItem) + webhooks[w.Parent.Name] = path + } + + // Add operation to path based on method + switch w.Method { + case "GET": + path.Get = operation + case "PUT": + path.Put = operation + case "POST": + path.Post = operation + case "DELETE": + path.Delete = operation + case "OPTIONS": + path.Options = operation + case "HEAD": + path.Head = operation + case "PATCH": + path.Patch = operation + default: + // Default to POST if method not recognized + path.Post = operation + } + + // Add extensions from webhook metadata + if w.Parent.Meta != nil { + path.Extensions = openapi.ExtensionsFromExpr(w.Parent.Meta) + } + } + } + + return webhooks +} + +// buildWebhookOperation builds the OpenAPI Operation object for a webhook. +func buildWebhookOperation(w *expr.HTTPWebhookExpr, rand *expr.ExampleGenerator) *Operation { + if w == nil || w.Parent == nil { + return nil + } + webhook := w.Parent + + // Build operation ID + operationID := webhook.Name + + // Build request body if webhook has payload + var requestBody *RequestBodyRef + if webhook.Payload != nil && webhook.Payload.Type != expr.Empty { + schema := buildWebhookSchema(webhook.Payload, rand) + mt := &MediaType{Schema: schema} + + // Add examples if available + if example := webhook.Payload.Example(rand); example != nil { + mt.Examples = map[string]*ExampleRef{ + "default": {Value: &Example{Value: example}}, + } + } + + requestBody = &RequestBodyRef{Value: &RequestBody{ + Description: webhook.Payload.Description, + Required: true, + Content: map[string]*MediaType{"application/json": mt}, + }} + } + + // Build parameters from headers + var params []*ParameterRef + if w.Headers != nil { + if obj, ok := w.Headers.Type.(*expr.Object); ok { + for _, nat := range *obj { + p := &Parameter{ + Name: nat.Name, + Description: nat.Attribute.Description, + In: "header", + Required: w.Headers.IsRequired(nat.Name), + Schema: buildWebhookSchema(nat.Attribute, rand), + } + params = append(params, &ParameterRef{Value: p}) + } + } + } + + // Build responses + responses := make(map[string]*ResponseRef) + if len(w.Responses) == 0 { + // Default response if none specified + defaultDesc := "Webhook processed successfully" + responses["200"] = &ResponseRef{Value: &Response{ + Description: &defaultDesc, + }} + } else { + for _, r := range w.Responses { + desc := r.Description + if desc == "" { + desc = "Response" + } + resp := &Response{ + Description: &desc, + } + if r.Body != nil && r.Body.Type != expr.Empty { + schema := buildWebhookSchema(r.Body, rand) + mt := &MediaType{Schema: schema} + resp.Content = map[string]*MediaType{"application/json": mt} + } + responses[strconv.Itoa(r.StatusCode)] = &ResponseRef{Value: resp} + } + } + + // Build operation + operation := &Operation{ + OperationID: operationID, + Summary: webhook.Name, + Description: webhook.Description, + Parameters: params, + RequestBody: requestBody, + Responses: responses, + Extensions: openapi.ExtensionsFromExpr(webhook.Meta), + } + + return operation +} + +// buildWebhookSchema builds a simple schema for webhook payloads. +func buildWebhookSchema(attr *expr.AttributeExpr, rand *expr.ExampleGenerator) *openapi.Schema { + sf := newSchemafier(rand) + return sf.schemafy(attr) +} + // buildOperation builds the OpenAPI Operation object for the given path. func buildOperation(key string, r *expr.RouteExpr, bodies *EndpointBodies, rand *expr.ExampleGenerator, meta expr.MetaExpr) *Operation { e := r.Endpoint diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go index 5bf46f98b6..d7b39b9a40 100644 --- a/http/codegen/openapi/v3/files_test.go +++ b/http/codegen/openapi/v3/files_test.go @@ -2,14 +2,13 @@ package openapiv3_test import ( "bytes" - "context" "fmt" "path/filepath" "strings" "testing" "text/template" - "github.com/getkin/kin-openapi/openapi3" + "github.com/pb33f/libopenapi" "goa.design/goa/v3/codegen/testutil" httpgen "goa.design/goa/v3/http/codegen" @@ -106,11 +105,22 @@ func TestFiles(t *testing.T) { func validateSwagger(t *testing.T, b []byte) { - swagger, err := openapi3.NewLoader().LoadFromData(b) - if err == nil { - err = swagger.Validate(context.Background()) - } + // Use libopenapi which supports OpenAPI 3.1 + document, err := libopenapi.NewDocument(b) if err != nil { - t.Errorf("invalid spec: %s\nspec:\n%s", err.Error(), string(b)) + t.Errorf("failed to parse OpenAPI spec: %s\nspec:\n%s", err.Error(), string(b)) + return + } + + // Check the version to determine which model to build + version := document.GetVersion() + if strings.HasPrefix(version, "3.1") || strings.HasPrefix(version, "3.0") { + // BuildV3Model handles both OpenAPI 3.0 and 3.1 + _, errs := document.BuildV3Model() + if len(errs) > 0 { + t.Errorf("invalid OpenAPI %s spec: %v\nspec:\n%s", version, errs, string(b)) + } + } else { + t.Errorf("unexpected OpenAPI version: %s", version) } } diff --git a/http/codegen/openapi/v3/openapi.go b/http/codegen/openapi/v3/openapi.go index e9aa193145..8fcc2ca30a 100644 --- a/http/codegen/openapi/v3/openapi.go +++ b/http/codegen/openapi/v3/openapi.go @@ -5,23 +5,26 @@ import "goa.design/goa/v3/http/codegen/openapi" type ( // OpenAPI is a data structure that encodes the information needed to // generate an OpenAPI specification as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md OpenAPI struct { - OpenAPI string `json:"openapi" yaml:"openapi"` // Required - Info *Info `json:"info" yaml:"info"` // Required - Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` - Paths map[string]*PathItem `json:"paths" yaml:"paths"` // Required - Components *Components `json:"components,omitempty" yaml:"components,omitempty"` - Tags []*openapi.Tag `json:"tags,omitempty" yaml:"tags,omitempty"` - Security []map[string][]string `json:"security,omitempty" yaml:"security,omitempty"` - ExternalDocs *openapi.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + OpenAPI string `json:"openapi" yaml:"openapi"` // Required + Info *Info `json:"info" yaml:"info"` // Required + JSONSchemaDialect string `json:"jsonSchemaDialect,omitempty" yaml:"jsonSchemaDialect,omitempty"` // New in 3.1.0 - JSON Schema dialect + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` // Optional in 3.1.0 (requires at least one of paths, webhooks, or components) + Webhooks map[string]*PathItem `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` // New in 3.1.0 + Components *Components `json:"components,omitempty" yaml:"components,omitempty"` + Tags []*openapi.Tag `json:"tags,omitempty" yaml:"tags,omitempty"` + Security []map[string][]string `json:"security,omitempty" yaml:"security,omitempty"` + ExternalDocs *openapi.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` } // Info represents an OpenAPI Info object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#infoObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#infoObject Info struct { Title string `json:"title" yaml:"title"` // Required + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` // New in 3.1.0 Description string `json:"description,omitempty" yaml:"description,omitempty"` TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` @@ -31,7 +34,7 @@ type ( } // Server represents an OpenAPI Server object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#serverObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#serverObject Server struct { URL string `json:"url" yaml:"url"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -39,7 +42,7 @@ type ( } // PathItem represents an OpenAPI Path Item object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathItemObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#pathItemObject PathItem struct { Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -59,7 +62,7 @@ type ( } // Components represents an OpenAPI Components object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#componentsObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#componentsObject Components struct { Schemas map[string]*openapi.Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters map[string]*ParameterRef `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -70,11 +73,12 @@ type ( Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"` Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + PathItems map[string]*PathItem `json:"pathItems,omitempty" yaml:"pathItems,omitempty"` // New in 3.1.0 Extensions map[string]any `json:"-" yaml:"-"` } // Contact represents an OpenAPI Contact object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#contactObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#contactObject Contact struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -83,15 +87,16 @@ type ( } // License represents an OpenAPI License object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#licenseObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#licenseObject License struct { Name string `json:"name" yaml:"name"` // Required + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // New in 3.1.0 - SPDX license identifier URL string `json:"url,omitempty" yaml:"url,omitempty"` Extensions map[string]any `json:"-" yaml:"-"` } // ServerVariable represents an OpenAPI Server Variable object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#serverVariableObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#serverVariableObject ServerVariable struct { Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` Default any `json:"default,omitempty" yaml:"default,omitempty"` @@ -99,7 +104,7 @@ type ( } // Operation represents an OpenAPI Operation object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#operationObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#operationObject Operation struct { Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -117,7 +122,7 @@ type ( } // Parameter represents an OpenAPI Parameter object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#parameterObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#parameterObject Parameter struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -136,7 +141,7 @@ type ( } // Response represents an OpenAPI Response object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#responseObject Response struct { Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -146,7 +151,7 @@ type ( } // MediaType represents an OpenAPI Media Type object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#mediaTypeObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#mediaTypeObject MediaType struct { Schema *openapi.Schema `json:"schema,omitempty" yaml:"schema,omitempty"` Example any `json:"example,omitempty" yaml:"example,omitempty"` @@ -156,7 +161,7 @@ type ( } // Encoding represents an OpenAPI Encoding object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#encodingObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#encodingObject Encoding struct { ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -167,7 +172,7 @@ type ( } // Header represents an OpenAPI Header object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#headerObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#headerObject Header struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` Style string `json:"style,omitempty" yaml:"style,omitempty"` @@ -184,7 +189,7 @@ type ( } // Link represents an OpenAPI Link object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#linkObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#linkObject Link struct { OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` @@ -196,7 +201,7 @@ type ( } // Example represents an OpenAPI Example object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#exampleObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#exampleObject Example struct { Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -206,7 +211,7 @@ type ( } // RequestBody represents an OpenAPI RequestBody object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#requestBodyObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#requestBodyObject RequestBody struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -215,7 +220,7 @@ type ( } // SecurityScheme represents an OpenAPI SecurityScheme object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#securitySchemeObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#securitySchemeObject SecurityScheme struct { Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -228,7 +233,7 @@ type ( } // OAuthFlows represents an OpenAPI OAuthFlows object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauthFlowsObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#oauthFlowsObject OAuthFlows struct { Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -238,7 +243,7 @@ type ( } // OAuthFlow represents an OpenAPI OAuthFlow object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauthFlowObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#oauthFlowObject OAuthFlow struct { AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` diff --git a/http/codegen/openapi/v3/ref.go b/http/codegen/openapi/v3/ref.go index 144872de17..dd40d8b38f 100644 --- a/http/codegen/openapi/v3/ref.go +++ b/http/codegen/openapi/v3/ref.go @@ -6,59 +6,59 @@ import ( type ( // ParameterRef represents an OpenAPI reference to a Parameter object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject ParameterRef struct { Ref string Value *Parameter } // ResponseRef represents an OpenAPI reference to a Response object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject ResponseRef struct { Ref string Value *Response } // HeaderRef represents an OpenAPI reference to a Header object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject HeaderRef struct { Ref string Value *Header } // CallbackRef represents an OpenAPI reference to a Callback object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject CallbackRef struct { Ref string Value map[string]*PathItem } // ExampleRef represents an OpenAPI reference to a Example object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject ExampleRef struct { Ref string Value *Example // LinkRef represents an OpenAPI reference to a Link object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject } // LinkRef represents an OpenAPI reference to a Link object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject LinkRef struct { Ref string Value *Link } // RequestBodyRef represents an OpenAPI reference to a RequestBody object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject RequestBodyRef struct { Ref string Value *RequestBody } // SecuritySchemeRef represents an OpenAPI reference to a SecurityScheme object as defined in - // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#referenceObject + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#referenceObject SecuritySchemeRef struct { Ref string Value *SecurityScheme diff --git a/http/codegen/openapi/v3/testdata/golden/array_file0.golden b/http/codegen/openapi/v3/testdata/golden/array_file0.golden index b9f28ccae9..e3331cac0e 100644 --- a/http/codegen/openapi/v3/testdata/golden/array_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/array_file0.golden @@ -2,12 +2,16 @@ "components": { "schemas": { "Bar": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "maxLength": 42, "minLength": 0, "type": "string" @@ -16,23 +20,9 @@ "type": "object" }, "Foobar": { - "example": { - "bar": [ - { - "string": "" - }, - { - "string": "" - } - ], - "foo": [ - "Repudiandae sit.", - "Asperiores fuga qui rem qui earum eos." - ] - }, - "properties": { - "bar": { - "example": [ + "examples": [ + { + "bar": [ { "string": "" }, @@ -40,6 +30,24 @@ "string": "" } ], + "foo": [ + "Repudiandae sit.", + "Asperiores fuga qui rem qui earum eos." + ] + } + ], + "properties": { + "bar": { + "examples": [ + [ + { + "string": "" + }, + { + "string": "" + } + ] + ], "items": { "$ref": "#/components/schemas/Bar" }, @@ -48,9 +56,13 @@ "type": "array" }, "foo": { - "example": [], + "examples": [ + [] + ], "items": { - "example": "Beatae non id consequatur.", + "examples": [ + "Beatae non id consequatur." + ], "type": "string" }, "maxItems": 42, @@ -66,7 +78,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { @@ -109,31 +122,33 @@ } ], "schema": { - "example": [ - { - "bar": [ - { - "string": "" - } - ], - "foo": [] - }, - { - "bar": [ - { - "string": "" - } - ], - "foo": [] - }, - { - "bar": [ - { - "string": "" - } - ], - "foo": [] - } + "examples": [ + [ + { + "bar": [ + { + "string": "" + } + ], + "foo": [] + }, + { + "bar": [ + { + "string": "" + } + ], + "foo": [] + }, + { + "bar": [ + { + "string": "" + } + ], + "foo": [] + } + ] ], "items": { "$ref": "#/components/schemas/Foobar" @@ -150,7 +165,9 @@ "application/json": { "example": "", "schema": { - "example": "", + "examples": [ + "" + ], "maxLength": 42, "minLength": 0, "type": "string" diff --git a/http/codegen/openapi/v3/testdata/golden/array_file1.golden b/http/codegen/openapi/v3/testdata/golden/array_file1.golden index 67d7e32f9f..fbed3f82b2 100644 --- a/http/codegen/openapi/v3/testdata/golden/array_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/array_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -19,16 +20,16 @@ paths: type: array items: $ref: '#/components/schemas/Foobar' - example: - - bar: - - string: "" - foo: [] - - bar: - - string: "" - foo: [] - - bar: - - string: "" - foo: [] + examples: + - - bar: + - string: "" + foo: [] + - bar: + - string: "" + foo: [] + - bar: + - string: "" + foo: [] example: - bar: - string: "" @@ -49,7 +50,8 @@ paths: application/json: schema: type: string - example: "" + examples: + - "" minLength: 0 maxLength: 42 example: "" @@ -60,11 +62,12 @@ components: properties: string: type: string - example: "" + examples: + - "" minLength: 0 maxLength: 42 - example: - string: "" + examples: + - string: "" Foobar: type: object properties: @@ -72,24 +75,26 @@ components: type: array items: $ref: '#/components/schemas/Bar' - example: - - string: "" - - string: "" + examples: + - - string: "" + - string: "" minItems: 0 maxItems: 42 foo: type: array items: type: string - example: Beatae non id consequatur. - example: [] + examples: + - Beatae non id consequatur. + examples: + - [] minItems: 0 maxItems: 42 - example: - bar: + examples: + - bar: - string: "" - string: "" - foo: + foo: - Repudiandae sit. - Asperiores fuga qui rem qui earum eos. tags: diff --git a/http/codegen/openapi/v3/testdata/golden/endpoint_file0.golden b/http/codegen/openapi/v3/testdata/golden/endpoint_file0.golden index 1f6b774b04..0540c951e8 100644 --- a/http/codegen/openapi/v3/testdata/golden/endpoint_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/endpoint_file0.golden @@ -2,12 +2,16 @@ "components": { "schemas": { "Payload": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string", "x-test-schema": "Payload" } @@ -15,12 +19,16 @@ "type": "object" }, "Result": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string", "x-test-schema": "Result" } @@ -34,7 +42,8 @@ "version": "0.0.1", "x-test-api": "API" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/endpoint_file1.golden b/http/codegen/openapi/v3/testdata/golden/endpoint_file1.golden index 0931f612ef..1f1479543f 100644 --- a/http/codegen/openapi/v3/testdata/golden/endpoint_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/endpoint_file1.golden @@ -1,8 +1,9 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 x-test-api: API +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -37,20 +38,22 @@ components: type: object properties: string: - example: "" + examples: + - "" type: string x-test-schema: Payload - example: - string: "" + examples: + - string: "" Result: type: object properties: string: - example: "" + examples: + - "" type: string x-test-schema: Result - example: - string: "" + examples: + - string: "" tags: - description: Description of Backend externalDocs: diff --git a/http/codegen/openapi/v3/testdata/golden/error-examples_file0.golden b/http/codegen/openapi/v3/testdata/golden/error-examples_file0.golden index a5cff6115e..ff898c5cc6 100644 --- a/http/codegen/openapi/v3/testdata/golden/error-examples_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/error-examples_file0.golden @@ -2,43 +2,57 @@ "components": { "schemas": { "Error": { - "example": { - "fault": true, - "id": "123abc", - "message": "parameter 'p' must be an integer", - "name": "bad_request", - "temporary": true, - "timeout": true - }, + "examples": [ + { + "fault": true, + "id": "123abc", + "message": "parameter 'p' must be an integer", + "name": "bad_request", + "temporary": true, + "timeout": true + } + ], "properties": { "fault": { "description": "Is the error a server-side fault?", - "example": true, + "examples": [ + true + ], "type": "boolean" }, "id": { "description": "ID is a unique identifier for this particular occurrence of the problem.", - "example": "123abc", + "examples": [ + "123abc" + ], "type": "string" }, "message": { "description": "Message is a human-readable explanation specific to this occurrence of the problem.", - "example": "parameter 'p' must be an integer", + "examples": [ + "parameter 'p' must be an integer" + ], "type": "string" }, "name": { "description": "Name is the name of this class of errors.", - "example": "bad_request", + "examples": [ + "bad_request" + ], "type": "string" }, "temporary": { "description": "Is the error temporary?", - "example": true, + "examples": [ + true + ], "type": "boolean" }, "timeout": { "description": "Is the error a timeout?", - "example": false, + "examples": [ + false + ], "type": "boolean" } }, @@ -53,17 +67,23 @@ "type": "object" }, "GoaCustomError": { - "example": { - "message": "error message", - "name": "custom" - }, + "examples": [ + { + "message": "error message", + "name": "custom" + } + ], "properties": { "message": { - "example": "error message", + "examples": [ + "error message" + ], "type": "string" }, "name": { - "example": "custom", + "examples": [ + "custom" + ], "type": "string" } }, @@ -79,7 +99,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/error-examples_file1.golden b/http/codegen/openapi/v3/testdata/golden/error-examples_file1.golden index b765774dc4..261847afd5 100644 --- a/http/codegen/openapi/v3/testdata/golden/error-examples_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/error-examples_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -51,34 +52,40 @@ components: fault: type: boolean description: Is the error a server-side fault? - example: true + examples: + - true id: type: string description: ID is a unique identifier for this particular occurrence of the problem. - example: 123abc + examples: + - 123abc message: type: string description: Message is a human-readable explanation specific to this occurrence of the problem. - example: parameter 'p' must be an integer + examples: + - parameter 'p' must be an integer name: type: string description: Name is the name of this class of errors. - example: bad_request + examples: + - bad_request temporary: type: boolean description: Is the error temporary? - example: true + examples: + - true timeout: type: boolean description: Is the error a timeout? - example: false - example: - fault: true - id: 123abc - message: parameter 'p' must be an integer - name: bad_request - temporary: true - timeout: true + examples: + - false + examples: + - fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true required: - name - id @@ -91,13 +98,15 @@ components: properties: message: type: string - example: error message + examples: + - error message name: type: string - example: custom - example: - message: error message - name: custom + examples: + - custom + examples: + - message: error message + name: custom required: - name - message diff --git a/http/codegen/openapi/v3/testdata/golden/explicit-view_file0.golden b/http/codegen/openapi/v3/testdata/golden/explicit-view_file0.golden index 821fc859b1..4825a20fba 100644 --- a/http/codegen/openapi/v3/testdata/golden/explicit-view_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/explicit-view_file0.golden @@ -2,18 +2,24 @@ "components": { "schemas": { "JSON": { - "example": { - "int": 1, - "string": "" - }, + "examples": [ + { + "int": 1, + "string": "" + } + ], "properties": { "int": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -21,18 +27,24 @@ }, "TestEndpointDefaultResponseBody": { "description": "TestEndpointDefaultResponseBody result type (default view)", - "example": { - "int": 1, - "string": "" - }, + "examples": [ + { + "int": 1, + "string": "" + } + ], "properties": { "int": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -40,12 +52,16 @@ }, "TestEndpointTinyResponseBodyTiny": { "description": "TestEndpointTinyResponseBody result type (tiny view)", - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -57,7 +73,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/explicit-view_file1.golden b/http/codegen/openapi/v3/testdata/golden/explicit-view_file1.golden index 3ab4d313a9..339aeb9b28 100644 --- a/http/codegen/openapi/v3/testdata/golden/explicit-view_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/explicit-view_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -44,36 +45,41 @@ components: properties: int: type: integer - example: 1 + examples: + - 1 format: int64 string: type: string - example: "" - example: - int: 1 - string: "" + examples: + - "" + examples: + - int: 1 + string: "" TestEndpointDefaultResponseBody: type: object properties: int: type: integer - example: 1 + examples: + - 1 format: int64 string: type: string - example: "" + examples: + - "" description: TestEndpointDefaultResponseBody result type (default view) - example: - int: 1 - string: "" + examples: + - int: 1 + string: "" TestEndpointTinyResponseBodyTiny: type: object properties: string: type: string - example: "" + examples: + - "" description: TestEndpointTinyResponseBody result type (tiny view) - example: - string: "" + examples: + - string: "" tags: - name: testService diff --git a/http/codegen/openapi/v3/testdata/golden/file-service_file0.golden b/http/codegen/openapi/v3/testdata/golden/file-service_file0.golden index f6e5559702..902b50b970 100644 --- a/http/codegen/openapi/v3/testdata/golden/file-service_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/file-service_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/path1": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/file-service_file1.golden b/http/codegen/openapi/v3/testdata/golden/file-service_file1.golden index 65e3df5c49..caee1e86e5 100644 --- a/http/codegen/openapi/v3/testdata/golden/file-service_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/file-service_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api diff --git a/http/codegen/openapi/v3/testdata/golden/headers_file0.golden b/http/codegen/openapi/v3/testdata/golden/headers_file0.golden index 12db30dde1..dd100b94a1 100644 --- a/http/codegen/openapi/v3/testdata/golden/headers_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/headers_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { @@ -16,7 +17,9 @@ "in": "header", "name": "foo", "schema": { - "example": 9176544974339886000, + "examples": [ + 9176544974339886000 + ], "format": "int64", "type": "integer" } @@ -27,7 +30,9 @@ "in": "header", "name": "bar", "schema": { - "example": 2166276375441812200, + "examples": [ + 2166276375441812200 + ], "format": "int64", "type": "integer" } diff --git a/http/codegen/openapi/v3/testdata/golden/headers_file1.golden b/http/codegen/openapi/v3/testdata/golden/headers_file1.golden index 14eeba00bb..a7e8c7f4c7 100644 --- a/http/codegen/openapi/v3/testdata/golden/headers_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/headers_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -18,7 +19,8 @@ paths: allowEmptyValue: true schema: type: integer - example: 9176544974339886224 + examples: + - 9176544974339886224 format: int64 example: 1933576090881074823 - name: bar @@ -26,7 +28,8 @@ paths: allowEmptyValue: true schema: type: integer - example: 2166276375441812184 + examples: + - 2166276375441812184 format: int64 example: 7595816812588075382 responses: diff --git a/http/codegen/openapi/v3/testdata/golden/integer_file0.golden b/http/codegen/openapi/v3/testdata/golden/integer_file0.golden index 1819f3d644..651ea57757 100644 --- a/http/codegen/openapi/v3/testdata/golden/integer_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/integer_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { @@ -14,7 +15,9 @@ "application/json": { "example": 1, "schema": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "maximum": 42, "minimum": 0, @@ -30,7 +33,9 @@ "application/json": { "example": 1, "schema": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "maximum": 42, "minimum": 0, diff --git a/http/codegen/openapi/v3/testdata/golden/integer_file1.golden b/http/codegen/openapi/v3/testdata/golden/integer_file1.golden index ad54b5291d..971d3fadf4 100644 --- a/http/codegen/openapi/v3/testdata/golden/integer_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/integer_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -17,7 +18,8 @@ paths: application/json: schema: type: integer - example: 1 + examples: + - 1 format: int64 minimum: 0 maximum: 42 @@ -29,7 +31,8 @@ paths: application/json: schema: type: integer - example: 1 + examples: + - 1 format: int64 minimum: 0 maximum: 42 diff --git a/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden index 068ee5c57c..f61f7d5720 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/path1": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden index 3d0cf89b44..593099a8f4 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden index 068ee5c57c..f61f7d5720 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/path1": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden index 3d0cf89b44..593099a8f4 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden index 068ee5c57c..f61f7d5720 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/path1": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden index 3d0cf89b44..593099a8f4 100644 --- a/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: diff --git a/http/codegen/openapi/v3/testdata/golden/multiple-services_file0.golden b/http/codegen/openapi/v3/testdata/golden/multiple-services_file0.golden index c9f4ae1ed8..28a51b41e3 100644 --- a/http/codegen/openapi/v3/testdata/golden/multiple-services_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/multiple-services_file0.golden @@ -2,24 +2,32 @@ "components": { "schemas": { "Payload": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "Result": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -31,7 +39,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/multiple-services_file1.golden b/http/codegen/openapi/v3/testdata/golden/multiple-services_file1.golden index 6957aea496..3a26dafc91 100644 --- a/http/codegen/openapi/v3/testdata/golden/multiple-services_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/multiple-services_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -57,17 +58,19 @@ components: properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" Result: type: object properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" tags: - name: testService - name: anotherTestService diff --git a/http/codegen/openapi/v3/testdata/golden/multiple-views_file0.golden b/http/codegen/openapi/v3/testdata/golden/multiple-views_file0.golden index ca9d495925..4ee544cc8d 100644 --- a/http/codegen/openapi/v3/testdata/golden/multiple-views_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/multiple-views_file0.golden @@ -2,18 +2,24 @@ "components": { "schemas": { "JSON": { - "example": { - "int": 1, - "string": "" - }, + "examples": [ + { + "int": 1, + "string": "" + } + ], "properties": { "int": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -21,12 +27,16 @@ }, "TestEndpointTinyResponseBodyTiny": { "description": "TestEndpointTinyResponseBody result type (tiny view)", - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -38,7 +48,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/multiple-views_file1.golden b/http/codegen/openapi/v3/testdata/golden/multiple-views_file1.golden index ea6360e74f..06b2181193 100644 --- a/http/codegen/openapi/v3/testdata/golden/multiple-views_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/multiple-views_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -44,22 +45,25 @@ components: properties: int: type: integer - example: 1 + examples: + - 1 format: int64 string: type: string - example: "" - example: - int: 1 - string: "" + examples: + - "" + examples: + - int: 1 + string: "" TestEndpointTinyResponseBodyTiny: type: object properties: string: type: string - example: "" + examples: + - "" description: TestEndpointTinyResponseBody result type (tiny view) - example: - string: "" + examples: + - string: "" tags: - name: testService diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file0.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file0.golden index 1bb53c016d..53531ff2f0 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file0.golden @@ -2,18 +2,27 @@ "components": { "schemas": { "Payload": { - "example": { - "required_string": "", - "string": "" - }, + "examples": [ + { + "required_string": "", + "string": "" + } + ], "properties": { "required_string": { - "example": "", + "examples": [ + "" + ], "type": "string" }, "string": { - "example": "", - "type": "string" + "examples": [ + "" + ], + "type": [ + "string", + "null" + ] } }, "required": [ @@ -22,18 +31,27 @@ "type": "object" }, "Result": { - "example": { - "int": 0, - "required_int": 0 - }, + "examples": [ + { + "int": 0, + "required_int": 0 + } + ], "properties": { "int": { - "example": 0, + "examples": [ + 0 + ], "format": "int64", - "type": "integer" + "type": [ + "integer", + "null" + ] }, "required_int": { - "example": 0, + "examples": [ + 0 + ], "format": "int64", "type": "integer" } @@ -49,7 +67,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file1.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file1.golden index 6dfd98316b..0f67048360 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-attribute_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -37,29 +38,37 @@ components: properties: required_string: type: string - example: "" + examples: + - "" string: - type: string - example: "" - example: - required_string: "" - string: "" + type: + - string + - "null" + examples: + - "" + examples: + - required_string: "" + string: "" required: - required_string Result: type: object properties: int: - type: integer - example: 0 + type: + - integer + - "null" + examples: + - 0 format: int64 required_int: type: integer - example: 0 + examples: + - 0 format: int64 - example: - int: 0 - required_int: 0 + examples: + - int: 0 + required_int: 0 required: - required_int tags: diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-host_file0.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-host_file0.golden index 63b6416eb4..a317f7b185 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-host_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-host_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { @@ -15,7 +16,9 @@ "application/json": { "example": "Aut sed ducimus repudiandae sit explicabo asperiores.", "schema": { - "example": "Beatae non id consequatur.", + "examples": [ + "Beatae non id consequatur." + ], "type": "string" } } diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-host_file1.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-host_file1.golden index 5f4d2bdda9..b5818eb3c4 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-host_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-host_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema paths: /: get: @@ -16,7 +17,8 @@ paths: application/json: schema: type: string - example: Beatae non id consequatur. + examples: + - Beatae non id consequatur. example: Aut sed ducimus repudiandae sit explicabo asperiores. components: {} tags: diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-server_file0.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-server_file0.golden index 63b6416eb4..a317f7b185 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-server_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-server_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { @@ -15,7 +16,9 @@ "application/json": { "example": "Aut sed ducimus repudiandae sit explicabo asperiores.", "schema": { - "example": "Beatae non id consequatur.", + "examples": [ + "Beatae non id consequatur." + ], "type": "string" } } diff --git a/http/codegen/openapi/v3/testdata/golden/not-generate-server_file1.golden b/http/codegen/openapi/v3/testdata/golden/not-generate-server_file1.golden index 5f4d2bdda9..b5818eb3c4 100644 --- a/http/codegen/openapi/v3/testdata/golden/not-generate-server_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/not-generate-server_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema paths: /: get: @@ -16,7 +17,8 @@ paths: application/json: schema: type: string - example: Beatae non id consequatur. + examples: + - Beatae non id consequatur. example: Aut sed ducimus repudiandae sit explicabo asperiores. components: {} tags: diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file0.golden b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file0.golden index a1f4052261..1799625bb4 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/{foo}/{bar}": { "post": { @@ -16,7 +17,9 @@ "name": "foo", "required": true, "schema": { - "example": 9176544974339886000, + "examples": [ + 9176544974339886000 + ], "format": "int64", "type": "integer" } @@ -27,7 +30,9 @@ "name": "bar", "required": true, "schema": { - "example": 2166276375441812200, + "examples": [ + 2166276375441812200 + ], "format": "int64", "type": "integer" } diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file1.golden b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file1.golden index c780651e9a..a78c71e480 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-explicit-wildcards_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -18,7 +19,8 @@ paths: required: true schema: type: integer - example: 9176544974339886224 + examples: + - 9176544974339886224 format: int64 example: 1933576090881074823 - name: bar @@ -26,7 +28,8 @@ paths: required: true schema: type: integer - example: 2166276375441812184 + examples: + - 2166276375441812184 format: int64 example: 7595816812588075382 responses: diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file0.golden b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file0.golden index a1f4052261..1799625bb4 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/{foo}/{bar}": { "post": { @@ -16,7 +17,9 @@ "name": "foo", "required": true, "schema": { - "example": 9176544974339886000, + "examples": [ + 9176544974339886000 + ], "format": "int64", "type": "integer" } @@ -27,7 +30,9 @@ "name": "bar", "required": true, "schema": { - "example": 2166276375441812200, + "examples": [ + 2166276375441812200 + ], "format": "int64", "type": "integer" } diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file1.golden b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file1.golden index c780651e9a..a78c71e480 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-multiple-wildcards_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -18,7 +19,8 @@ paths: required: true schema: type: integer - example: 9176544974339886224 + examples: + - 9176544974339886224 format: int64 example: 1933576090881074823 - name: bar @@ -26,7 +28,8 @@ paths: required: true schema: type: integer - example: 2166276375441812184 + examples: + - 2166276375441812184 format: int64 example: 7595816812588075382 responses: diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file0.golden b/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file0.golden index e5c408ec2a..1a5d4b1722 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/{int_map}": { "post": { @@ -16,7 +17,9 @@ "name": "int_map", "required": true, "schema": { - "example": 9176544974339886000, + "examples": [ + 9176544974339886000 + ], "format": "int64", "type": "integer" } diff --git a/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file1.golden b/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file1.golden index e0a4a6edb9..ea3401f22f 100644 --- a/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/path-with-wildcards_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -18,7 +19,8 @@ paths: required: true schema: type: integer - example: 9176544974339886224 + examples: + - 9176544974339886224 format: int64 example: 1933576090881074823 responses: diff --git a/http/codegen/openapi/v3/testdata/golden/security_file0.golden b/http/codegen/openapi/v3/testdata/golden/security_file0.golden index 363b4fd733..30f082912b 100644 --- a/http/codegen/openapi/v3/testdata/golden/security_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/security_file0.golden @@ -59,7 +59,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { @@ -72,7 +73,9 @@ "name": "k", "required": true, "schema": { - "example": "Quia molestias.", + "examples": [ + "Quia molestias." + ], "type": "string" } }, @@ -83,7 +86,9 @@ "name": "Token", "required": true, "schema": { - "example": "Et tempora et quae.", + "examples": [ + "Et tempora et quae." + ], "type": "string" } }, @@ -94,7 +99,9 @@ "name": "X-Authorization", "required": true, "schema": { - "example": "Ullam aut.", + "examples": [ + "Ullam aut." + ], "type": "string" } } @@ -131,7 +138,9 @@ "name": "auth", "required": true, "schema": { - "example": "Harum et.", + "examples": [ + "Harum et." + ], "type": "string" } } diff --git a/http/codegen/openapi/v3/testdata/golden/security_file1.golden b/http/codegen/openapi/v3/testdata/golden/security_file1.golden index 9668b7e1c9..5e11f2781f 100644 --- a/http/codegen/openapi/v3/testdata/golden/security_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/security_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -19,7 +20,8 @@ paths: required: true schema: type: string - example: Quia molestias. + examples: + - Quia molestias. example: Doloribus qui quia. - name: Token in: header @@ -27,7 +29,8 @@ paths: required: true schema: type: string - example: Et tempora et quae. + examples: + - Et tempora et quae. example: Itaque inventore optio. - name: X-Authorization in: header @@ -35,7 +38,8 @@ paths: required: true schema: type: string - example: Ullam aut. + examples: + - Ullam aut. example: Iste perspiciatis. responses: "204": @@ -59,7 +63,8 @@ paths: required: true schema: type: string - example: Harum et. + examples: + - Harum et. example: Neque nisi quibusdam nisi sint sunt. responses: "204": diff --git a/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file0.golden b/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file0.golden index 675ba8cf8b..fcb3593dd2 100644 --- a/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file1.golden b/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file1.golden index 29e3c33130..7bd452599f 100644 --- a/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/server-host-with-variables_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://{version}.goa.design variables: diff --git a/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file0.golden b/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file0.golden index 2aba1d707a..6c9bc8bd3b 100644 --- a/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/binary": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file1.golden b/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file1.golden index b5f3dfcb29..d8ae9b80e7 100644 --- a/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/skip-response-body-encode-decode_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api diff --git a/http/codegen/openapi/v3/testdata/golden/string_file0.golden b/http/codegen/openapi/v3/testdata/golden/string_file0.golden index 3e6cf0c0ac..aa77ac50f4 100644 --- a/http/codegen/openapi/v3/testdata/golden/string_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/string_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { @@ -14,7 +15,9 @@ "application/json": { "example": "", "schema": { - "example": "", + "examples": [ + "" + ], "maxLength": 42, "minLength": 0, "type": "string" @@ -29,7 +32,9 @@ "application/json": { "example": "", "schema": { - "example": "", + "examples": [ + "" + ], "maxLength": 42, "minLength": 0, "type": "string" diff --git a/http/codegen/openapi/v3/testdata/golden/string_file1.golden b/http/codegen/openapi/v3/testdata/golden/string_file1.golden index 4e6dc726af..d6a3981e99 100644 --- a/http/codegen/openapi/v3/testdata/golden/string_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/string_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -17,7 +18,8 @@ paths: application/json: schema: type: string - example: "" + examples: + - "" minLength: 0 maxLength: 42 example: "" @@ -28,7 +30,8 @@ paths: application/json: schema: type: string - example: "" + examples: + - "" minLength: 0 maxLength: 42 example: "" diff --git a/http/codegen/openapi/v3/testdata/golden/typename_file0.golden b/http/codegen/openapi/v3/testdata/golden/typename_file0.golden index d3987bb626..1596150d3c 100644 --- a/http/codegen/openapi/v3/testdata/golden/typename_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/typename_file0.golden @@ -2,72 +2,96 @@ "components": { "schemas": { "BarPayload": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "BazPayload": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "BazResult": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "Foo": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "FooResult": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "GoaExampleBar": { - "example": { - "value": "" - }, + "examples": [ + { + "value": "" + } + ], "properties": { "value": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -79,7 +103,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/bar": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/typename_file1.golden b/http/codegen/openapi/v3/testdata/golden/typename_file1.golden index 238a5121d8..6287f86d52 100644 --- a/http/codegen/openapi/v3/testdata/golden/typename_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/typename_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -81,48 +82,54 @@ components: properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" BazPayload: type: object properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" BazResult: type: object properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" Foo: type: object properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" FooResult: type: object properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" GoaExampleBar: type: object properties: value: type: string - example: "" - example: - value: "" + examples: + - "" + examples: + - value: "" tags: - name: testService diff --git a/http/codegen/openapi/v3/testdata/golden/valid_file0.golden b/http/codegen/openapi/v3/testdata/golden/valid_file0.golden index ed2b70db80..104d94ad64 100644 --- a/http/codegen/openapi/v3/testdata/golden/valid_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/valid_file0.golden @@ -2,24 +2,32 @@ "components": { "schemas": { "Payload": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "Result": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -31,7 +39,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "get": { diff --git a/http/codegen/openapi/v3/testdata/golden/valid_file1.golden b/http/codegen/openapi/v3/testdata/golden/valid_file1.golden index fcb037702d..a989554701 100644 --- a/http/codegen/openapi/v3/testdata/golden/valid_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/valid_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://goa.design paths: @@ -35,16 +36,18 @@ components: properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" Result: type: object properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" tags: - name: testService diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden index a0d2b331ff..a744df9a09 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden @@ -2,37 +2,49 @@ "components": { "schemas": { "TestEndpointRequestBody": { - "example": { - "any": "", - "any_array": [ - "", - "" - ], - "any_map": { - "": "" + "examples": [ + { + "any": "", + "any_array": [ + "", + "" + ], + "any_map": { + "": "" + } } - }, + ], "properties": { "any": { - "example": "" + "examples": [ + "" + ], + "type": "" }, "any_array": { - "example": [ - "", - "", - "", - "" + "examples": [ + [ + "", + "", + "", + "" + ] ], "items": { - "example": "" + "examples": [ + "" + ], + "type": "" }, "type": "array" }, "any_map": { "additionalProperties": true, - "example": { - "": "" - }, + "examples": [ + { + "": "" + } + ], "type": "object" } }, @@ -44,7 +56,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden index 6417a5c6a5..9e26e71aba 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -48,27 +49,31 @@ components: type: object properties: any: - example: "" + type: "" + examples: + - "" any_array: type: array items: - example: "" - example: - - "" - - "" - - "" - - "" + type: "" + examples: + - "" + examples: + - - "" + - "" + - "" + - "" any_map: type: object - example: - "": "" + examples: + - "": "" additionalProperties: true - example: - any: "" - any_array: + examples: + - any: "" + any_array: - "" - "" - any_map: + any_map: "": "" tags: - name: testService diff --git a/http/codegen/openapi/v3/testdata/golden/with-map_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-map_file0.golden index 9fb35a392f..d721f537f4 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-map_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-map_file0.golden @@ -2,38 +2,25 @@ "components": { "schemas": { "Bar": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "GoaFoobar": { - "example": { - "bar": [ - { - "string": "" - }, - { - "string": "" - } - ], - "foo": "" - }, - "properties": { - "bar": { - "example": [ - { - "string": "" - }, - { - "string": "" - }, + "examples": [ + { + "bar": [ { "string": "" }, @@ -41,103 +28,108 @@ "string": "" } ], + "foo": "" + } + ], + "properties": { + "bar": { + "examples": [ + [ + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + } + ] + ], "items": { "$ref": "#/components/schemas/Bar" }, "type": "array" }, "foo": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "TestEndpointRequestBody": { - "example": { - "int_map": { - "": 1 - }, - "type_map": { - "": { - "string": "" + "examples": [ + { + "int_map": { + "": 1 + }, + "type_map": { + "": { + "string": "" + } + }, + "uint_map": { + "": 1 } - }, - "uint_map": { - "": 1 } - }, + ], "properties": { "int_map": { "additionalProperties": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, - "example": { - "": 1 - }, + "examples": [ + { + "": 1 + } + ], "type": "object" }, "type_map": { "additionalProperties": { "$ref": "#/components/schemas/Bar" }, - "example": { - "": { - "string": "" + "examples": [ + { + "": { + "string": "" + } } - }, + ], "type": "object" }, "uint_map": { "additionalProperties": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, - "example": { - "": 1 - }, + "examples": [ + { + "": 1 + } + ], "type": "object" } }, "type": "object" }, "TestEndpointResponseBody": { - "example": { - "resulttype_map": { - "": { - "bar": [ - { - "string": "" - }, - { - "string": "" - }, - { - "string": "" - }, - { - "string": "" - } - ], - "foo": "" - } - }, - "uint32_map": { - "": 1 - }, - "uint64_map": { - "": 1 - } - }, - "properties": { - "resulttype_map": { - "additionalProperties": { - "$ref": "#/components/schemas/GoaFoobar" - }, - "example": { + "examples": [ + { + "resulttype_map": { "": { "bar": [ { @@ -156,28 +148,70 @@ "foo": "" } }, + "uint32_map": { + "": 1 + }, + "uint64_map": { + "": 1 + } + } + ], + "properties": { + "resulttype_map": { + "additionalProperties": { + "$ref": "#/components/schemas/GoaFoobar" + }, + "examples": [ + { + "": { + "bar": [ + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + } + ], + "foo": "" + } + } + ], "type": "object" }, "uint32_map": { "additionalProperties": { - "example": 1, + "examples": [ + 1 + ], "format": "int32", "type": "integer" }, - "example": { - "": 1 - }, + "examples": [ + { + "": 1 + } + ], "type": "object" }, "uint64_map": { "additionalProperties": { - "example": 1, + "examples": [ + 1 + ], "format": "int64", "type": "integer" }, - "example": { - "": 1 - }, + "examples": [ + { + "": 1 + } + ], "type": "object" } }, @@ -189,7 +223,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/with-map_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-map_file1.golden index 52493c9d25..c76a707f4e 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-map_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-map_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -53,9 +54,10 @@ components: properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" GoaFoobar: type: object properties: @@ -63,60 +65,63 @@ components: type: array items: $ref: '#/components/schemas/Bar' - example: - - string: "" - - string: "" - - string: "" - - string: "" + examples: + - - string: "" + - string: "" + - string: "" + - string: "" foo: type: string - example: "" - example: - bar: + examples: + - "" + examples: + - bar: - string: "" - string: "" - foo: "" + foo: "" TestEndpointRequestBody: type: object properties: int_map: type: object - example: - "": 1 + examples: + - "": 1 additionalProperties: type: integer - example: 1 + examples: + - 1 format: int64 type_map: type: object - example: - "": + examples: + - "": string: "" additionalProperties: $ref: '#/components/schemas/Bar' uint_map: type: object - example: - "": 1 + examples: + - "": 1 additionalProperties: type: integer - example: 1 + examples: + - 1 format: int64 - example: - int_map: + examples: + - int_map: "": 1 - type_map: + type_map: "": string: "" - uint_map: + uint_map: "": 1 TestEndpointResponseBody: type: object properties: resulttype_map: type: object - example: - "": + examples: + - "": bar: - string: "" - string: "" @@ -127,22 +132,24 @@ components: $ref: '#/components/schemas/GoaFoobar' uint32_map: type: object - example: - "": 1 + examples: + - "": 1 additionalProperties: type: integer - example: 1 + examples: + - 1 format: int32 uint64_map: type: object - example: - "": 1 + examples: + - "": 1 additionalProperties: type: integer - example: 1 + examples: + - 1 format: int64 - example: - resulttype_map: + examples: + - resulttype_map: "": bar: - string: "" @@ -150,9 +157,9 @@ components: - string: "" - string: "" foo: "" - uint32_map: + uint32_map: "": 1 - uint64_map: + uint64_map: "": 1 tags: - name: test service diff --git a/http/codegen/openapi/v3/testdata/golden/with-spaces_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-spaces_file0.golden index c8a59350d0..c89e53e36d 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-spaces_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-spaces_file0.golden @@ -2,38 +2,25 @@ "components": { "schemas": { "Bar": { - "example": { - "string": "" - }, + "examples": [ + { + "string": "" + } + ], "properties": { "string": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, "type": "object" }, "GoaFoobar": { - "example": { - "bar": [ - { - "string": "" - }, - { - "string": "" - } - ], - "foo": "" - }, - "properties": { - "bar": { - "example": [ - { - "string": "" - }, - { - "string": "" - }, + "examples": [ + { + "bar": [ { "string": "" }, @@ -41,13 +28,36 @@ "string": "" } ], + "foo": "" + } + ], + "properties": { + "bar": { + "examples": [ + [ + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + }, + { + "string": "" + } + ] + ], "items": { "$ref": "#/components/schemas/Bar" }, "type": "array" }, "foo": { - "example": "", + "examples": [ + "" + ], "type": "string" } }, @@ -59,7 +69,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/": { "post": { diff --git a/http/codegen/openapi/v3/testdata/golden/with-spaces_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-spaces_file1.golden index 8feadf09e4..6acf7b0711 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-spaces_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-spaces_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -52,9 +53,10 @@ components: properties: string: type: string - example: "" - example: - string: "" + examples: + - "" + examples: + - string: "" GoaFoobar: type: object properties: @@ -62,18 +64,19 @@ components: type: array items: $ref: '#/components/schemas/Bar' - example: - - string: "" - - string: "" - - string: "" - - string: "" + examples: + - - string: "" + - string: "" + - string: "" + - string: "" foo: type: string - example: "" - example: - bar: + examples: + - "" + examples: + - bar: - string: "" - string: "" - foo: "" + foo: "" tags: - name: test service diff --git a/http/codegen/openapi/v3/testdata/golden/with-tags_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-tags_file0.golden index 7362bb01c8..f6aebe95f3 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-tags_file0.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-tags_file0.golden @@ -4,7 +4,8 @@ "title": "Goa API", "version": "0.0.1" }, - "openapi": "3.0.3", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "openapi": "3.1.1", "paths": { "/{int_map}": { "post": { @@ -16,7 +17,9 @@ "name": "int_map", "required": true, "schema": { - "example": 9176544974339886000, + "examples": [ + 9176544974339886000 + ], "format": "int64", "type": "integer" } diff --git a/http/codegen/openapi/v3/testdata/golden/with-tags_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-tags_file1.golden index 942d32394b..24765d9754 100644 --- a/http/codegen/openapi/v3/testdata/golden/with-tags_file1.golden +++ b/http/codegen/openapi/v3/testdata/golden/with-tags_file1.golden @@ -1,7 +1,8 @@ -openapi: 3.0.3 +openapi: 3.1.1 info: title: Goa API version: 0.0.1 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:80 description: Default server for test api @@ -18,7 +19,8 @@ paths: required: true schema: type: integer - example: 9176544974339886224 + examples: + - 9176544974339886224 format: int64 example: 1933576090881074823 responses: diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go index 9bd8a6df47..aad8abb236 100644 --- a/http/codegen/openapi/v3/types.go +++ b/http/codegen/openapi/v3/types.go @@ -97,7 +97,15 @@ func buildBodyTypes(api *expr.APIExpr, types []expr.UserType, resultTypes []*exp if sreq.Ref != "" { note = sreq.Ref } else { - note = string(sreq.Type) + // Handle Type which can be string or array + switch t := sreq.Type.(type) { + case string: + note = t + case openapi.Type: + note = string(t) + default: + note = fmt.Sprintf("%v", t) + } } if req == nil { req = sreq @@ -143,6 +151,11 @@ func buildBodyTypes(api *expr.APIExpr, types []expr.UserType, resultTypes []*exp } func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi.Schema { + return sf.schemafyWithNullable(attr, false, noref...) +} + +// schemafyWithNullable creates a schema with nullable support +func (sf *schemafier) schemafyWithNullable(attr *expr.AttributeExpr, nullable bool, noref ...bool) *openapi.Schema { if attr.Type == expr.Empty { return nil } @@ -157,16 +170,32 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi case expr.IntKind, expr.UIntKind, expr.Int64Kind, expr.UInt64Kind: // Use int64 format for IntKind and UIntKind because the OpenAPI // generator produced int32 by default. - s.Type = openapi.Type("integer") + if nullable { + s.SetNullableType(openapi.Type("integer")) + } else { + s.SetType(openapi.Type("integer")) + } s.Format = "int64" case expr.Int32Kind, expr.UInt32Kind: - s.Type = openapi.Type("integer") + if nullable { + s.SetNullableType(openapi.Type("integer")) + } else { + s.SetType(openapi.Type("integer")) + } s.Format = "int32" case expr.Float32Kind: - s.Type = openapi.Type("number") + if nullable { + s.SetNullableType(openapi.Type("number")) + } else { + s.SetType(openapi.Type("number")) + } s.Format = "float" case expr.Float64Kind: - s.Type = openapi.Type("number") + if nullable { + s.SetNullableType(openapi.Type("number")) + } else { + s.SetType(openapi.Type("number")) + } s.Format = "double" case expr.BytesKind: if bases := attr.Bases; len(bases) > 0 { @@ -176,33 +205,47 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi s.AnyOf = append(s.AnyOf, val) } } else { - s.Type = openapi.Type("string") + if nullable { + s.SetNullableType(openapi.Type("string")) + } else { + s.SetType(openapi.Type("string")) + } s.Format = "binary" } case expr.AnyKind: // A schema without a type matches any data type. // See https://swagger.io/docs/specification/data-models/data-types/#any. - s.Type = openapi.Type("") + s.SetType(openapi.Type("")) default: - s.Type = openapi.Type(t.Name()) + if nullable { + s.SetNullableType(openapi.Type(t.Name())) + } else { + s.SetType(openapi.Type(t.Name())) + } } case *expr.Array: - s.Type = openapi.Array + s.SetType(openapi.Array) s.Items = sf.schemafy(t.ElemType) case *expr.Object: - s.Type = openapi.Object + s.SetType(openapi.Object) var itemNotes []string for _, nat := range *t { if !openapi.MustGenerate(nat.Attribute.Meta) { continue } - s.Properties[nat.Name] = sf.schemafy(nat.Attribute) + // Check if this field should be nullable (not required and is a pointer type) + isNullable := false + if attr.Validation != nil && !attr.IsRequired(nat.Name) { + // Field is not required, so it can be null in OpenAPI 3.1 + isNullable = true + } + s.Properties[nat.Name] = sf.schemafyWithNullable(nat.Attribute, isNullable) } if len(itemNotes) > 0 { note = strings.Join(itemNotes, "\n") } case *expr.Map: - s.Type = openapi.Object + s.SetType(openapi.Object) if t.ElemType.Type == expr.Any { // Use free-form objects when elements are of type "Any", otherwise, use full schema // See https://swagger.io/docs/specification/data-models/dictionaries/. @@ -261,7 +304,11 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi // Default value, example, extensions s.DefaultValue = toStringMap(attr.DefaultValue) - s.Example = attr.Example(sf.rand) + // For OpenAPI 3.1, use Examples array only (not both example and examples) + if example := attr.Example(sf.rand); example != nil { + s.Examples = []any{example} + // Don't set Example field in OpenAPI 3.1 to avoid conflicts + } s.Extensions = openapi.ExtensionsFromExpr(attr.Meta) // Validations diff --git a/http/codegen/openapi/v3/types_test.go b/http/codegen/openapi/v3/types_test.go index 469f63868b..746e5bf131 100644 --- a/http/codegen/openapi/v3/types_test.go +++ b/http/codegen/openapi/v3/types_test.go @@ -2,6 +2,7 @@ package openapiv3 import ( "encoding/json" + "fmt" "hash/fnv" "strings" "testing" @@ -241,8 +242,18 @@ func matchesSchemaWithPrefix(t *testing.T, ctx string, s *openapi.Schema, types return } } - if tt.Type != string(s.Type) { - t.Errorf("%s: %sgot type %q, expected %q", ctx, prefix, s.Type, tt.Type) + // Handle Type which can be string or array + var typeStr string + switch ty := s.Type.(type) { + case string: + typeStr = ty + case openapi.Type: + typeStr = string(ty) + default: + typeStr = fmt.Sprintf("%v", ty) + } + if tt.Type != typeStr { + t.Errorf("%s: %sgot type %q, expected %q", ctx, prefix, typeStr, tt.Type) } if tt.Format != "" { if s.Format != tt.Format { @@ -440,9 +451,18 @@ func validateAdditionalPropsSchema(t *testing.T, ctx string, schema *openapi.Sch schema = resolvedSchema } - // Check type - if string(schema.Type) != expected.Type { - t.Errorf("%s: %sexpected type %s, got %s", ctx, prefix, expected.Type, schema.Type) + // Check type - handle Type which can be string or array + var typeStr string + switch ty := schema.Type.(type) { + case string: + typeStr = ty + case openapi.Type: + typeStr = string(ty) + default: + typeStr = fmt.Sprintf("%v", ty) + } + if typeStr != expected.Type { + t.Errorf("%s: %sexpected type %s, got %s", ctx, prefix, expected.Type, typeStr) } // Check array items if expected diff --git a/http/codegen/openapi/v3/webhook_test.go b/http/codegen/openapi/v3/webhook_test.go new file mode 100644 index 0000000000..b035621560 --- /dev/null +++ b/http/codegen/openapi/v3/webhook_test.go @@ -0,0 +1,140 @@ +package openapiv3 + +import ( + "testing" + + "goa.design/goa/v3/dsl" + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +func TestWebhookGeneration(t *testing.T) { + // Define a simple service with a webhook + root := runWebhookDSL(t, func() { + dsl.API("test", func() { + dsl.Title("Test API") + dsl.Version("1.0") + }) + + dsl.Service("TestService", func() { + dsl.Webhook("test.event", func() { + dsl.WebhookDescription("Test webhook") + dsl.WebhookPayload(func() { + dsl.Attribute("id", expr.String) + dsl.Attribute("name", expr.String) + dsl.Required("id") + }) + dsl.WebhookHTTP(func() { + dsl.WebhookPOST("/webhook/test") + dsl.WebhookHeaders(func() { + dsl.Attribute("X-Signature", expr.String, "Signature") + dsl.Required("X-Signature") + }) + dsl.WebhookResponse(200, func() { + dsl.Description("Success") + }) + }) + }) + + // Add a simple method to make the service valid + dsl.Method("test", func() { + dsl.HTTP(func() { + dsl.GET("/test") + }) + }) + }) + }) + + // Generate OpenAPI specification + spec := New(root) + if spec == nil { + t.Fatal("failed to generate OpenAPI spec: got nil") + } + + // Check that webhooks were generated + if spec.Webhooks == nil { + t.Fatal("expected webhooks in OpenAPI spec, got nil") + } + + if len(spec.Webhooks) != 1 { + t.Fatalf("expected 1 webhook, got %d", len(spec.Webhooks)) + } + + // Check webhook exists with correct name + webhook, ok := spec.Webhooks["test.event"] + if !ok { + t.Fatal("expected webhook 'test.event' not found") + } + + // Check webhook has POST operation + if webhook.Post == nil { + t.Fatal("expected POST operation on webhook") + } + + // Verify operation details + if webhook.Post.Summary != "test.event" { + t.Errorf("expected summary 'test.event', got %q", webhook.Post.Summary) + } + + if webhook.Post.Description != "Test webhook" { + t.Errorf("expected description 'Test webhook', got %q", webhook.Post.Description) + } + + // Check request body exists + if webhook.Post.RequestBody == nil { + t.Fatal("expected request body on webhook POST operation") + } + + // Check parameters (headers) + if len(webhook.Post.Parameters) != 1 { + t.Fatalf("expected 1 parameter (header), got %d", len(webhook.Post.Parameters)) + } + + param := webhook.Post.Parameters[0].Value + if param.Name != "X-Signature" { + t.Errorf("expected header parameter 'X-Signature', got %q", param.Name) + } + + if param.In != "header" { + t.Errorf("expected parameter in 'header', got %q", param.In) + } + + // Check responses + if len(webhook.Post.Responses) != 1 { + t.Fatalf("expected 1 response, got %d", len(webhook.Post.Responses)) + } + + _, ok = webhook.Post.Responses["200"] + if !ok { + t.Error("expected response with status code 200") + } +} + +// runWebhookDSL helper for testing +func runWebhookDSL(t *testing.T, dslFunc func()) *expr.RootExpr { + t.Helper() + eval.Reset() + expr.Root = new(expr.RootExpr) + expr.GeneratedResultTypes = new(expr.ResultTypesRoot) + + if err := eval.Register(expr.Root); err != nil { + t.Fatalf("failed to register root: %v", err) + } + if err := eval.Register(expr.GeneratedResultTypes); err != nil { + t.Fatalf("failed to register result types: %v", err) + } + + expr.Root.API = expr.NewAPIExpr("test", func() {}) + expr.Root.API.HTTP = new(expr.HTTPExpr) + expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()} + + if !eval.Execute(dslFunc, nil) { + t.Fatalf("DSL execution failed: %s", eval.Context.Error()) + } + + if err := eval.RunDSL(); err != nil { + t.Fatalf("DSL run failed: %v", err) + } + + return expr.Root +} \ No newline at end of file From 1f7dc0db4f968149445216ab46a741759082f631 Mon Sep 17 00:00:00 2001 From: Bijan Rahnema Date: Sun, 10 Aug 2025 10:38:05 +0200 Subject: [PATCH 2/2] update dsl/doc.go comment showing new webhook structure --- dsl/doc.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dsl/doc.go b/dsl/doc.go index 1744b3ae35..32be145be2 100644 --- a/dsl/doc.go +++ b/dsl/doc.go @@ -50,7 +50,15 @@ The general structure of the DSL is shown below (partial list): │ ├── Error │ ├── GRPC │ └── HTTP - └── Files + ├── Files + └── Webhook + ├── WebhookDescription + ├── WebhookPayload + └── WebhookHTTP + ├── WebhookMethod/POST/GET/etc. + ├── WebhookPath + ├── WebhookHeaders + └── WebhookResponse */ package dsl