diff --git a/adapter/README.md b/adapter/README.md index 50da103..0b2097d 100644 --- a/adapter/README.md +++ b/adapter/README.md @@ -9,7 +9,8 @@ This directory contains framework-specific adapters for `oaswrap/spec` that prov | Framework | Adapter | Go Module | Description | |-----------|---------|-----------|-------------| | [Chi](https://github.com/go-chi/chi) | [`chiopenapi`](./chiopenapi) | `github.com/oaswrap/spec/adapter/chiopenapi` | Lightweight router with middleware support | -| [Echo](https://github.com/labstack/echo) | [`echoopenapi`](./echoopenapi) | `github.com/oaswrap/spec/adapter/echoopenapi` | High performance, extensible, minimalist framework | +| [Echo v4](https://github.com/labstack/echo) | [`echoopenapi`](./echoopenapi) | `github.com/oaswrap/spec/adapter/echoopenapi` | High performance, extensible, minimalist framework | +| [Echo v5](https://github.com/labstack/echo) | [`echov5openapi`](./echov5openapi) | `github.com/oaswrap/spec/adapter/echov5openapi` | Echo v5 with updated Context API | | [Fiber](https://github.com/gofiber/fiber) | [`fiberopenapi`](./fiberopenapi) | `github.com/oaswrap/spec/adapter/fiberopenapi` | Express-inspired framework built on Fasthttp | | [Gin](https://github.com/gin-gonic/gin) | [`ginopenapi`](./ginopenapi) | `github.com/oaswrap/spec/adapter/ginopenapi` | Fast HTTP web framework with zero allocation | | [net/http](https://pkg.go.dev/net/http) | [`httpopenapi`](./httpopenapi) | `github.com/oaswrap/spec/adapter/httpopenapi` | Standard library HTTP package | diff --git a/adapter/echov5openapi/README.MD b/adapter/echov5openapi/README.MD new file mode 100644 index 0000000..05d42b9 --- /dev/null +++ b/adapter/echov5openapi/README.MD @@ -0,0 +1,244 @@ +# echov5openapi + +[![Go Reference](https://pkg.go.dev/badge/github.com/oaswrap/spec/adapter/echov5openapi.svg)](https://pkg.go.dev/github.com/oaswrap/spec/adapter/echov5openapi) +[![Go Report Card](https://goreportcard.com/badge/github.com/oaswrap/spec/adapter/echov5openapi)](https://goreportcard.com/report/github.com/oaswrap/spec/adapter/echov5openapi) + +A lightweight adapter for the [Echo v5](https://github.com/labstack/echo) web framework that automatically generates OpenAPI 3.x specifications from your routes using [`oaswrap/spec`](https://github.com/oaswrap/spec). + +> **Note:** This adapter is for Echo v5. For Echo v4, use [`echoopenapi`](../echoopenapi). + +## Echo v5 Key Changes + +Echo v5 includes several breaking changes from v4: + +- **Handler signature changed**: `func(c echo.Context) error` -> `func(c *echo.Context) error` (Context is now a pointer) +- **Logger**: Now uses Go's standard `log/slog` instead of custom interface +- **Route returns**: Methods return `RouteInfo` instead of `*Route` +- **Static file methods**: Now accept middleware parameters + +## Features + +- **Seamless Integration** - Works with your existing Echo v5 routes and handlers +- **Automatic Documentation** - Generate OpenAPI specs from route definitions and struct tags +- **Type Safety** - Full Go type safety for OpenAPI configuration +- **Multiple UI Options** - Swagger UI, Stoplight Elements, ReDoc, Scalar or RapiDoc served automatically at `/docs` +- **YAML Export** - OpenAPI spec available at `/docs/openapi.yaml` +- **Zero Overhead** - Minimal performance impact on your API + +## Installation + +```bash +go get github.com/oaswrap/spec/adapter/echov5openapi +``` + +## Quick Start + +```go +package main + +import ( + "log" + + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec/adapter/echov5openapi" + "github.com/oaswrap/spec/option" +) + +func main() { + e := echo.New() + + // Create a new OpenAPI router + r := echov5openapi.NewRouter(e, + option.WithTitle("My API"), + option.WithVersion("1.0.0"), + option.WithSecurity("bearerAuth", option.SecurityHTTPBearer("Bearer")), + ) + // Add routes + v1 := r.Group("/api/v1") + + v1.POST("/login", LoginHandler).With( + option.Summary("User login"), + option.Request(new(LoginRequest)), + option.Response(200, new(LoginResponse)), + ) + + auth := v1.Group("", AuthMiddleware).With( + option.GroupSecurity("bearerAuth"), + ) + auth.GET("/users/:id", GetUserHandler).With( + option.Summary("Get user by ID"), + option.Request(new(GetUserRequest)), + option.Response(200, new(User)), + ) + + log.Printf("OpenAPI docs available at: %s", "http://localhost:3000/docs") + + if err := e.Start(":3000"); err != nil { + log.Fatal(err) + } +} + +type LoginRequest struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +type GetUserRequest struct { + ID string `param:"id" required:"true"` +} + +type User struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Note: Echo v5 uses *echo.Context instead of echo.Context +func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + // Simulate authentication logic + authHeader := c.Request().Header.Get("Authorization") + if authHeader != "" && authHeader == "Bearer example-token" { + return next(c) + } + return c.JSON(401, map[string]string{"error": "Unauthorized"}) + } +} + +func LoginHandler(c *echo.Context) error { + var req LoginRequest + if err := c.Bind(&req); err != nil { + return c.JSON(400, map[string]string{"error": "Invalid request"}) + } + // Simulate login logic + return c.JSON(200, LoginResponse{Token: "example-token"}) +} + +func GetUserHandler(c *echo.Context) error { + var req GetUserRequest + if err := c.Bind(&req); err != nil { + return c.JSON(400, map[string]string{"error": "Invalid request"}) + } + // Simulate fetching user by ID + user := User{ID: req.ID, Name: "John Doe"} + return c.JSON(200, user) +} +``` + +## Migrating from Echo v4 + +If you're migrating from the v4 adapter (`echoopenapi`), here are the main changes: + +1. **Update import path**: + ```go + // Before (v4) + import "github.com/oaswrap/spec/adapter/echoopenapi" + + // After (v5) + import "github.com/oaswrap/spec/adapter/echov5openapi" + ``` + +2. **Update handler signatures**: + ```go + // Before (v4) + func MyHandler(c echo.Context) error { ... } + + // After (v5) + func MyHandler(c *echo.Context) error { ... } + ``` + +3. **Update middleware signatures**: + ```go + // Before (v4) + func MyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { ... } + } + + // After (v5) + func MyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { ... } + } + ``` + +## Documentation Features + +### Built-in Endpoints +When you create an echov5openapi router, the following endpoints are automatically available: + +- **`/docs`** - Interactive UI documentation +- **`/docs/openapi.yaml`** - Raw OpenAPI specification in YAML format + +If you want to disable the built-in UI, you can do so by passing `option.WithDisableDocs()` when creating the router: + +```go +r := echov5openapi.NewRouter(c, + option.WithTitle("My API"), + option.WithVersion("1.0.0"), + option.WithDisableDocs(), +) +``` + +### Supported Documentation UIs +Choose from multiple UI options, powered by [`oaswrap/spec-ui`](https://github.com/oaswrap/spec-ui): + +- **Stoplight Elements** - Modern, clean design (default) +- **Swagger UI** - Classic interface with try-it functionality +- **ReDoc** - Three-panel responsive layout +- **Scalar** - Beautiful and fast interface +- **RapiDoc** - Highly customizable + +```go +r := echov5openapi.NewRouter(c, + option.WithTitle("My API"), + option.WithVersion("1.0.0"), + option.WithScalar(), // Use Scalar as the documentation UI +) +``` + +### Rich Schema Documentation +Use struct tags to generate detailed OpenAPI schemas. **Note: These tags are used only for OpenAPI spec generation and documentation - they do not perform actual request validation.** + +```go +type CreateProductRequest struct { + Name string `json:"name" required:"true" minLength:"1" maxLength:"100"` + Description string `json:"description" maxLength:"500"` + Price float64 `json:"price" required:"true" minimum:"0" maximum:"999999.99"` + Category string `json:"category" required:"true" enum:"electronics,books,clothing"` + Tags []string `json:"tags" maxItems:"10"` + InStock bool `json:"in_stock" default:"true"` +} +``` + +For more struct tag options, see the [swaggest/openapi-go](https://github.com/swaggest/openapi-go?tab=readme-ov-file#features). + +## Examples + +Check out complete examples in the main repository: +- [Basic](https://github.com/oaswrap/spec/tree/main/examples/adapter/echov5openapi/basic) + +## Best Practices + +1. **Organize with Tags** - Group related operations using `option.Tags()` +2. **Document Everything** - Use `option.Summary()` and `option.Description()` for all routes +3. **Define Error Responses** - Include common error responses (400, 401, 404, 500) +4. **Use Validation Tags** - Leverage struct tags for request validation documentation +5. **Security First** - Define and apply appropriate security schemes +6. **Version Your API** - Use route groups for API versioning (`/api/v1`, `/api/v2`) + +## API Reference + +- **Spec**: [pkg.go.dev/github.com/oaswrap/spec](https://pkg.go.dev/github.com/oaswrap/spec) +- **Echo v5 Adapter**: [pkg.go.dev/github.com/oaswrap/spec/adapter/echov5openapi](https://pkg.go.dev/github.com/oaswrap/spec/adapter/echov5openapi) +- **Options**: [pkg.go.dev/github.com/oaswrap/spec/option](https://pkg.go.dev/github.com/oaswrap/spec/option) +- **Spec UI**: [pkg.go.dev/github.com/oaswrap/spec-ui](https://pkg.go.dev/github.com/oaswrap/spec-ui) + +## Contributing + +We welcome contributions! Please open issues and PRs at the main [oaswrap/spec](https://github.com/oaswrap/spec) repository. + +## License + +[MIT](../../LICENSE) diff --git a/adapter/echov5openapi/examples/basic/go.mod b/adapter/echov5openapi/examples/basic/go.mod new file mode 100644 index 0000000..8e0ca9f --- /dev/null +++ b/adapter/echov5openapi/examples/basic/go.mod @@ -0,0 +1,20 @@ +module github.com/oaswrap/spec/adapter/echov5openapi/examples/basic + +go 1.25.0 + +require ( + github.com/labstack/echo/v5 v5.0.2 + github.com/oaswrap/spec v0.3.6 + github.com/oaswrap/spec/adapter/echov5openapi v0.0.0 +) + +require ( + github.com/kr/text v0.2.0 // indirect + github.com/oaswrap/spec-ui v0.1.4 // indirect + github.com/swaggest/jsonschema-go v0.3.78 // indirect + github.com/swaggest/openapi-go v0.2.60 // indirect + github.com/swaggest/refl v1.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +replace github.com/oaswrap/spec/adapter/echov5openapi => ../.. diff --git a/adapter/echov5openapi/examples/basic/go.sum b/adapter/echov5openapi/examples/basic/go.sum new file mode 100644 index 0000000..20351a3 --- /dev/null +++ b/adapter/echov5openapi/examples/basic/go.sum @@ -0,0 +1,50 @@ +github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE= +github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v5 v5.0.2 h1:DwPe1Rla27Zf3QxbW+DxhPKRIbKHHTgHQyaLJC2gE3s= +github.com/labstack/echo/v5 v5.0.2/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +github.com/oaswrap/spec v0.3.6 h1:igKJvrrEYP/pK5I4TzEzYVcdbbr8eJ1gfALUXgZ/Oc8= +github.com/oaswrap/spec v0.3.6/go.mod h1:e6cGQJcVCkQozwsw8T0ydSWEgQPA/dHFmQME4KawOYU= +github.com/oaswrap/spec-ui v0.1.4 h1:XM2Z/ZS2Su90EtDSVuOHGr2+DLpVc2933mxkn6F4aeU= +github.com/oaswrap/spec-ui v0.1.4/go.mod h1:D8EnD6zbYJ3q65wdltw6QHXfw+nut5XwSSA1xtlSEQQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.78 h1:5+YFQrLxOR8z6CHvgtZc42WRy/Q9zRQQ4HoAxlinlHw= +github.com/swaggest/jsonschema-go v0.3.78/go.mod h1:4nniXBuE+FIGkOGuidjOINMH7OEqZK3HCSbfDuLRI0g= +github.com/swaggest/openapi-go v0.2.60 h1:kglHH/WIfqAglfuWL4tu0LPakqNYySzklUWx06SjSKo= +github.com/swaggest/openapi-go v0.2.60/go.mod h1:jmFOuYdsWGtHU0BOuILlHZQJxLqHiAE6en+baE+QQUk= +github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k= +github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/adapter/echov5openapi/examples/basic/main.go b/adapter/echov5openapi/examples/basic/main.go new file mode 100644 index 0000000..a4d5144 --- /dev/null +++ b/adapter/echov5openapi/examples/basic/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "log" + + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec/adapter/echov5openapi" + "github.com/oaswrap/spec/option" +) + +func main() { + e := echo.New() + + // Create a new OpenAPI router + r := echov5openapi.NewRouter(e, + option.WithTitle("My API"), + option.WithVersion("1.0.0"), + option.WithSecurity("bearerAuth", option.SecurityHTTPBearer("Bearer")), + ) + // Add routes + v1 := r.Group("/api/v1") + + v1.POST("/login", LoginHandler).With( + option.Summary("User login"), + option.Request(new(LoginRequest)), + option.Response(200, new(LoginResponse)), + ) + + auth := v1.Group("", AuthMiddleware).With( + option.GroupSecurity("bearerAuth"), + ) + auth.GET("/users/:id", GetUserHandler).With( + option.Summary("Get user by ID"), + option.Request(new(GetUserRequest)), + option.Response(200, new(User)), + ) + + log.Printf("OpenAPI docs available at: %s", "http://localhost:3000/docs") + + if err := e.Start(":3000"); err != nil { + log.Fatal(err) + } +} + +type LoginRequest struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +type GetUserRequest struct { + ID string `param:"id" required:"true"` +} + +type User struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + // Simulate authentication logic + authHeader := c.Request().Header.Get("Authorization") + if authHeader != "" && authHeader == "Bearer example-token" { + return next(c) + } + return c.JSON(401, map[string]string{"error": "Unauthorized"}) + } +} + +func LoginHandler(c *echo.Context) error { + var req LoginRequest + if err := c.Bind(&req); err != nil { + return c.JSON(400, map[string]string{"error": "Invalid request"}) + } + // Simulate login logic + return c.JSON(200, LoginResponse{Token: "example-token"}) +} + +func GetUserHandler(c *echo.Context) error { + var req GetUserRequest + if err := c.Bind(&req); err != nil { + return c.JSON(400, map[string]string{"error": "Invalid request"}) + } + // Simulate fetching user by ID + user := User{ID: req.ID, Name: "John Doe"} + return c.JSON(200, user) +} diff --git a/adapter/echov5openapi/go.mod b/adapter/echov5openapi/go.mod new file mode 100644 index 0000000..0a8fec5 --- /dev/null +++ b/adapter/echov5openapi/go.mod @@ -0,0 +1,22 @@ +module github.com/oaswrap/spec/adapter/echov5openapi + +go 1.25.0 + +require ( + github.com/labstack/echo/v5 v5.0.0 + github.com/oaswrap/spec v0.3.6 + github.com/oaswrap/spec-ui v0.1.4 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/swaggest/jsonschema-go v0.3.78 // indirect + github.com/swaggest/openapi-go v0.2.60 // indirect + github.com/swaggest/refl v1.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/adapter/echov5openapi/go.sum b/adapter/echov5openapi/go.sum new file mode 100644 index 0000000..e56a38c --- /dev/null +++ b/adapter/echov5openapi/go.sum @@ -0,0 +1,50 @@ +github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE= +github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4= +github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +github.com/oaswrap/spec v0.3.6 h1:igKJvrrEYP/pK5I4TzEzYVcdbbr8eJ1gfALUXgZ/Oc8= +github.com/oaswrap/spec v0.3.6/go.mod h1:e6cGQJcVCkQozwsw8T0ydSWEgQPA/dHFmQME4KawOYU= +github.com/oaswrap/spec-ui v0.1.4 h1:XM2Z/ZS2Su90EtDSVuOHGr2+DLpVc2933mxkn6F4aeU= +github.com/oaswrap/spec-ui v0.1.4/go.mod h1:D8EnD6zbYJ3q65wdltw6QHXfw+nut5XwSSA1xtlSEQQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.78 h1:5+YFQrLxOR8z6CHvgtZc42WRy/Q9zRQQ4HoAxlinlHw= +github.com/swaggest/jsonschema-go v0.3.78/go.mod h1:4nniXBuE+FIGkOGuidjOINMH7OEqZK3HCSbfDuLRI0g= +github.com/swaggest/openapi-go v0.2.60 h1:kglHH/WIfqAglfuWL4tu0LPakqNYySzklUWx06SjSKo= +github.com/swaggest/openapi-go v0.2.60/go.mod h1:jmFOuYdsWGtHU0BOuILlHZQJxLqHiAE6en+baE+QQUk= +github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k= +github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/adapter/echov5openapi/internal/constant/constant.go b/adapter/echov5openapi/internal/constant/constant.go new file mode 100644 index 0000000..8738009 --- /dev/null +++ b/adapter/echov5openapi/internal/constant/constant.go @@ -0,0 +1,7 @@ +package constant + +const ( + DefaultTitle = "Echo OpenAPI" + DefaultDescription = "OpenAPI documentation for Echo applications" + DefaultVersion = "1.0.0" +) diff --git a/adapter/echov5openapi/route.go b/adapter/echov5openapi/route.go new file mode 100644 index 0000000..c0ce6a2 --- /dev/null +++ b/adapter/echov5openapi/route.go @@ -0,0 +1,34 @@ +package echov5openapi + +import ( + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec" + "github.com/oaswrap/spec/option" +) + +type route struct { + echoRoute echo.RouteInfo + specRoute spec.Route +} + +var _ Route = (*route)(nil) + +func (r *route) Method() string { + return r.echoRoute.Method +} + +func (r *route) Path() string { + return r.echoRoute.Path +} + +func (r *route) Name() string { + return r.echoRoute.Name +} + +func (r *route) With(opts ...option.OperationOption) Route { + if r.specRoute == nil { + return r + } + r.specRoute.With(opts...) + return r +} diff --git a/adapter/echov5openapi/router.go b/adapter/echov5openapi/router.go new file mode 100644 index 0000000..9a446dd --- /dev/null +++ b/adapter/echov5openapi/router.go @@ -0,0 +1,176 @@ +package echov5openapi + +import ( + "io/fs" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec" + specui "github.com/oaswrap/spec-ui" + "github.com/oaswrap/spec/adapter/echov5openapi/internal/constant" + "github.com/oaswrap/spec/openapi" + "github.com/oaswrap/spec/option" + "github.com/oaswrap/spec/pkg/mapper" + "github.com/oaswrap/spec/pkg/parser" +) + +type router struct { + echoGroup *echo.Group + specRouter spec.Router + gen spec.Generator +} + +// NewRouter creates a new OpenAPI router with the provided Echo instance and options. +// +// It initializes the OpenAPI configuration and sets up the necessary routes for serving. +func NewRouter(e *echo.Echo, opts ...option.OpenAPIOption) Generator { + return NewGenerator(e, opts...) +} + +// NewGenerator creates a new OpenAPI generator with the provided Echo instance and options. +// +// It initializes the OpenAPI configuration and sets up the necessary routes for serving. +func NewGenerator(e *echo.Echo, opts ...option.OpenAPIOption) Generator { + defaultOpts := []option.OpenAPIOption{ + option.WithTitle(constant.DefaultTitle), + option.WithDescription(constant.DefaultDescription), + option.WithVersion(constant.DefaultVersion), + option.WithPathParser(parser.NewColonParamParser()), + option.WithStoplightElements(), + option.WithCacheAge(0), + option.WithReflectorConfig( + option.ParameterTagMapping(openapi.ParameterInPath, "param"), + ), + } + opts = append(defaultOpts, opts...) + gen := spec.NewRouter(opts...) + cfg := gen.Config() + + rr := &router{ + echoGroup: e.Group(""), + specRouter: gen, + gen: gen, + } + + if cfg.DisableDocs { + return rr + } + + handler := specui.NewHandler(mapper.SpecUIOpts(gen)...) + + rr.echoGroup.GET(cfg.DocsPath, echo.WrapHandler(handler.Docs())) + rr.echoGroup.GET(cfg.SpecPath, echo.WrapHandler(handler.Spec())) + + return rr +} + +func (r *router) Add(method, path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + if handler == nil { + handler = func(c *echo.Context) error { + return c.NoContent(http.StatusNotImplemented) + } + } + echoRoute := r.echoGroup.Add(method, path, handler, m...) + route := &route{echoRoute: echoRoute} + + if method == http.MethodConnect { + // CONNECT method is not supported by OpenAPI, so we skip it + return route + } + route.specRoute = r.specRouter.Add(method, path) + + return route +} + +func (r *router) GET(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodGet, path, handler, m...) +} + +func (r *router) POST(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodPost, path, handler, m...) +} + +func (r *router) PUT(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodPut, path, handler, m...) +} + +func (r *router) DELETE(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodDelete, path, handler, m...) +} + +func (r *router) PATCH(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodPatch, path, handler, m...) +} + +func (r *router) HEAD(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodHead, path, handler, m...) +} + +func (r *router) OPTIONS(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodOptions, path, handler, m...) +} + +func (r *router) TRACE(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodTrace, path, handler, m...) +} + +func (r *router) CONNECT(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route { + return r.Add(http.MethodConnect, path, handler, m...) +} + +func (r *router) Group(prefix string, m ...echo.MiddlewareFunc) Router { + group := r.echoGroup.Group(prefix, m...) + specGroup := r.specRouter.Group(prefix) + + return &router{ + echoGroup: group, + specRouter: specGroup, + gen: r.gen, + } +} + +func (r *router) Use(m ...echo.MiddlewareFunc) Router { + r.echoGroup.Use(m...) + return r +} + +func (r *router) File(path, file string, m ...echo.MiddlewareFunc) { + r.echoGroup.File(path, file, m...) +} + +func (r *router) FileFS(path, file string, fs fs.FS, m ...echo.MiddlewareFunc) { + r.echoGroup.FileFS(path, file, fs, m...) +} + +func (r *router) Static(prefix, root string, m ...echo.MiddlewareFunc) { + r.echoGroup.Static(prefix, root, m...) +} + +func (r *router) StaticFS(prefix string, fs fs.FS, m ...echo.MiddlewareFunc) { + r.echoGroup.StaticFS(prefix, fs, m...) +} + +func (r *router) With(opts ...option.GroupOption) Router { + r.specRouter.With(opts...) + return r +} + +func (r *router) WriteSchemaTo(filepath string) error { + return r.gen.WriteSchemaTo(filepath) +} + +func (r *router) MarshalYAML() ([]byte, error) { + return r.gen.MarshalYAML() +} + +func (r *router) MarshalJSON() ([]byte, error) { + return r.gen.MarshalJSON() +} + +func (r *router) GenerateSchema(format ...string) ([]byte, error) { + return r.gen.GenerateSchema(format...) +} + +func (r *router) Validate() error { + return r.gen.Validate() +} diff --git a/adapter/echov5openapi/router_test.go b/adapter/echov5openapi/router_test.go new file mode 100644 index 0000000..39c4426 --- /dev/null +++ b/adapter/echov5openapi/router_test.go @@ -0,0 +1,657 @@ +package echov5openapi_test + +import ( + "flag" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec/adapter/echov5openapi" + "github.com/oaswrap/spec/openapi" + "github.com/oaswrap/spec/option" + "github.com/oaswrap/spec/pkg/dto" + "github.com/oaswrap/spec/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//nolint:gochecknoglobals // test flag for golden file updates +var update = flag.Bool("update", false, "update golden files") + +type HelloRequest struct { + Name string `json:"name" query:"name"` +} + +type HelloResponse struct { + Response string `json:"response"` +} + +func HelloHandler(c *echo.Context) error { + var req HelloRequest + if err := c.Bind(&req); err != nil { + return c.JSON(400, map[string]string{"error": "Invalid request"}) + } + return c.JSON(200, map[string]string{"response": "Hello " + req.Name}) +} + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Name string `json:"name"` +} + +type Response[T any] struct { + Status int `json:"status"` + Data T `json:"data"` +} + +type Token struct { + Token string `json:"token"` +} + +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ErrorResponse struct { + Status int `json:"status"` + Title string `json:"title"` + Detail string `json:"detail,omitempty"` +} + +type ValidationResponse struct { + ErrorResponse + + Errors []struct { + Field string `json:"field"` + Message string `json:"message"` + } `json:"errors"` +} + +func DummyHandler(c *echo.Context) error { + return c.JSON(200, map[string]string{"message": "Dummy handler"}) +} + +func TestRouter_Spec(t *testing.T) { + tests := []struct { + name string + golden string + opts []option.OpenAPIOption + setup func(r echov5openapi.Router) + shouldErr bool + }{ + { + name: "Petstore API", + golden: "petstore", + opts: []option.OpenAPIOption{ + option.WithDescription("This is a sample Petstore server."), + option.WithVersion("1.0.0"), + option.WithTermsOfService("https://swagger.io/terms/"), + option.WithContact(openapi.Contact{ + Email: "apiteam@swagger.io", + }), + option.WithLicense(openapi.License{ + Name: "Apache 2.0", + URL: "https://www.apache.org/licenses/LICENSE-2.0.html", + }), + option.WithExternalDocs("https://swagger.io", "Find more info here about swagger"), + option.WithServer("https://petstore3.swagger.io/api/v3"), + option.WithTags( + openapi.Tag{ + Name: "pet", + Description: "Everything about your Pets", + ExternalDocs: &openapi.ExternalDocs{ + Description: "Find out more about our Pets", + URL: "https://swagger.io", + }, + }, + openapi.Tag{ + Name: "store", + Description: "Access to Petstore orders", + ExternalDocs: &openapi.ExternalDocs{ + Description: "Find out more about our Store", + URL: "https://swagger.io", + }, + }, + openapi.Tag{ + Name: "user", + Description: "Operations about user", + }, + ), + option.WithSecurity("petstore_auth", option.SecurityOAuth2( + openapi.OAuthFlows{ + Implicit: &openapi.OAuthFlowsImplicit{ + AuthorizationURL: "https://petstore3.swagger.io/oauth/authorize", + Scopes: map[string]string{ + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }), + ), + option.WithSecurity("apiKey", option.SecurityAPIKey("api_key", openapi.SecuritySchemeAPIKeyInHeader)), + }, + setup: func(r echov5openapi.Router) { + pet := r.Group("/pet").With( + option.GroupTags("pet"), + option.GroupSecurity("petstore_auth", "write:pets", "read:pets"), + ) + pet.PUT("/", nil).With( + option.OperationID("updatePet"), + option.Summary("Update an existing pet"), + option.Description("Update the details of an existing pet in the store."), + option.Request(new(dto.Pet)), + option.Response(200, new(dto.Pet)), + ) + pet.POST("/", nil).With( + option.OperationID("addPet"), + option.Summary("Add a new pet"), + option.Description("Add a new pet to the store."), + option.Request(new(dto.Pet)), + option.Response(201, new(dto.Pet)), + ) + pet.GET("/findByStatus", nil).With( + option.OperationID("findPetsByStatus"), + option.Summary("Find pets by status"), + option.Description("Finds Pets by status. Multiple status values can be provided with comma separated strings."), + option.Request(new(struct { + Status string `query:"status" enum:"available,pending,sold"` + })), + option.Response(200, new([]dto.Pet)), + ) + pet.GET("/findByTags", nil).With( + option.OperationID("findPetsByTags"), + option.Summary("Find pets by tags"), + option.Description("Finds Pets by tags. Multiple tags can be provided with comma separated strings."), + option.Request(new(struct { + Tags []string `query:"tags"` + })), + option.Response(200, new([]dto.Pet)), + ) + pet.POST("/{petId}/uploadImage", nil).With( + option.OperationID("uploadFile"), + option.Summary("Upload an image for a pet"), + option.Description("Uploads an image for a pet."), + option.Request(new(dto.UploadImageRequest)), + option.Response(200, new(dto.APIResponse)), + ) + pet.GET("/{petId}", nil).With( + option.OperationID("getPetById"), + option.Summary("Get pet by ID"), + option.Description("Retrieve a pet by its ID."), + option.Request(new(struct { + ID int `param:"petId" required:"true"` + })), + option.Response(200, new(dto.Pet)), + ) + pet.POST("/{petId}", nil).With( + option.OperationID("updatePetWithForm"), + option.Summary("Update pet with form"), + option.Description("Updates a pet in the store with form data."), + option.Request(new(dto.UpdatePetWithFormRequest)), + option.Response(200, nil), + ) + pet.DELETE("/{petId}", nil).With( + option.OperationID("deletePet"), + option.Summary("Delete a pet"), + option.Description("Delete a pet from the store by its ID."), + option.Request(new(dto.DeletePetRequest)), + option.Response(204, nil), + ) + store := r.Group("/store").With( + option.GroupTags("store"), + ) + store.POST("/order", nil).With( + option.OperationID("placeOrder"), + option.Summary("Place an order"), + option.Description("Place a new order for a pet."), + option.Request(new(dto.Order)), + option.Response(201, new(dto.Order)), + ) + store.GET("/order/{orderId}", nil).With( + option.OperationID("getOrderById"), + option.Summary("Get order by ID"), + option.Description("Retrieve an order by its ID."), + option.Request(new(struct { + ID int `param:"orderId" required:"true"` + })), + option.Response(200, new(dto.Order)), + option.Response(404, nil), + ) + store.DELETE("/order/{orderId}", nil).With( + option.OperationID("deleteOrder"), + option.Summary("Delete an order"), + option.Description("Delete an order by its ID."), + option.Request(new(struct { + ID int `param:"orderId" required:"true"` + })), + option.Response(204, nil), + ) + + user := r.Group("/user").With( + option.GroupTags("user"), + ) + user.POST("/createWithList", nil).With( + option.OperationID("createUsersWithList"), + option.Summary("Create users with list"), + option.Description("Create multiple users in the store with a list."), + option.Request(new([]dto.PetUser)), + option.Response(201, nil), + ) + user.POST("/", nil).With( + option.OperationID("createUser"), + option.Summary("Create a new user"), + option.Description("Create a new user in the store."), + option.Request(new(dto.PetUser)), + option.Response(201, new(dto.PetUser)), + ) + user.GET("/{username}", nil).With( + option.OperationID("getUserByName"), + option.Summary("Get user by username"), + option.Description("Retrieve a user by their username."), + option.Request(new(struct { + Username string `param:"username" required:"true"` + })), + option.Response(200, new(dto.PetUser)), + option.Response(404, nil), + ) + user.PUT("/{username}", nil).With( + option.OperationID("updateUser"), + option.Summary("Update an existing user"), + option.Description("Update the details of an existing user."), + option.Request(new(struct { + dto.PetUser + + Username string `param:"username" required:"true"` + })), + option.Response(200, new(dto.PetUser)), + option.Response(404, nil), + ) + user.DELETE("/{username}", nil).With( + option.OperationID("deleteUser"), + option.Summary("Delete a user"), + option.Description("Delete a user from the store by their username."), + option.Request(new(struct { + Username string `param:"username" required:"true"` + })), + option.Response(204, nil), + ) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := echo.New() + r := echov5openapi.NewRouter(e, tt.opts...) + tt.setup(r) + + err := r.Validate() + if tt.shouldErr { + require.Error(t, err, "Expected error for test: %s", tt.name) + return + } + require.NoError(t, err, "Expected no error for test: %s", tt.name) + + // Test the OpenAPI schema generation + schema, err := r.GenerateSchema() + require.NoError(t, err, "failed to generate schema") + + golden := filepath.Join("testdata", tt.golden+".yaml") + if *update { + err = r.WriteSchemaTo(golden) + require.NoError(t, err, "failed to write golden file") + t.Logf("Updated golden file: %s", golden) + } + + want, err := os.ReadFile(golden) + require.NoError(t, err, "failed to read golden file %s", golden) + + testutil.EqualYAML(t, want, schema) + }) + } +} + +type SingleRouteFunc func(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) echov5openapi.Route + +func TestRouter_Single(t *testing.T) { + tests := []struct { + method string + path string + methodFunc func(r echov5openapi.Router) SingleRouteFunc + }{ + {"GET", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.GET }}, + {"POST", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.POST }}, + {"PUT", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.PUT }}, + {"PATCH", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.PATCH }}, + {"DELETE", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.DELETE }}, + {"HEAD", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.HEAD }}, + {"OPTIONS", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.OPTIONS }}, + {"TRACE", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.TRACE }}, + {"CONNECT", "/hello", func(r echov5openapi.Router) SingleRouteFunc { return r.CONNECT }}, + } + for _, tt := range tests { + t.Run(tt.method, func(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Single"), + option.WithVersion("1.0.0"), + ) + // Setup the route + route := tt.methodFunc(r)(tt.path, HelloHandler).With( + option.Summary("Hello Handler"), + option.Description("Handles hello requests"), + option.OperationID(fmt.Sprintf("hello%s", tt.method)), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + // Verify the route is registered + assert.Equal(t, tt.method, route.Method(), "Expected method to be %s", tt.method) + assert.Equal(t, tt.path, route.Path(), "Expected path to be %s", tt.path) + assert.NotEmpty(t, route.Name(), "Expected route name to be set") + req := httptest.NewRequest(tt.method, tt.path, nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for %s %s", tt.method, tt.path) + assert.Contains(t, rec.Body.String(), "Hello", "Expected response to contain 'Hello'") + + // Verify the OpenAPI schema + if tt.method == http.MethodConnect { + schema, err := r.MarshalYAML() + require.NoError(t, err, "Expected no error while generating OpenAPI schema") + assert.NotEmpty(t, schema, "Expected OpenAPI schema to be generated") + assert.NotContains(t, string(schema), fmt.Sprintf("operationId: hello%s", tt.method)) + return + } + schema, err := r.MarshalYAML() + require.NoError(t, err, "Expected no error while generating OpenAPI schema") + assert.NotEmpty(t, schema, "Expected OpenAPI schema to be generated") + assert.Contains(t, string(schema), fmt.Sprintf("operationId: hello%s", tt.method)) + assert.Contains( + t, + string(schema), + "summary: Hello Handler", + "Expected OpenAPI schema to contain the summary", + ) + }) + } +} + +func TestRouter(t *testing.T) { + t.Run("Use", func(t *testing.T) { + totalCalled := 0 + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + totalCalled++ + return next(c) + } + } + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Middleware"), + option.WithVersion("1.0.0"), + ) + r.Use(middleware) + + r.GET("/test", func(c *echo.Context) error { + return c.String(200, "Hello Middleware") + }) + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + assert.Equal(t, 200, rec.Code, "Expected status code 200") + assert.Equal(t, "Hello Middleware", rec.Body.String(), "Expected response body to be 'Hello Middleware'") + assert.Equal(t, 1, totalCalled, "Expected middleware to be called once") + }) +} + +func TestRouter_Group(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Group"), + option.WithVersion("1.0.0"), + ) + + v1 := r.Group("/v1") + v1.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler V1"), + option.Description("Handles hello requests for V1"), + option.OperationID("helloV1"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + v2 := r.Group("/v2") + v2.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler V2"), + option.Description("Handles hello requests for V2"), + option.OperationID("helloV2"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + req := httptest.NewRequest(http.MethodGet, "/v1/hello?name=World", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /v1/hello") + assert.Contains(t, rec.Body.String(), "Hello World", "Expected response to contain 'Hello World'") + + req = httptest.NewRequest(http.MethodGet, "/v2/hello?name=Echo", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /v2/hello") + assert.Contains(t, rec.Body.String(), "Hello Echo", "Expected response to contain 'Hello Echo'") +} + +func TestRouter_StaticFS(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API StaticFS"), + option.WithVersion("1.0.0"), + ) + tempDir := t.TempDir() + // Create a test file in the temporary directory + testFilePath := fmt.Sprintf("%s/test.txt", tempDir) + if err := os.WriteFile(testFilePath, []byte("This is a test file."), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + // Serve static files from the temporary directory + r.StaticFS("/static", os.DirFS(tempDir)) + + req := httptest.NewRequest(http.MethodGet, "/static/test.txt", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /static/test.txt") + assert.Equal(t, "This is a test file.", rec.Body.String(), "Expected response body to match test file content") +} + +func TestRouter_Static(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Static"), + option.WithVersion("1.0.0"), + ) + tempDir := t.TempDir() + // Create a test file in the temporary directory + testFilePath := fmt.Sprintf("%s/test.txt", tempDir) + if err := os.WriteFile(testFilePath, []byte("This is a test file."), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + // Serve static files from the temporary directory + r.Static("/static", tempDir) + + req := httptest.NewRequest(http.MethodGet, "/static/test.txt", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /static/test.txt") + assert.Equal(t, "This is a test file.", rec.Body.String(), "Expected response body to match test file content") +} + +func TestRouter_File(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API File"), + option.WithVersion("1.0.0"), + ) + tempDir := t.TempDir() + // Create a test file in the temporary directory + testFilePath := fmt.Sprintf("%s/test.txt", tempDir) + if err := os.WriteFile(testFilePath, []byte("This is a test file."), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + // Serve a static file + r.File("/test.txt", testFilePath) + + req := httptest.NewRequest(http.MethodGet, "/test.txt", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /test.txt") + assert.Equal(t, "This is a test file.", rec.Body.String(), "Expected response body to match test file content") +} + +func TestRouter_FileFS(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API FileFS"), + option.WithVersion("1.0.0"), + ) + tempDir := t.TempDir() + // Create a test file in the temporary directory + testFilePath := fmt.Sprintf("%s/test.txt", tempDir) + if err := os.WriteFile(testFilePath, []byte("This is a test file."), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + // Serve a static file from the filesystem + r.FileFS("/test.txt", "test.txt", os.DirFS(tempDir)) + + req := httptest.NewRequest(http.MethodGet, "/test.txt", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /test.txt") + assert.Equal(t, "This is a test file.", rec.Body.String(), "Expected response body to match test file content") +} + +func TestGenerator_WriteSchemaTo(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API WriteSchemaTo"), + option.WithVersion("1.0.0"), + ) + + // Define a route + r.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler"), + option.Description("Handles hello requests"), + option.OperationID("hello"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + // Write the OpenAPI schema to a file + tempFile := t.TempDir() + "/openapi.yaml" + err := r.WriteSchemaTo(tempFile) + require.NoError(t, err, "Expected no error while writing OpenAPI schema to file") + + // Verify the file exists and is not empty + info, err := os.Stat(tempFile) + require.NoError(t, err, "Expected no error while checking file stats") + assert.False(t, info.IsDir(), "Expected file to not be a directory") + assert.Positive(t, info.Size(), "Expected file size to be greater than 0") +} + +func TestGenerator_MarshalJSON(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API MarshalJSON"), + option.WithVersion("1.0.0"), + ) + + // Define a route + r.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler"), + option.Description("Handles hello requests"), + option.OperationID("hello"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + // Marshal the OpenAPI schema to JSON + schema, err := r.MarshalJSON() + require.NoError(t, err, "Expected no error while marshaling OpenAPI schema to JSON") + assert.NotEmpty(t, schema, "Expected OpenAPI schema JSON to not be empty") +} + +func TestGenerator_Docs(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Docs"), + option.WithVersion("1.0.0"), + ) + + // Define a route + r.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler"), + option.Description("Handles hello requests"), + option.OperationID("hello"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + req := httptest.NewRequest(http.MethodGet, "/docs", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code, "Expected status code 200 for /docs") + assert.Contains(t, rec.Body.String(), "Test API Docs", "Expected response to contain API title") +} + +func TestGenerator_DisableDocs(t *testing.T) { + e := echo.New() + r := echov5openapi.NewGenerator(e, + option.WithTitle("Test API Disable Docs"), + option.WithVersion("1.0.0"), + option.WithDisableDocs(true), + ) + + // Define a route + r.GET("/hello", HelloHandler).With( + option.Summary("Hello Handler"), + option.Description("Handles hello requests"), + option.OperationID("hello"), + option.Tags("greeting"), + option.Request(new(HelloRequest)), + option.Response(200, new(HelloResponse)), + ) + + req := httptest.NewRequest(http.MethodGet, "/docs", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, 404, rec.Code, "Expected status code 404 for /docs when docs are disabled") +} diff --git a/adapter/echov5openapi/testdata/petstore.yaml b/adapter/echov5openapi/testdata/petstore.yaml new file mode 100644 index 0000000..90f0e1f --- /dev/null +++ b/adapter/echov5openapi/testdata/petstore.yaml @@ -0,0 +1,536 @@ +openapi: 3.0.3 +info: + contact: + email: apiteam@swagger.io + description: This is a sample Petstore server. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: https://swagger.io/terms/ + title: Echo OpenAPI + version: 1.0.0 +externalDocs: + description: Find more info here about swagger + url: https://swagger.io +servers: +- url: https://petstore3.swagger.io/api/v3 +tags: +- description: Everything about your Pets + externalDocs: + description: Find out more about our Pets + url: https://swagger.io + name: pet +- description: Access to Petstore orders + externalDocs: + description: Find out more about our Store + url: https://swagger.io + name: store +- description: Operations about user + name: user +paths: + /pet: + post: + description: Add a new pet to the store. + operationId: addPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPet' + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPet' + description: Created + security: + - petstore_auth: + - write:pets + - read:pets + summary: Add a new pet + tags: + - pet + put: + description: Update the details of an existing pet in the store. + operationId: updatePet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPet' + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPet' + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Update an existing pet + tags: + - pet + /pet/{petId}: + delete: + description: Delete a pet from the store by its ID. + operationId: deletePet + parameters: + - in: path + name: petId + required: true + schema: + type: integer + - in: header + name: api_key + schema: + type: string + responses: + "204": + description: No Content + security: + - petstore_auth: + - write:pets + - read:pets + summary: Delete a pet + tags: + - pet + get: + description: Retrieve a pet by its ID. + operationId: getPetById + parameters: + - in: path + name: petId + required: true + schema: + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPet' + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Get pet by ID + tags: + - pet + post: + description: Updates a pet in the store with form data. + operationId: updatePetWithForm + parameters: + - in: path + name: petId + required: true + schema: + type: integer + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/FormDataDtoUpdatePetWithFormRequest' + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Update pet with form + tags: + - pet + /pet/{petId}/uploadImage: + post: + description: Uploads an image for a pet. + operationId: uploadFile + parameters: + - in: query + name: additionalMetadata + schema: + type: string + - in: path + name: petId + required: true + schema: + format: int64 + type: integer + requestBody: + content: + application/octet-stream: + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoAPIResponse' + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Upload an image for a pet + tags: + - pet + /pet/findByStatus: + get: + description: Finds Pets by status. Multiple status values can be provided with + comma separated strings. + operationId: findPetsByStatus + parameters: + - in: query + name: status + schema: + enum: + - available + - pending + - sold + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/DtoPet' + type: array + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Find pets by status + tags: + - pet + /pet/findByTags: + get: + description: Finds Pets by tags. Multiple tags can be provided with comma separated + strings. + operationId: findPetsByTags + parameters: + - in: query + name: tags + schema: + items: + type: string + type: array + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/DtoPet' + type: array + description: OK + security: + - petstore_auth: + - write:pets + - read:pets + summary: Find pets by tags + tags: + - pet + /store/order: + post: + description: Place a new order for a pet. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoOrder' + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoOrder' + description: Created + summary: Place an order + tags: + - store + /store/order/{orderId}: + delete: + description: Delete an order by its ID. + operationId: deleteOrder + parameters: + - in: path + name: orderId + required: true + schema: + type: integer + responses: + "204": + description: No Content + summary: Delete an order + tags: + - store + get: + description: Retrieve an order by its ID. + operationId: getOrderById + parameters: + - in: path + name: orderId + required: true + schema: + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoOrder' + description: OK + "404": + description: Not Found + summary: Get order by ID + tags: + - store + /user: + post: + description: Create a new user in the store. + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPetUser' + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPetUser' + description: Created + summary: Create a new user + tags: + - user + /user/{username}: + delete: + description: Delete a user from the store by their username. + operationId: deleteUser + parameters: + - in: path + name: username + required: true + schema: + type: string + responses: + "204": + description: No Content + summary: Delete a user + tags: + - user + get: + description: Retrieve a user by their username. + operationId: getUserByName + parameters: + - in: path + name: username + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPetUser' + description: OK + "404": + description: Not Found + summary: Get user by username + tags: + - user + put: + description: Update the details of an existing user. + operationId: updateUser + parameters: + - in: path + name: username + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + email: + type: string + firstName: + type: string + id: + type: integer + lastName: + type: string + password: + type: string + phone: + type: string + userStatus: + enum: + - 0 + - 1 + - 2 + type: integer + username: + type: string + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DtoPetUser' + description: OK + "404": + description: Not Found + summary: Update an existing user + tags: + - user + /user/createWithList: + post: + description: Create multiple users in the store with a list. + operationId: createUsersWithList + requestBody: + content: + application/json: + schema: + items: + $ref: '#/components/schemas/DtoPetUser' + nullable: true + type: array + responses: + "201": + description: Created + summary: Create users with list + tags: + - user +components: + schemas: + DtoAPIResponse: + properties: + code: + type: integer + message: + type: string + type: + type: string + type: object + DtoCategory: + properties: + id: + type: integer + name: + type: string + type: object + DtoOrder: + properties: + complete: + type: boolean + id: + type: integer + petId: + type: integer + quantity: + type: integer + shipDate: + format: date-time + type: string + status: + enum: + - placed + - approved + - delivered + type: string + type: object + DtoPet: + properties: + category: + $ref: '#/components/schemas/DtoCategory' + id: + type: integer + name: + type: string + photoUrls: + items: + type: string + nullable: true + type: array + status: + enum: + - available + - pending + - sold + type: string + tags: + items: + $ref: '#/components/schemas/DtoTag' + nullable: true + type: array + type: + type: string + type: object + DtoPetUser: + properties: + email: + type: string + firstName: + type: string + id: + type: integer + lastName: + type: string + password: + type: string + phone: + type: string + userStatus: + enum: + - 0 + - 1 + - 2 + type: integer + username: + type: string + type: object + DtoTag: + properties: + id: + type: integer + name: + type: string + type: object + FormDataDtoUpdatePetWithFormRequest: + properties: + name: + type: string + status: + enum: + - available + - pending + - sold + type: string + required: + - name + type: object + securitySchemes: + apiKey: + in: header + name: api_key + type: apiKey + petstore_auth: + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + read:pets: read your pets + write:pets: modify pets in your account + type: oauth2 diff --git a/adapter/echov5openapi/types.go b/adapter/echov5openapi/types.go new file mode 100644 index 0000000..c8f55f9 --- /dev/null +++ b/adapter/echov5openapi/types.go @@ -0,0 +1,103 @@ +package echov5openapi + +import ( + "io/fs" + + "github.com/labstack/echo/v5" + "github.com/oaswrap/spec/option" +) + +// Generator defines an Echo-compatible OpenAPI generator. +// +// It combines routing and OpenAPI schema generation. +type Generator interface { + Router + + // Validate checks if the OpenAPI specification is valid. + Validate() error + + // GenerateSchema generates the OpenAPI schema. + // Defaults to YAML. Pass "json" to generate JSON. + GenerateSchema(format ...string) ([]byte, error) + + // MarshalYAML marshals the OpenAPI schema to YAML. + MarshalYAML() ([]byte, error) + + // MarshalJSON marshals the OpenAPI schema to JSON. + MarshalJSON() ([]byte, error) + + // WriteSchemaTo writes the schema to the given file. + // The format is inferred from the file extension. + WriteSchemaTo(filepath string) error +} + +// Router defines an OpenAPI-aware Echo router. +// +// It wraps Echo routes and supports OpenAPI metadata. +type Router interface { + // GET registers a new GET route. + GET(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // POST registers a new POST route. + POST(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // PUT registers a new PUT route. + PUT(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // DELETE registers a new DELETE route. + DELETE(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // PATCH registers a new PATCH route. + PATCH(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // HEAD registers a new HEAD route. + HEAD(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // OPTIONS registers a new OPTIONS route. + OPTIONS(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // TRACE registers a new TRACE route. + TRACE(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // CONNECT registers a new CONNECT route. + CONNECT(path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // Add registers a new route with the given method, path, and handler. + Add(method, path string, handler echo.HandlerFunc, m ...echo.MiddlewareFunc) Route + + // Group creates a new sub-group with the given prefix and middleware. + Group(prefix string, m ...echo.MiddlewareFunc) Router + + // Use adds global middleware. + Use(m ...echo.MiddlewareFunc) Router + + // File serves a single static file. + File(path, file string, m ...echo.MiddlewareFunc) + + // FileFS serves a static file from the given filesystem. + FileFS(path, file string, fs fs.FS, m ...echo.MiddlewareFunc) + + // Static serves static files from a directory under the given prefix. + Static(prefix, root string, m ...echo.MiddlewareFunc) + + // StaticFS serves static files from the given filesystem. + StaticFS(prefix string, fs fs.FS, m ...echo.MiddlewareFunc) + + // With applies OpenAPI group options to this router. + With(opts ...option.GroupOption) Router +} + +// Route represents a single Echo route with OpenAPI metadata. +type Route interface { + // Method returns the HTTP method (GET, POST, etc.). + Method() string + + // Path returns the route path. + Path() string + + // Name returns the route name. + Name() string + + // With applies OpenAPI operation options to this route. + With(opts ...option.OperationOption) Route +} diff --git a/go.work b/go.work index 60a798b..8671868 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.0 +go 1.25.0 use ( . @@ -6,6 +6,8 @@ use ( ./adapter/chiopenapi/examples/basic ./adapter/echoopenapi ./adapter/echoopenapi/examples/basic + ./adapter/echov5openapi + ./adapter/echov5openapi/examples/basic ./adapter/fiberopenapi ./adapter/fiberopenapi/examples/basic ./adapter/ginopenapi diff --git a/go.work.sum b/go.work.sum index c58e3bc..290e374 100644 --- a/go.work.sum +++ b/go.work.sum @@ -25,12 +25,7 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -77,6 +72,7 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -85,6 +81,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -101,6 +98,7 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -111,6 +109,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -133,6 +132,7 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= @@ -148,6 +148,7 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -159,6 +160,7 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -168,6 +170,7 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -179,8 +182,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=