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 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