diff --git a/apirouter/router.go b/apirouter/router.go index d00f886..6b09ddf 100644 --- a/apirouter/router.go +++ b/apirouter/router.go @@ -3,4 +3,5 @@ package apirouter type Router[HandlerFunc any, Route any] interface { AddRoute(method string, path string, handler HandlerFunc) Route SwaggerHandler(contentType string, blob []byte) HandlerFunc + TransformPathToOasPath(path string) string } diff --git a/apirouter/transformpath.go b/apirouter/transformpath.go new file mode 100644 index 0000000..56efb78 --- /dev/null +++ b/apirouter/transformpath.go @@ -0,0 +1,15 @@ +package apirouter + +import ( + "strings" +) + +func TransformPathParamsWithColon(path string) string { + pathParams := strings.Split(path, "/") + for i, param := range pathParams { + if strings.HasPrefix(param, ":") { + pathParams[i] = strings.Replace(param, ":", "{", 1) + "}" + } + } + return strings.Join(pathParams, "/") +} diff --git a/apirouter/transformpath_test.go b/apirouter/transformpath_test.go new file mode 100644 index 0000000..cc636be --- /dev/null +++ b/apirouter/transformpath_test.go @@ -0,0 +1,59 @@ +package apirouter + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTransformPathParamsWithColon(t *testing.T) { + testCases := []struct { + name string + path string + expectedPath string + }{ + { + name: "only /", + path: "/", + expectedPath: "/", + }, + { + name: "without params", + path: "/foo", + expectedPath: "/foo", + }, + { + name: "without params ending with /", + path: "/foo/", + expectedPath: "/foo/", + }, + { + name: "with params", + path: "/foo/:par1", + expectedPath: "/foo/{par1}", + }, + { + name: "with params ending with /", + path: "/foo/:par1/", + expectedPath: "/foo/{par1}/", + }, + { + name: "with multiple params", + path: "/:par1/:par2/:par3", + expectedPath: "/{par1}/{par2}/{par3}", + }, + { + name: "with multiple params ending with /", + path: "/:par1/:par2/:par3/", + expectedPath: "/{par1}/{par2}/{par3}/", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + actual := TransformPathParamsWithColon(test.path) + + require.Equal(t, test.expectedPath, actual) + }) + } +} diff --git a/main_test.go b/main_test.go index 4d57f86..d0336f5 100644 --- a/main_test.go +++ b/main_test.go @@ -336,7 +336,7 @@ func TestGenerateAndExposeSwagger(t *testing.T) { w.Write([]byte("ok")) }, Definitions{}) - mSubRouter := mRouter.PathPrefix("/prefix").Subrouter() + mSubRouter := mRouter.NewRoute().Subrouter() subrouter, err := router.SubRouter(gorilla.NewRouter(mSubRouter), SubRouterOptions{ PathPrefix: "/prefix", }) @@ -370,6 +370,30 @@ func TestGenerateAndExposeSwagger(t *testing.T) { actual, err := os.ReadFile("testdata/subrouter.json") require.NoError(t, err) require.JSONEq(t, string(actual), body) + + t.Run("test request /prefix", func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/prefix", nil) + mRouter.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + }) + + t.Run("test request /prefix/taz", func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/prefix/taz", nil) + mRouter.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + }) + + t.Run("test request /foo", func(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/foo", nil) + mRouter.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + }) }) t.Run("ok - new router with path prefix", func(t *testing.T) { @@ -405,7 +429,7 @@ func TestGenerateAndExposeSwagger(t *testing.T) { body := readBody(t, w.Result().Body) actual, err := os.ReadFile("testdata/router_with_prefix.json") require.NoError(t, err) - require.JSONEq(t, string(actual), body) + require.JSONEq(t, string(actual), body, body) }) } diff --git a/route.go b/route.go index 9e5752f..b9b076d 100644 --- a/route.go +++ b/route.go @@ -38,7 +38,8 @@ func (r Router[HandlerFunc, Route]) AddRawRoute(method string, routePath string, } } pathWithPrefix := path.Join(r.pathPrefix, routePath) - r.swaggerSchema.AddOperation(pathWithPrefix, method, op) + oasPath := r.router.TransformPathToOasPath(pathWithPrefix) + r.swaggerSchema.AddOperation(oasPath, method, op) // Handle, when content-type is json, the request/response marshalling? Maybe with a specific option. return r.router.AddRoute(method, pathWithPrefix, handler), nil @@ -104,7 +105,8 @@ func (r Router[HandlerFunc, Route]) AddRoute(method string, path string, handler return getZero[Route](), fmt.Errorf("%w: %s", ErrResponses, err) } - err = r.resolveParameterSchema(pathParamsType, getPathParamsAutofilled(schema, path), operation) + oasPath := r.router.TransformPathToOasPath(path) + err = r.resolveParameterSchema(pathParamsType, getPathParamsAutoComplete(schema, oasPath), operation) if err != nil { return getZero[Route](), fmt.Errorf("%w: %s", ErrPathParams, err) } @@ -260,7 +262,7 @@ func (r Router[_, _]) addContentToOASSchema(content Content) (openapi3.Content, return oasContent, nil } -func getPathParamsAutofilled(schema Definitions, path string) ParameterValue { +func getPathParamsAutoComplete(schema Definitions, path string) ParameterValue { if schema.PathParams == nil { pathParams := strings.Split(path, "/") for _, param := range pathParams { diff --git a/support/echo/echo.go b/support/echo/echo.go index 282eaf3..d9acd49 100644 --- a/support/echo/echo.go +++ b/support/echo/echo.go @@ -25,6 +25,10 @@ func (r echoRouter) SwaggerHandler(contentType string, blob []byte) echo.Handler } } +func (r echoRouter) TransformPathToOasPath(path string) string { + return apirouter.TransformPathParamsWithColon(path) +} + func NewRouter(router *echo.Echo) apirouter.Router[echo.HandlerFunc, Route] { return echoRouter{ router: router, diff --git a/support/echo/integration_test.go b/support/echo/integration_test.go index 887992a..3acf191 100644 --- a/support/echo/integration_test.go +++ b/support/echo/integration_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "os" "testing" oasEcho "github.com/davidebianchi/gswagger/support/echo" @@ -22,19 +23,36 @@ const ( type echoSwaggerRouter = swagger.Router[echo.HandlerFunc, *echo.Route] -func TestIntegration(t *testing.T) { +func TestEchoIntegration(t *testing.T) { t.Run("router works correctly - echo", func(t *testing.T) { - echoRouter, _ := setupEchoSwagger(t) + echoRouter, oasRouter := setupEchoSwagger(t) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/hello", nil) + err := oasRouter.GenerateAndExposeOpenapi() + require.NoError(t, err) - echoRouter.ServeHTTP(w, r) + t.Run("/hello", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/hello", nil) - require.Equal(t, http.StatusOK, w.Result().StatusCode) + echoRouter.ServeHTTP(w, r) - body := readBody(t, w.Result().Body) - require.Equal(t, "OK", body) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + body := readBody(t, w.Result().Body) + require.Equal(t, "OK", body) + }) + + t.Run("/hello/:value", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/hello/something", nil) + + echoRouter.ServeHTTP(w, r) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + body := readBody(t, w.Result().Body) + require.Equal(t, "OK", body) + }) t.Run("and generate swagger", func(t *testing.T) { w := httptest.NewRecorder() @@ -45,14 +63,14 @@ func TestIntegration(t *testing.T) { require.Equal(t, http.StatusOK, w.Result().StatusCode) body := readBody(t, w.Result().Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/integration.json"), body) }) }) t.Run("works correctly with subrouter - handles path prefix - echo", func(t *testing.T) { - eRouter, swaggerRouter := setupEchoSwagger(t) + eRouter, oasRouter := setupEchoSwagger(t) - subRouter, err := swaggerRouter.SubRouter(oasEcho.NewRouter(eRouter), swagger.SubRouterOptions{ + subRouter, err := oasRouter.SubRouter(oasEcho.NewRouter(eRouter), swagger.SubRouterOptions{ PathPrefix: "/prefix", }) require.NoError(t, err) @@ -60,15 +78,20 @@ func TestIntegration(t *testing.T) { _, err = subRouter.AddRoute(http.MethodGet, "/foo", okHandler, swagger.Definitions{}) require.NoError(t, err) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/hello", nil) + err = oasRouter.GenerateAndExposeOpenapi() + require.NoError(t, err) - eRouter.ServeHTTP(w, r) + t.Run("correctly call /hello", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/hello", nil) - require.Equal(t, http.StatusOK, w.Result().StatusCode) + eRouter.ServeHTTP(w, r) - body := readBody(t, w.Result().Body) - require.Equal(t, "OK", body) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + body := readBody(t, w.Result().Body) + require.Equal(t, "OK", body) + }) t.Run("correctly call sub router", func(t *testing.T) { w := httptest.NewRecorder() @@ -91,7 +114,7 @@ func TestIntegration(t *testing.T) { require.Equal(t, http.StatusOK, w.Result().StatusCode) body := readBody(t, w.Result().Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/intergation-subrouter.json"), body, body) }) }) } @@ -127,7 +150,7 @@ func setupEchoSwagger(t *testing.T) (*echo.Echo, *echoSwaggerRouter) { _, err = router.AddRawRoute(http.MethodGet, "/hello", okHandler, operation) require.NoError(t, err) - err = router.GenerateAndExposeOpenapi() + _, err = router.AddRoute(http.MethodPost, "/hello/:value", okHandler, swagger.Definitions{}) require.NoError(t, err) return e, router @@ -136,3 +159,12 @@ func setupEchoSwagger(t *testing.T) (*echo.Echo, *echoSwaggerRouter) { func okHandler(c echo.Context) error { return c.String(http.StatusOK, "OK") } + +func readFile(t *testing.T, path string) string { + t.Helper() + + fileContent, err := os.ReadFile(path) + require.NoError(t, err) + + return string(fileContent) +} diff --git a/support/fiber/fiber.go b/support/fiber/fiber.go index adb4337..05670c7 100644 --- a/support/fiber/fiber.go +++ b/support/fiber/fiber.go @@ -28,3 +28,7 @@ func (r fiberRouter) SwaggerHandler(contentType string, blob []byte) HandlerFunc return c.Send(blob) } } + +func (r fiberRouter) TransformPathToOasPath(path string) string { + return apirouter.TransformPathParamsWithColon(path) +} diff --git a/support/fiber/integration_test.go b/support/fiber/integration_test.go index 96b907d..4f06d76 100644 --- a/support/fiber/integration_test.go +++ b/support/fiber/integration_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "os" "testing" swagger "github.com/davidebianchi/gswagger" @@ -22,18 +23,34 @@ const ( swaggerOpenapiVersion = "test swagger version" ) -func TestWithFiber(t *testing.T) { +func TestFiberIntegration(t *testing.T) { t.Run("router works correctly", func(t *testing.T) { - router, _ := setupSwagger(t) + router, oasRouter := setupSwagger(t) - r := httptest.NewRequest(http.MethodGet, "/hello", nil) - - resp, err := router.Test(r) + err := oasRouter.GenerateAndExposeOpenapi() require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - body := readBody(t, resp.Body) - require.Equal(t, "OK", body) + t.Run("/hello", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/hello", nil) + + resp, err := router.Test(r) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + body := readBody(t, resp.Body) + require.Equal(t, "OK", body) + }) + + t.Run("/hello/:value", func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/hello/something", nil) + + resp, err := router.Test(r) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + body := readBody(t, resp.Body) + require.Equal(t, "OK", body) + }) t.Run("and generate swagger", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, swagger.DefaultJSONDocumentationPath, nil) @@ -43,36 +60,26 @@ func TestWithFiber(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) body := readBody(t, resp.Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/integration.json"), body, body) }) }) t.Run("works correctly with subrouter - handles path prefix - gorilla mux", func(t *testing.T) { fiberRouter, oasRouter := setupSwagger(t) - fiberRouter.Route("/foo", func(router fiber.Router) { - subRouter, err := oasRouter.SubRouter(oasFiber.NewRouter(router), swagger.SubRouterOptions{ - PathPrefix: "/prefix", - }) - require.NoError(t, err) - - _, err = subRouter.AddRoute(http.MethodGet, "/nested", okHandler, swagger.Definitions{}) - require.NoError(t, err) + subRouter, err := oasRouter.SubRouter(oasFiber.NewRouter(fiberRouter), swagger.SubRouterOptions{ + PathPrefix: "/prefix", }) - - oasRouter.AddRoute(http.MethodGet, "/foo", okHandler, swagger.Definitions{}) - - r := httptest.NewRequest(http.MethodGet, "/hello", nil) - - resp, err := fiberRouter.Test(r) require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - body := readBody(t, resp.Body) - require.Equal(t, "OK", body) + _, err = subRouter.AddRoute(http.MethodGet, "/foo", okHandler, swagger.Definitions{}) + require.NoError(t, err) - t.Run("correctly call router", func(t *testing.T) { - r := httptest.NewRequest(http.MethodGet, "/foo", nil) + err = oasRouter.GenerateAndExposeOpenapi() + require.NoError(t, err) + + t.Run("correctly call /hello", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/hello", nil) resp, err := fiberRouter.Test(r) require.NoError(t, err) @@ -83,7 +90,7 @@ func TestWithFiber(t *testing.T) { }) t.Run("correctly call sub router", func(t *testing.T) { - r := httptest.NewRequest(http.MethodGet, "/foo/prefix/nested", nil) + r := httptest.NewRequest(http.MethodGet, "/prefix/foo", nil) resp, err := fiberRouter.Test(r) require.NoError(t, err) @@ -101,7 +108,7 @@ func TestWithFiber(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) body := readBody(t, resp.Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/intergation-subrouter.json"), body, body) }) }) } @@ -128,7 +135,7 @@ func setupSwagger(t *testing.T) (*fiber.App, *SwaggerRouter) { _, err = router.AddRawRoute(http.MethodGet, "/hello", okHandler, operation) require.NoError(t, err) - err = router.GenerateAndExposeOpenapi() + _, err = router.AddRoute(http.MethodPost, "/hello/:value", okHandler, swagger.Definitions{}) require.NoError(t, err) return fiberRouter, router @@ -147,3 +154,12 @@ func readBody(t *testing.T, requestBody io.ReadCloser) string { return string(body) } + +func readFile(t *testing.T, path string) string { + t.Helper() + + fileContent, err := os.ReadFile(path) + require.NoError(t, err) + + return string(fileContent) +} diff --git a/support/gorilla/gorilla.go b/support/gorilla/gorilla.go index c51a6b5..a830e5f 100644 --- a/support/gorilla/gorilla.go +++ b/support/gorilla/gorilla.go @@ -28,6 +28,10 @@ func (r gorillaRouter) SwaggerHandler(contentType string, blob []byte) HandlerFu } } +func (r gorillaRouter) TransformPathToOasPath(path string) string { + return path +} + func NewRouter(router *mux.Router) apirouter.Router[HandlerFunc, Route] { return gorillaRouter{ router: router, diff --git a/support/gorilla/integration_test.go b/support/gorilla/integration_test.go index 28bba24..e952846 100644 --- a/support/gorilla/integration_test.go +++ b/support/gorilla/integration_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "os" "testing" swagger "github.com/davidebianchi/gswagger" @@ -23,7 +24,10 @@ type SwaggerRouter = swagger.Router[gorilla.HandlerFunc, gorilla.Route] func TestGorillaIntegration(t *testing.T) { t.Run("router works correctly", func(t *testing.T) { - muxRouter, _ := setupSwagger(t) + muxRouter, oasRouter := setupSwagger(t) + + err := oasRouter.GenerateAndExposeOpenapi() + require.NoError(t, err) w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/hello", nil) @@ -44,15 +48,15 @@ func TestGorillaIntegration(t *testing.T) { require.Equal(t, http.StatusOK, w.Result().StatusCode) body := readBody(t, w.Result().Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/integration.json"), body) }) }) t.Run("works correctly with subrouter - handles path prefix - gorilla mux", func(t *testing.T) { - muxRouter, swaggerRouter := setupSwagger(t) + muxRouter, oasRouter := setupSwagger(t) muxSubRouter := muxRouter.NewRoute().Subrouter() - subRouter, err := swaggerRouter.SubRouter(gorilla.NewRouter(muxSubRouter), swagger.SubRouterOptions{ + subRouter, err := oasRouter.SubRouter(gorilla.NewRouter(muxSubRouter), swagger.SubRouterOptions{ PathPrefix: "/prefix", }) require.NoError(t, err) @@ -60,15 +64,20 @@ func TestGorillaIntegration(t *testing.T) { _, err = subRouter.AddRoute(http.MethodGet, "/foo", okHandler, swagger.Definitions{}) require.NoError(t, err) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/hello", nil) + err = oasRouter.GenerateAndExposeOpenapi() + require.NoError(t, err) - muxRouter.ServeHTTP(w, r) + t.Run("correctly call router", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/hello", nil) - require.Equal(t, http.StatusOK, w.Result().StatusCode) + muxRouter.ServeHTTP(w, r) - body := readBody(t, w.Result().Body) - require.Equal(t, "OK", body) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + body := readBody(t, w.Result().Body) + require.Equal(t, "OK", body) + }) t.Run("correctly call sub router", func(t *testing.T) { w := httptest.NewRecorder() @@ -91,7 +100,7 @@ func TestGorillaIntegration(t *testing.T) { require.Equal(t, http.StatusOK, w.Result().StatusCode) body := readBody(t, w.Result().Body) - require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body) + require.JSONEq(t, readFile(t, "../testdata/intergation-subrouter.json"), body, body) }) }) } @@ -127,7 +136,7 @@ func setupSwagger(t *testing.T) (*mux.Router, *SwaggerRouter) { _, err = router.AddRawRoute(http.MethodGet, "/hello", okHandler, operation) require.NoError(t, err) - err = router.GenerateAndExposeOpenapi() + _, err = router.AddRoute(http.MethodPost, "/hello/{value}", okHandler, swagger.Definitions{}) require.NoError(t, err) return muxRouter, router @@ -137,3 +146,12 @@ func okHandler(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`OK`)) } + +func readFile(t *testing.T, path string) string { + t.Helper() + + fileContent, err := os.ReadFile(path) + require.NoError(t, err) + + return string(fileContent) +} diff --git a/support/testdata/integration.json b/support/testdata/integration.json new file mode 100644 index 0000000..73b962a --- /dev/null +++ b/support/testdata/integration.json @@ -0,0 +1,38 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": { + "/hello": { + "get": { + "responses": { + "default": { + "description": "" + } + } + } + }, + "/hello/{value}": { + "post": { + "parameters": [ + { + "in": "path", + "name": "value", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + } + } +} diff --git a/support/testdata/intergation-subrouter.json b/support/testdata/intergation-subrouter.json new file mode 100644 index 0000000..1c13906 --- /dev/null +++ b/support/testdata/intergation-subrouter.json @@ -0,0 +1,47 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": { + "/hello": { + "get": { + "responses": { + "default": { + "description": "" + } + } + } + }, + "/hello/{value}": { + "post": { + "parameters": [ + { + "in": "path", + "name": "value", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/prefix/foo": { + "get": { + "responses": { + "default": { + "description": "" + } + } + } + } + } +}