diff --git a/module/openapi.go b/module/openapi.go index e91cfbae..73c8fb81 100644 --- a/module/openapi.go +++ b/module/openapi.go @@ -35,12 +35,13 @@ type OpenAPISwaggerUIConfig struct { // OpenAPIConfig holds the full configuration for an OpenAPI module. type OpenAPIConfig struct { - SpecFile string `yaml:"spec_file" json:"spec_file"` - BasePath string `yaml:"base_path" json:"base_path"` - Validation OpenAPIValidationConfig `yaml:"validation" json:"validation"` - SwaggerUI OpenAPISwaggerUIConfig `yaml:"swagger_ui" json:"swagger_ui"` - RouterName string `yaml:"router" json:"router"` // optional: explicit router to attach to - MaxBodyBytes int64 `yaml:"max_body_bytes" json:"max_body_bytes"` // max request body size (bytes); 0 = use default + SpecFile string `yaml:"spec_file" json:"spec_file"` + BasePath string `yaml:"base_path" json:"base_path"` + Validation OpenAPIValidationConfig `yaml:"validation" json:"validation"` + SwaggerUI OpenAPISwaggerUIConfig `yaml:"swagger_ui" json:"swagger_ui"` + RouterName string `yaml:"router" json:"router"` // optional: explicit router to attach to + MaxBodyBytes int64 `yaml:"max_body_bytes" json:"max_body_bytes"` // max request body size (bytes); 0 = use default + RegisterRoutes *bool `yaml:"register_routes" json:"register_routes"` // when false, skip spec-path route registration; default true } // defaultMaxBodyBytes is the default request body size limit (1 MiB) applied @@ -217,22 +218,24 @@ func (m *OpenAPIModule) RegisterRoutes(router HTTPRouter) { basePath := strings.TrimRight(m.cfg.BasePath, "/") - // Register a route for each path+method in the spec - for specPath, pathItem := range m.spec.Paths { - for method, op := range pathItem { - httpMethod := strings.ToUpper(method) - if !isValidHTTPMethod(httpMethod) { - continue + // Register a route for each path+method in the spec, unless register_routes is explicitly false. + if m.cfg.RegisterRoutes == nil || *m.cfg.RegisterRoutes { + for specPath, pathItem := range m.spec.Paths { + for method, op := range pathItem { + httpMethod := strings.ToUpper(method) + if !isValidHTTPMethod(httpMethod) { + continue + } + routePath := basePath + openAPIPathToHTTPPath(specPath) + handler := m.buildRouteHandler(specPath, httpMethod, op) + router.AddRoute(httpMethod, routePath, handler) + m.logger.Debug("OpenAPI route registered", + "module", m.name, + "method", httpMethod, + "path", routePath, + "operationId", op.OperationID, + ) } - routePath := basePath + openAPIPathToHTTPPath(specPath) - handler := m.buildRouteHandler(specPath, httpMethod, op) - router.AddRoute(httpMethod, routePath, handler) - m.logger.Debug("OpenAPI route registered", - "module", m.name, - "method", httpMethod, - "path", routePath, - "operationId", op.OperationID, - ) } } diff --git a/module/openapi_test.go b/module/openapi_test.go index 87d889b5..ec9d9eb3 100644 --- a/module/openapi_test.go +++ b/module/openapi_test.go @@ -271,6 +271,82 @@ func TestOpenAPIModule_RegisterRoutes(t *testing.T) { } } +func TestOpenAPIModule_RegisterRoutesFalse(t *testing.T) { + specPath := writeTempSpec(t, ".yaml", petstoreYAML) + + falseVal := false + mod := NewOpenAPIModule("petstore", OpenAPIConfig{ + SpecFile: specPath, + BasePath: "/api/v1", + RegisterRoutes: &falseVal, + SwaggerUI: OpenAPISwaggerUIConfig{ + Enabled: true, + Path: "/docs", + }, + }) + if err := mod.Init(nil); err != nil { + t.Fatalf("Init: %v", err) + } + + router := &testRouter{} + mod.RegisterRoutes(router) + + paths := make(map[string]bool) + for _, rt := range router.routes { + paths[rt.method+":"+rt.path] = true + } + + // Spec endpoints and Swagger UI should still be registered + if !paths["GET:/api/v1/openapi.json"] { + t.Error("expected GET:/api/v1/openapi.json to be registered even when register_routes=false") + } + if !paths["GET:/api/v1/openapi.yaml"] { + t.Error("expected GET:/api/v1/openapi.yaml to be registered even when register_routes=false") + } + if !paths["GET:/api/v1/docs"] { + t.Error("expected GET:/api/v1/docs (Swagger UI) to be registered even when register_routes=false") + } + + // Spec-path routes must NOT be registered + specRoutes := []string{ + "GET:/api/v1/pets", + "POST:/api/v1/pets", + "GET:/api/v1/pets/{petId}", + } + for _, route := range specRoutes { + if paths[route] { + t.Errorf("expected spec route %q NOT to be registered when register_routes=false", route) + } + } +} + +func TestOpenAPIModule_RegisterRoutesNilDefaultsTrue(t *testing.T) { + specPath := writeTempSpec(t, ".yaml", petstoreYAML) + + // When RegisterRoutes is nil (not set), spec-path routes should be registered (default true). + mod := NewOpenAPIModule("petstore", OpenAPIConfig{ + SpecFile: specPath, + BasePath: "/api/v1", + }) + if err := mod.Init(nil); err != nil { + t.Fatalf("Init: %v", err) + } + + router := &testRouter{} + mod.RegisterRoutes(router) + + paths := make(map[string]bool) + for _, rt := range router.routes { + paths[rt.method+":"+rt.path] = true + } + + for _, route := range []string{"GET:/api/v1/pets", "POST:/api/v1/pets", "GET:/api/v1/pets/{petId}"} { + if !paths[route] { + t.Errorf("expected spec route %q to be registered when register_routes is not set (default true)", route) + } + } +} + func TestOpenAPIModule_SwaggerUIRoute(t *testing.T) { specPath := writeTempSpec(t, ".yaml", petstoreYAML) diff --git a/plugins/openapi/plugin.go b/plugins/openapi/plugin.go index d987a10d..326b8aa4 100644 --- a/plugins/openapi/plugin.go +++ b/plugins/openapi/plugin.go @@ -103,6 +103,10 @@ func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory { } } + if v, ok := cfg["register_routes"].(bool); ok { + oacfg.RegisterRoutes = &v + } + return module.NewOpenAPIModule(name, oacfg) }, } @@ -142,6 +146,13 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema { Placeholder: "my-router", InheritFrom: "dependency.name", }, + { + Key: "register_routes", + Label: "Register Routes", + Type: schema.FieldTypeBool, + Description: "When false, skip registering spec-path routes; only serve spec endpoints and Swagger UI (default: true)", + DefaultValue: true, + }, { Key: "validation", Label: "Validation", @@ -168,8 +179,9 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema { }, }, DefaultConfig: map[string]any{ - "validation": map[string]any{"request": true, "response": false}, - "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}, + "register_routes": true, + "validation": map[string]any{"request": true, "response": false}, + "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}, }, }, } diff --git a/schema/module_schema.go b/schema/module_schema.go index f3d40591..5ddac07e 100644 --- a/schema/module_schema.go +++ b/schema/module_schema.go @@ -837,10 +837,12 @@ func (r *ModuleSchemaRegistry) registerBuiltins() { {Key: "spec_file", Label: "Spec File", Type: FieldTypeFilePath, Required: true, Description: "Path to the OpenAPI v3 spec file (JSON or YAML)", Placeholder: "specs/petstore.yaml"}, {Key: "base_path", Label: "Base Path", Type: FieldTypeString, Description: "Base path prefix for all generated routes", Placeholder: "/api/v1"}, {Key: "router", Label: "Router Module", Type: FieldTypeString, Description: "Name of the http.router module to register routes on (auto-detected if omitted)", Placeholder: "my-router"}, + {Key: "register_routes", Label: "Register Routes", Type: FieldTypeBool, DefaultValue: true, Description: "When false, skip registering spec-path routes (only serve spec endpoints and Swagger UI); default true"}, {Key: "validation", Label: "Validation", Type: FieldTypeJSON, DefaultValue: map[string]any{"request": true, "response": false}, Description: "Request/response validation settings, e.g. {\"request\": true, \"response\": false}"}, {Key: "swagger_ui", Label: "Swagger UI", Type: FieldTypeJSON, DefaultValue: map[string]any{"enabled": false, "path": "/docs"}, Description: "Swagger UI settings, e.g. {\"enabled\": false, \"path\": \"/docs\"}"}, + {Key: "max_body_bytes", Label: "Max Body Bytes", Type: FieldTypeNumber, Description: "Maximum allowed request body size in bytes for validated OpenAPI operations (leave empty to use module default)", Placeholder: "1048576"}, }, - DefaultConfig: map[string]any{"validation": map[string]any{"request": true, "response": false}, "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}}, + DefaultConfig: map[string]any{"register_routes": true, "validation": map[string]any{"request": true, "response": false}, "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}}, }) r.Register(&ModuleSchema{