From 248fd1b4a4c57f57105910a0b31a415c0094c6a2 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Fri, 10 Apr 2026 18:58:07 +0700 Subject: [PATCH 1/4] feat: add WithStripTrailingSlash option to remove trailing slashes from operation paths - Add StripTrailingSlash field to openapi.Config - Add WithStripTrailingSlash() OpenAPI option constructor - Apply stripping in buildOnce before passing paths to reflector - Fix muxopenapi petstore test to use /prefix (leading slash) on PathPrefix calls - Remove unused JoinURL helper from pkg/util - Document WithStripTrailingSlash in README --- README.md | 7 ++ adapter/muxopenapi/router_test.go | 7 +- adapter/muxopenapi/testdata/petstore.yaml | 22 ++-- internal/debuglog/debuglog.go | 2 +- openapi/config.go | 13 +-- option/openapi.go | 11 ++ pkg/util/util.go | 12 +-- pkg/util/util_test.go | 120 +++++++++++----------- router.go | 6 +- 9 files changed, 106 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 2b51de4..a8119fd 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,13 @@ internalGroup := r.Group("/internal", ) ``` +### Path Handling +```go +// Remove trailing slashes from all operation paths in the spec. +// "/pet/" becomes "/pet", "/" is left unchanged. +option.WithStripTrailingSlash() +``` + ## Advanced Features ### Rich Schema Documentation diff --git a/adapter/muxopenapi/router_test.go b/adapter/muxopenapi/router_test.go index 5056d60..705dea1 100644 --- a/adapter/muxopenapi/router_test.go +++ b/adapter/muxopenapi/router_test.go @@ -81,9 +81,10 @@ func TestRouter_Spec(t *testing.T) { }), ), option.WithSecurity("apiKey", option.SecurityAPIKey("api_key", openapi.SecuritySchemeAPIKeyInHeader)), + option.WithStripTrailingSlash(), }, setup: func(r muxopenapi.Router) { - pet := r.PathPrefix("pet").Subrouter().With( + pet := r.PathPrefix("/pet").Subrouter().With( option.GroupTags("pet"), option.GroupSecurity("petstore_auth", "write:pets", "read:pets"), ) @@ -150,7 +151,7 @@ func TestRouter_Spec(t *testing.T) { option.Response(204, nil), ) - store := r.PathPrefix("store").Subrouter().With( + store := r.PathPrefix("/store").Subrouter().With( option.GroupTags("store"), ) store.HandleFunc("/order", nil).Methods("POST").With( @@ -180,7 +181,7 @@ func TestRouter_Spec(t *testing.T) { option.Response(204, nil), ) - user := r.PathPrefix("user").Subrouter().With( + user := r.PathPrefix("/user").Subrouter().With( option.GroupTags("user"), ) user.HandleFunc("/createWithList", nil).Methods("POST").With( diff --git a/adapter/muxopenapi/testdata/petstore.yaml b/adapter/muxopenapi/testdata/petstore.yaml index ee63687..ac16c66 100644 --- a/adapter/muxopenapi/testdata/petstore.yaml +++ b/adapter/muxopenapi/testdata/petstore.yaml @@ -28,7 +28,7 @@ tags: - description: Operations about user name: user paths: - pet/: + /pet: get: description: Update the details of an existing pet in the store. operationId: updatePet @@ -68,7 +68,7 @@ paths: summary: Add a new pet tags: - pet - pet/{petId}: + /pet/{petId}: get: description: Retrieve a pet by its ID. operationId: getPetById @@ -116,7 +116,7 @@ paths: summary: Update pet with form tags: - pet - pet/{petId}/uploadImage: + /pet/{petId}/uploadImage: post: description: Uploads an image for a pet. operationId: uploadFile @@ -150,7 +150,7 @@ paths: summary: Upload an image for a pet tags: - pet - pet/delete/{petId}: + /pet/delete/{petId}: delete: description: Delete a pet from the store by its ID. operationId: deletePet @@ -174,7 +174,7 @@ paths: summary: Delete a pet tags: - pet - pet/findByStatus: + /pet/findByStatus: get: description: Finds Pets by status. Multiple status values can be provided with comma separated strings. @@ -204,7 +204,7 @@ paths: summary: Find pets by status tags: - pet - pet/findByTags: + /pet/findByTags: get: description: Finds Pets by tags. Multiple tags can be provided with comma separated strings. @@ -232,7 +232,7 @@ paths: summary: Find pets by tags tags: - pet - store/order: + /store/order: post: description: Place a new order for a pet. operationId: placeOrder @@ -251,7 +251,7 @@ paths: summary: Place an order tags: - store - store/order/{orderId}: + /store/order/{orderId}: delete: description: Delete an order by its ID. operationId: deleteOrder @@ -288,7 +288,7 @@ paths: summary: Get order by ID tags: - store - user/: + /user: post: description: Create a new user in the store. operationId: createUser @@ -307,7 +307,7 @@ paths: summary: Create a new user tags: - user - user/{username}: + /user/{username}: delete: description: Delete a user from the store by their username. operationId: deleteUser @@ -391,7 +391,7 @@ paths: summary: Update an existing user tags: - user - user/createWithList: + /user/createWithList: post: description: Create multiple users in the store with a list. operationId: createUsersWithList diff --git a/internal/debuglog/debuglog.go b/internal/debuglog/debuglog.go index 5c5a000..661f175 100644 --- a/internal/debuglog/debuglog.go +++ b/internal/debuglog/debuglog.go @@ -81,7 +81,7 @@ func (l *Logger) LogServer(server openapi.Server) { if len(server.Variables) > 0 { serverInfo += ", variables: " for name, variable := range server.Variables { - serverInfo += name + ": " + variable.Default + ", " + serverInfo += name + ": " + variable.Default + ", " //nolint:perfsprint // simple diagnostic string build } serverInfo = serverInfo[:len(serverInfo)-2] // Remove trailing comma and space } diff --git a/openapi/config.go b/openapi/config.go index ca34dcd..e75a875 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -24,12 +24,13 @@ type Config struct { ReflectorConfig *ReflectorConfig // Configuration for schema reflection. - DocsPath string // Path where the documentation will be served. - SpecPath string // Path for the OpenAPI specification JSON or YAML. - CacheAge *int // Cache age for OpenAPI specification responses. - DisableDocs bool // If true, disables serving OpenAPI docs. - Logger Logger // Logger for diagnostic output. - PathParser PathParser // Path parser for framework-specific path conversions. + DocsPath string // Path where the documentation will be served. + SpecPath string // Path for the OpenAPI specification JSON or YAML. + CacheAge *int // Cache age for OpenAPI specification responses. + DisableDocs bool // If true, disables serving OpenAPI docs. + StripTrailingSlash bool // If true, trailing slashes are removed from all operation paths. + Logger Logger // Logger for diagnostic output. + PathParser PathParser // Path parser for framework-specific path conversions. UIProvider config.Provider // UI provider for the OpenAPI documentation. SwaggerUIConfig *config.SwaggerUI // Configuration for embedded Swagger UI. diff --git a/option/openapi.go b/option/openapi.go index 1eb102a..874f31d 100644 --- a/option/openapi.go +++ b/option/openapi.go @@ -176,6 +176,17 @@ func WithDisableDocs(disable ...bool) OpenAPIOption { } } +// WithStripTrailingSlash removes trailing slashes from all registered operation paths +// before they are written to the spec. +// +// For example, "/pet/" becomes "/pet". +// The root path "/" is left unchanged. +func WithStripTrailingSlash(strip ...bool) OpenAPIOption { + return func(c *openapi.Config) { + c.StripTrailingSlash = util.Optional(true, strip...) + } +} + // WithDocsPath sets the path for the OpenAPI documentation. // // This is the path where the OpenAPI documentation will be served. diff --git a/pkg/util/util.go b/pkg/util/util.go index 0beb797..23284e5 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,8 +1,7 @@ -package util //nolint:revive // Utility functions +package util import ( "path" - "strings" ) // Optional returns the first value from the provided values or the default value if no values are provided. @@ -18,15 +17,6 @@ func PtrOf[T any](value T) *T { return &value } -// JoinURL joins the base URL with the provided segments, ensuring proper formatting. -func JoinURL(base string, segments ...string) string { - base = strings.TrimRight(base, "/") - if len(segments) == 0 { - return base - } - return base + "/" + strings.TrimLeft(JoinPath(segments...), "/") -} - // JoinPath joins multiple path segments into a single path, ensuring proper formatting. func JoinPath(paths ...string) string { if len(paths) == 0 { diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 33cab28..5a46694 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -70,105 +70,103 @@ func TestPtrOf(t *testing.T) { }) } -func TestJoinURL(t *testing.T) { +func TestJoinPath(t *testing.T) { tests := []struct { name string - base string - segments []string + paths []string expected string }{ { - name: "base without trailing slash", - base: "https://example.com", - segments: []string{"api", "v1", "users"}, - expected: "https://example.com/api/v1/users", + name: "empty paths", + paths: []string{}, + expected: "", }, { - name: "base with trailing slash", - base: "https://example.com/", - segments: []string{"api", "v1", "users"}, - expected: "https://example.com/api/v1/users", + name: "single path without trailing slash", + paths: []string{"api"}, + expected: "api", }, { - name: "base with multiple trailing slashes", - base: "https://example.com///", - segments: []string{"api", "v1", "users"}, - expected: "https://example.com/api/v1/users", + name: "single path with trailing slash", + paths: []string{"api/"}, + expected: "api/", }, { - name: "empty segments", - base: "https://example.com", - segments: []string{}, - expected: "https://example.com", + name: "two paths without trailing slash", + paths: []string{"api", "v1"}, + expected: "api/v1", }, { - name: "single segment", - base: "https://example.com", - segments: []string{"api"}, - expected: "https://example.com/api", + name: "two paths with trailing slash on last", + paths: []string{"api", "v1/"}, + expected: "api/v1/", }, { - name: "segments with slashes", - base: "https://example.com", - segments: []string{"api/v1", "users"}, - expected: "https://example.com/api/v1/users", + name: "multiple paths without trailing slash", + paths: []string{"api", "v1", "users"}, + expected: "api/v1/users", }, { - name: "segments with leading slashes", - base: "https://example.com", - segments: []string{"/api/v1", "/users"}, - expected: "https://example.com/api/v1/users", + name: "multiple paths with trailing slash on last", + paths: []string{"api", "v1", "users/"}, + expected: "api/v1/users/", }, { - name: "empty base", - base: "", - segments: []string{"api", "v1"}, + name: "absolute paths", + paths: []string{"/api", "v1"}, expected: "/api/v1", }, { - name: "empty", - expected: "", + name: "absolute paths with trailing slash", + paths: []string{"/api", "v1/"}, + expected: "/api/v1/", }, { - name: "base with only slashes", - base: "///", - segments: []string{"api", "v1"}, - expected: "/api/v1", + name: "normalize double slashes", + paths: []string{"api/", "/v1"}, + expected: "api/v1", }, { - name: "trailing slash", - segments: []string{"api", "v1", "/"}, - expected: "/api/v1/", + name: "normalize double slashes with trailing slash", + paths: []string{"api/", "/v1/"}, + expected: "api/v1/", }, { - name: "trailing slashes", - segments: []string{"api", "v1", "///"}, - expected: "/api/v1/", + name: "empty string in middle paths", + paths: []string{"api", "", "v1"}, + expected: "api/v1", }, { - name: "trailing slash in the last part of the path", - segments: []string{"api", "v1/"}, - expected: "/api/v1/", + name: "empty string on last path", + paths: []string{"api", "v1", ""}, + expected: "api/v1", }, { - name: "trailing slashes in the last part of the path", - segments: []string{"api", "v1///"}, - expected: "/api/v1/", + name: "many path segments", + paths: []string{"api", "v1", "users", "123", "profile"}, + expected: "api/v1/users/123/profile", + }, + { + name: "many path segments with trailing slash", + paths: []string{"api", "v1", "users", "123", "profile/"}, + expected: "api/v1/users/123/profile/", + }, + { + name: "dot paths", + paths: []string{".", "api", "v1"}, + expected: "api/v1", + }, + { + name: "parent directory paths", + paths: []string{"api", "..", "v1"}, + expected: "v1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := util.JoinURL(tt.base, tt.segments...) + result := util.JoinPath(tt.paths...) assert.Equal(t, tt.expected, result) }) } } - -func TestJoinPath_EmptyInput(t *testing.T) { - got := util.JoinPath() - want := "" - if got != want { - t.Errorf("JoinPath() = %q, want %q", got, want) - } -} diff --git a/router.go b/router.go index fd0be9e..4342387 100644 --- a/router.go +++ b/router.go @@ -223,7 +223,11 @@ func (g *generator) Validate() error { func (g *generator) buildOnce() { g.once.Do(func() { for _, r := range g.build() { - g.reflector.Add(r.method, r.path, r.opts...) + path := r.path + if g.cfg.StripTrailingSlash && len(path) > 1 { + path = strings.TrimRight(path, "/") + } + g.reflector.Add(r.method, path, r.opts...) } }) } From 994760bf6bd0c27595625d22e4ce8f0dd5e2dbbe Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Fri, 10 Apr 2026 19:10:19 +0700 Subject: [PATCH 2/4] fix: propagate gen field to subrouters and fix nil panic in modifyReqStructure - Fix nil panic in operation.go modifyReqStructure when structure is nil and parameterTagMapping is configured - Propagate gen field to subrouters in fiberopenapi, fiberv3openapi, ginopenapi, muxopenapi, httprouteropenapi (calling Validate/GenerateSchema on a sub-router previously caused a nil pointer dereference) - Add gen field to muxopenapi route struct so Subrouter() can forward it - Fix httprouteropenapi Handler() passing unprefixed path to spec - Fix chiopenapi Group() passing "/" instead of "" as spec group prefix --- adapter/chiopenapi/router.go | 2 +- adapter/fiberopenapi/router.go | 2 ++ adapter/fiberv3openapi/router.go | 2 ++ adapter/ginopenapi/router.go | 1 + adapter/httprouteropenapi/router.go | 6 ++++-- adapter/muxopenapi/route.go | 2 ++ adapter/muxopenapi/router.go | 3 +++ operation.go | 3 +++ 8 files changed, 18 insertions(+), 3 deletions(-) diff --git a/adapter/chiopenapi/router.go b/adapter/chiopenapi/router.go index 99a8837..e912c39 100644 --- a/adapter/chiopenapi/router.go +++ b/adapter/chiopenapi/router.go @@ -86,7 +86,7 @@ func (r *router) Group(fn func(r Router), opts ...option.GroupOption) Router { r.chiRouter.Group(func(chiRouter chi.Router) { group = &router{ chiRouter: chiRouter, - specRouter: r.specRouter.Group("/", opts...), + specRouter: r.specRouter.Group("", opts...), gen: r.gen, } fn(group) diff --git a/adapter/fiberopenapi/router.go b/adapter/fiberopenapi/router.go index fb1de55..4ef7215 100644 --- a/adapter/fiberopenapi/router.go +++ b/adapter/fiberopenapi/router.go @@ -133,6 +133,7 @@ func (r *router) Group(prefix string, handlers ...fiber.Handler) Router { return &router{ fiberRouter: rr, specRouter: sr, + gen: r.gen, } } @@ -143,6 +144,7 @@ func (r *router) Route(prefix string, fn func(router Router), opts ...option.Gro subRouter := &router{ fiberRouter: fr, specRouter: sr, + gen: r.gen, } fn(subRouter) diff --git a/adapter/fiberv3openapi/router.go b/adapter/fiberv3openapi/router.go index 78a9f9b..c8dae78 100644 --- a/adapter/fiberv3openapi/router.go +++ b/adapter/fiberv3openapi/router.go @@ -144,6 +144,7 @@ func (r *router) Group(prefix string, handlers ...fiber.Handler) Router { return &router{ fiberRouter: fr, specRouter: sr, + gen: r.gen, } } @@ -154,6 +155,7 @@ func (r *router) Route(prefix string, fn func(router Router), opts ...option.Gro subRouter := &router{ fiberRouter: fr, specRouter: sr, + gen: r.gen, } fn(subRouter) diff --git a/adapter/ginopenapi/router.go b/adapter/ginopenapi/router.go index b7f0cdf..2283432 100644 --- a/adapter/ginopenapi/router.go +++ b/adapter/ginopenapi/router.go @@ -125,6 +125,7 @@ func (r *router) Group(prefix string, handlers ...gin.HandlerFunc) Router { return &router{ ginRouter: ginGroup, specRouter: specGroup, + gen: r.gen, } } diff --git a/adapter/httprouteropenapi/router.go b/adapter/httprouteropenapi/router.go index d2ce3eb..48306f1 100644 --- a/adapter/httprouteropenapi/router.go +++ b/adapter/httprouteropenapi/router.go @@ -103,10 +103,11 @@ func (r *router) Handler(method, path string, handler http.Handler) Route { handler = r.middlewares[i](handler) } } - r.router.Handler(method, r.pathOf(path), handler) + fullPath := r.pathOf(path) + r.router.Handler(method, fullPath, handler) rr := &route{} if method != http.MethodConnect { - rr.specRoute = r.specRouter.Add(method, path) + rr.specRoute = r.specRouter.Add(method, fullPath) } return rr @@ -162,6 +163,7 @@ func (r *router) Group(prefix string, middlewares ...func(http.Handler) http.Han middlewares: append(r.middlewares, middlewares...), specRouter: r.specRouter.Group(""), prefix: r.pathOf(prefix), + gen: r.gen, } return group } diff --git a/adapter/muxopenapi/route.go b/adapter/muxopenapi/route.go index f847072..582b3a5 100644 --- a/adapter/muxopenapi/route.go +++ b/adapter/muxopenapi/route.go @@ -12,6 +12,7 @@ type route struct { muxRoute *mux.Route specRoute spec.Route specRouter spec.Router + gen spec.Generator pathPrefix string } @@ -93,6 +94,7 @@ func (r *route) Subrouter(opts ...option.GroupOption) Router { return &router{ muxRouter: r.muxRoute.Subrouter(), specRouter: r.specRouter.Group(r.pathPrefix, opts...), + gen: r.gen, } } diff --git a/adapter/muxopenapi/router.go b/adapter/muxopenapi/router.go index 5772217..e03a93a 100644 --- a/adapter/muxopenapi/router.go +++ b/adapter/muxopenapi/router.go @@ -67,6 +67,7 @@ func (r *router) Get(name string) Route { return &route{ muxRoute: muxRoute, specRouter: r.specRouter, + gen: r.gen, } } @@ -76,6 +77,7 @@ func (r *router) GetRoute(name string) Route { return &route{ muxRoute: muxRoute, specRouter: r.specRouter, + gen: r.gen, } } @@ -108,6 +110,7 @@ func (r *router) NewRoute() Route { muxRoute: r.muxRouter.NewRoute(), specRoute: r.specRouter.NewRoute(), specRouter: r.specRouter, + gen: r.gen, } } diff --git a/operation.go b/operation.go index cea5051..588a90e 100644 --- a/operation.go +++ b/operation.go @@ -228,6 +228,9 @@ func (oc *operationContextImpl) modifyReqStructure(structure any) any { } t := reflect.TypeOf(structure) + if t == nil { + return structure + } if t.Kind() == reflect.Ptr { t = t.Elem() } From 1b8373faf4958729a9171379c10cd24ba1c9ef3b Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Fri, 10 Apr 2026 19:21:14 +0700 Subject: [PATCH 3/4] chore: update golangci-lint version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d3797cf..b354874 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ BLUE := \033[0;34m NC := \033[0m # No Color # Tool versions -GOLANGCI_LINT_VERSION := v2.3.1 +GOLANGCI_LINT_VERSION := v2.11.4 GOTESTSUM_VERSION := v1.12.3 # Normalize VERSION input so targets accept both 1.2.3 and v1.2.3. From 0234206b62bb652e4e8eec6bbe4f5cdee0b69100 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Sun, 12 Apr 2026 17:56:42 +0700 Subject: [PATCH 4/4] feat: add some unit test and map encoding function to mapper package --- internal/mapper/openapi3.go | 11 +++++ internal/mapper/openapi31.go | 11 +++++ internal/mapper/openapi_test.go | 64 +++++++++++++++++++++++++ operation.go | 27 ++--------- option/openapi_test.go | 28 +++++++++++ router_test.go | 24 ++++++++++ testdata/strip_trailing_slashes_3.yaml | 56 ++++++++++++++++++++++ testdata/strip_trailing_slashes_31.yaml | 59 +++++++++++++++++++++++ 8 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 testdata/strip_trailing_slashes_3.yaml create mode 100644 testdata/strip_trailing_slashes_31.yaml diff --git a/internal/mapper/openapi3.go b/internal/mapper/openapi3.go index 2cf897a..d3c0740 100644 --- a/internal/mapper/openapi3.go +++ b/internal/mapper/openapi3.go @@ -225,3 +225,14 @@ func OAS3OauthFlowsAuthorizationCode(flows *openapi.OAuthFlowsAuthorizationCode) MapOfAnything: flows.MapOfAnything, } } + +func StringMapToEncodingMap3(enc map[string]string) map[string]openapi3.Encoding { + res := map[string]openapi3.Encoding{} + for k, v := range enc { + rv := v + res[k] = openapi3.Encoding{ + ContentType: &rv, + } + } + return res +} diff --git a/internal/mapper/openapi31.go b/internal/mapper/openapi31.go index 2a3ec09..7a7f595 100644 --- a/internal/mapper/openapi31.go +++ b/internal/mapper/openapi31.go @@ -220,3 +220,14 @@ func OAS31OauthFlowsAuthorizationCode( MapOfAnything: flows.MapOfAnything, } } + +func StringMapToEncodingMap31(enc map[string]string) map[string]openapi31.Encoding { + res := map[string]openapi31.Encoding{} + for k, v := range enc { + rv := v + res[k] = openapi31.Encoding{ + ContentType: &rv, + } + } + return res +} diff --git a/internal/mapper/openapi_test.go b/internal/mapper/openapi_test.go index c728a0d..c3317c2 100644 --- a/internal/mapper/openapi_test.go +++ b/internal/mapper/openapi_test.go @@ -1118,3 +1118,67 @@ func TestOASOauth2Flows(t *testing.T) { }) } } + +func TestStringMapToEncodingMap(t *testing.T) { + tests := []struct { + name string + input map[string]string + expected3 map[string]openapi3.Encoding + expected31 map[string]openapi31.Encoding + }{ + { + name: "empty map", + input: map[string]string{}, + expected3: map[string]openapi3.Encoding{}, + expected31: map[string]openapi31.Encoding{}, + }, + { + name: "single encoding", + input: map[string]string{ + "field1": "application/json", + }, + expected3: map[string]openapi3.Encoding{ + "field1": { + ContentType: util.PtrOf("application/json"), + }, + }, + expected31: map[string]openapi31.Encoding{ + "field1": { + ContentType: util.PtrOf("application/json"), + }, + }, + }, + { + name: "multiple encodings", + input: map[string]string{ + "field1": "application/json", + "field2": "text/plain", + }, + expected3: map[string]openapi3.Encoding{ + "field1": { + ContentType: util.PtrOf("application/json"), + }, + "field2": { + ContentType: util.PtrOf("text/plain"), + }, + }, + expected31: map[string]openapi31.Encoding{ + "field1": { + ContentType: util.PtrOf("application/json"), + }, + "field2": { + ContentType: util.PtrOf("text/plain"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result3 := mapper.StringMapToEncodingMap3(tt.input) + assert.Equal(t, tt.expected3, result3, "String map to OpenAPI 3 Encoding map conversion failed") + result31 := mapper.StringMapToEncodingMap31(tt.input) + assert.Equal(t, tt.expected31, result31, "String map to OpenAPI 3.1 Encoding map conversion failed") + }) + } +} diff --git a/operation.go b/operation.go index 588a90e..07b8a74 100644 --- a/operation.go +++ b/operation.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/oaswrap/spec/internal/debuglog" + "github.com/oaswrap/spec/internal/mapper" specopenapi "github.com/oaswrap/spec/openapi" "github.com/oaswrap/spec/option" "github.com/swaggest/jsonschema-go" @@ -140,28 +141,6 @@ func mergeResponses(responses []*specopenapi.ContentUnit) []*specopenapi.Content return result } -func stringMapToEncodingMap3(enc map[string]string) map[string]openapi3.Encoding { - res := map[string]openapi3.Encoding{} - for k, v := range enc { - rv := v - res[k] = openapi3.Encoding{ - ContentType: &rv, - } - } - return res -} - -func stringMapToEncodingMap31(enc map[string]string) map[string]openapi31.Encoding { - res := map[string]openapi31.Encoding{} - for k, v := range enc { - rv := v - res[k] = openapi31.Encoding{ - ContentType: &rv, - } - } - return res -} - func (oc *operationContextImpl) buildRequestOpts(req *specopenapi.ContentUnit) ([]openapi.ContentOption, string) { log := fmt.Sprintf("%T", req.Structure) var opts []openapi.ContentOption @@ -181,13 +160,13 @@ func (oc *operationContextImpl) buildRequestOpts(req *specopenapi.ContentUnit) ( case *openapi3.RequestBodyOrRef: content := map[string]openapi3.MediaType{} for k, val := range v.RequestBody.Content { - content[k] = *val.WithEncoding(stringMapToEncodingMap3(req.Encoding)) + content[k] = *val.WithEncoding(mapper.StringMapToEncodingMap3(req.Encoding)) } v.RequestBody.WithContent(content) case *openapi31.RequestBodyOrReference: content := map[string]openapi31.MediaType{} for k, val := range v.RequestBody.Content { - content[k] = *val.WithEncoding(stringMapToEncodingMap31(req.Encoding)) + content[k] = *val.WithEncoding(mapper.StringMapToEncodingMap31(req.Encoding)) } v.RequestBody.WithContent(content) } diff --git a/option/openapi_test.go b/option/openapi_test.go index 04ffa51..f75b18c 100644 --- a/option/openapi_test.go +++ b/option/openapi_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/oaswrap/spec-ui/config" + "github.com/oaswrap/spec-ui/swaggeruiemb" "github.com/oaswrap/spec/openapi" "github.com/oaswrap/spec/option" "github.com/oaswrap/spec/pkg/util" @@ -800,6 +801,33 @@ func (m *mockPathParser) Parse(path string) (string, error) { return path, nil } +func TestOpenAPIWithUIOption(t *testing.T) { + config := &openapi.Config{} + opt := option.WithUIOption( + swaggeruiemb.WithUI(), + ) + opt(config) + + assert.NotNil(t, config.UIOption) + assert.Nil(t, config.SwaggerUIConfig) +} + +func TestOpnenAPIWithStripTrailingSlash(t *testing.T) { + config := &openapi.Config{} + opt := option.WithStripTrailingSlash(true) + opt(config) + + assert.True(t, config.StripTrailingSlash) +} + +func TestOpenAPIWithSpecPath(t *testing.T) { + config := &openapi.Config{} + opt := option.WithSpecPath("/openapi.yaml") + opt(config) + + assert.Equal(t, "/openapi.yaml", config.SpecPath) +} + func TestOpenAPIConfigDefaults(t *testing.T) { config := &openapi.Config{} diff --git a/router_test.go b/router_test.go index 191be70..418d1d7 100644 --- a/router_test.go +++ b/router_test.go @@ -625,6 +625,30 @@ func TestRouter(t *testing.T) { ) }, }, + { + name: "Strip Trailing Slashes", + golden: "strip_trailing_slashes", + opts: []option.OpenAPIOption{ + option.WithStripTrailingSlash(), + }, + setup: func(r spec.Router) { + r.Get("/path/with/trailing/slash/", + option.OperationID("getPathWithTrailingSlash"), + option.Summary("Get Path With Trailing Slash"), + option.Description("This operation tests paths with trailing slashes."), + option.Response(200, new(User)), + ) + r.Route("/api/v1", func(r spec.Router) { + r.Route("/users", func(r spec.Router) { + r.Get("/", + option.Summary("Get Users"), + option.Response(200, new([]User)), + option.Tags("Users"), + ) + }) + }) + }, + }, { name: "Server Variables", golden: "server_variables", diff --git a/testdata/strip_trailing_slashes_3.yaml b/testdata/strip_trailing_slashes_3.yaml new file mode 100644 index 0000000..2dfdc5b --- /dev/null +++ b/testdata/strip_trailing_slashes_3.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.3 +info: + description: This is the API documentation for Strip Trailing Slashes + title: 'API Doc: Strip Trailing Slashes' + version: 1.0.0 +paths: + /api/v1/users: + get: + description: Get Users + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/SpecTestUser' + type: array + description: OK + summary: Get Users + tags: + - Users + /path/with/trailing/slash: + get: + description: This operation tests paths with trailing slashes. + operationId: getPathWithTrailingSlash + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestUser' + description: OK + summary: Get Path With Trailing Slash +components: + schemas: + SpecTestNullString: + type: object + SpecTestNullTime: + type: object + SpecTestUser: + properties: + age: + nullable: true + type: integer + created_at: + format: date-time + type: string + email: + $ref: '#/components/schemas/SpecTestNullString' + id: + type: integer + updated_at: + $ref: '#/components/schemas/SpecTestNullTime' + username: + type: string + type: object diff --git a/testdata/strip_trailing_slashes_31.yaml b/testdata/strip_trailing_slashes_31.yaml new file mode 100644 index 0000000..96724da --- /dev/null +++ b/testdata/strip_trailing_slashes_31.yaml @@ -0,0 +1,59 @@ +openapi: 3.1.0 +info: + description: This is the API documentation for Strip Trailing Slashes + title: 'API Doc: Strip Trailing Slashes' + version: 1.0.0 +paths: + /api/v1/users: + get: + description: Get Users + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/SpecTestUser' + type: + - "null" + - array + description: OK + summary: Get Users + tags: + - Users + /path/with/trailing/slash: + get: + description: This operation tests paths with trailing slashes. + operationId: getPathWithTrailingSlash + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestUser' + description: OK + summary: Get Path With Trailing Slash +components: + schemas: + SpecTestNullString: + type: object + SpecTestNullTime: + type: object + SpecTestUser: + properties: + age: + type: + - "null" + - integer + created_at: + format: date-time + type: string + email: + $ref: '#/components/schemas/SpecTestNullString' + id: + type: integer + updated_at: + $ref: '#/components/schemas/SpecTestNullTime' + username: + type: string + type: object