Skip to content

Commit 67faeab

Browse files
Copilotintel352
andauthored
openapi: add register_routes option to disable spec-path route registration (#249)
* Initial plan * feat: add register_routes option to openapi module to disable spec-path route registration Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * fix: add max_body_bytes to built-in openapi module schema for parity with plugin schema Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Co-authored-by: Jonathan Langevin <codingsloth@pm.me>
1 parent 6dd4a83 commit 67faeab

4 files changed

Lines changed: 117 additions & 24 deletions

File tree

module/openapi.go

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ type OpenAPISwaggerUIConfig struct {
3535

3636
// OpenAPIConfig holds the full configuration for an OpenAPI module.
3737
type OpenAPIConfig struct {
38-
SpecFile string `yaml:"spec_file" json:"spec_file"`
39-
BasePath string `yaml:"base_path" json:"base_path"`
40-
Validation OpenAPIValidationConfig `yaml:"validation" json:"validation"`
41-
SwaggerUI OpenAPISwaggerUIConfig `yaml:"swagger_ui" json:"swagger_ui"`
42-
RouterName string `yaml:"router" json:"router"` // optional: explicit router to attach to
43-
MaxBodyBytes int64 `yaml:"max_body_bytes" json:"max_body_bytes"` // max request body size (bytes); 0 = use default
38+
SpecFile string `yaml:"spec_file" json:"spec_file"`
39+
BasePath string `yaml:"base_path" json:"base_path"`
40+
Validation OpenAPIValidationConfig `yaml:"validation" json:"validation"`
41+
SwaggerUI OpenAPISwaggerUIConfig `yaml:"swagger_ui" json:"swagger_ui"`
42+
RouterName string `yaml:"router" json:"router"` // optional: explicit router to attach to
43+
MaxBodyBytes int64 `yaml:"max_body_bytes" json:"max_body_bytes"` // max request body size (bytes); 0 = use default
44+
RegisterRoutes *bool `yaml:"register_routes" json:"register_routes"` // when false, skip spec-path route registration; default true
4445
}
4546

4647
// defaultMaxBodyBytes is the default request body size limit (1 MiB) applied
@@ -217,22 +218,24 @@ func (m *OpenAPIModule) RegisterRoutes(router HTTPRouter) {
217218

218219
basePath := strings.TrimRight(m.cfg.BasePath, "/")
219220

220-
// Register a route for each path+method in the spec
221-
for specPath, pathItem := range m.spec.Paths {
222-
for method, op := range pathItem {
223-
httpMethod := strings.ToUpper(method)
224-
if !isValidHTTPMethod(httpMethod) {
225-
continue
221+
// Register a route for each path+method in the spec, unless register_routes is explicitly false.
222+
if m.cfg.RegisterRoutes == nil || *m.cfg.RegisterRoutes {
223+
for specPath, pathItem := range m.spec.Paths {
224+
for method, op := range pathItem {
225+
httpMethod := strings.ToUpper(method)
226+
if !isValidHTTPMethod(httpMethod) {
227+
continue
228+
}
229+
routePath := basePath + openAPIPathToHTTPPath(specPath)
230+
handler := m.buildRouteHandler(specPath, httpMethod, op)
231+
router.AddRoute(httpMethod, routePath, handler)
232+
m.logger.Debug("OpenAPI route registered",
233+
"module", m.name,
234+
"method", httpMethod,
235+
"path", routePath,
236+
"operationId", op.OperationID,
237+
)
226238
}
227-
routePath := basePath + openAPIPathToHTTPPath(specPath)
228-
handler := m.buildRouteHandler(specPath, httpMethod, op)
229-
router.AddRoute(httpMethod, routePath, handler)
230-
m.logger.Debug("OpenAPI route registered",
231-
"module", m.name,
232-
"method", httpMethod,
233-
"path", routePath,
234-
"operationId", op.OperationID,
235-
)
236239
}
237240
}
238241

module/openapi_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,82 @@ func TestOpenAPIModule_RegisterRoutes(t *testing.T) {
271271
}
272272
}
273273

274+
func TestOpenAPIModule_RegisterRoutesFalse(t *testing.T) {
275+
specPath := writeTempSpec(t, ".yaml", petstoreYAML)
276+
277+
falseVal := false
278+
mod := NewOpenAPIModule("petstore", OpenAPIConfig{
279+
SpecFile: specPath,
280+
BasePath: "/api/v1",
281+
RegisterRoutes: &falseVal,
282+
SwaggerUI: OpenAPISwaggerUIConfig{
283+
Enabled: true,
284+
Path: "/docs",
285+
},
286+
})
287+
if err := mod.Init(nil); err != nil {
288+
t.Fatalf("Init: %v", err)
289+
}
290+
291+
router := &testRouter{}
292+
mod.RegisterRoutes(router)
293+
294+
paths := make(map[string]bool)
295+
for _, rt := range router.routes {
296+
paths[rt.method+":"+rt.path] = true
297+
}
298+
299+
// Spec endpoints and Swagger UI should still be registered
300+
if !paths["GET:/api/v1/openapi.json"] {
301+
t.Error("expected GET:/api/v1/openapi.json to be registered even when register_routes=false")
302+
}
303+
if !paths["GET:/api/v1/openapi.yaml"] {
304+
t.Error("expected GET:/api/v1/openapi.yaml to be registered even when register_routes=false")
305+
}
306+
if !paths["GET:/api/v1/docs"] {
307+
t.Error("expected GET:/api/v1/docs (Swagger UI) to be registered even when register_routes=false")
308+
}
309+
310+
// Spec-path routes must NOT be registered
311+
specRoutes := []string{
312+
"GET:/api/v1/pets",
313+
"POST:/api/v1/pets",
314+
"GET:/api/v1/pets/{petId}",
315+
}
316+
for _, route := range specRoutes {
317+
if paths[route] {
318+
t.Errorf("expected spec route %q NOT to be registered when register_routes=false", route)
319+
}
320+
}
321+
}
322+
323+
func TestOpenAPIModule_RegisterRoutesNilDefaultsTrue(t *testing.T) {
324+
specPath := writeTempSpec(t, ".yaml", petstoreYAML)
325+
326+
// When RegisterRoutes is nil (not set), spec-path routes should be registered (default true).
327+
mod := NewOpenAPIModule("petstore", OpenAPIConfig{
328+
SpecFile: specPath,
329+
BasePath: "/api/v1",
330+
})
331+
if err := mod.Init(nil); err != nil {
332+
t.Fatalf("Init: %v", err)
333+
}
334+
335+
router := &testRouter{}
336+
mod.RegisterRoutes(router)
337+
338+
paths := make(map[string]bool)
339+
for _, rt := range router.routes {
340+
paths[rt.method+":"+rt.path] = true
341+
}
342+
343+
for _, route := range []string{"GET:/api/v1/pets", "POST:/api/v1/pets", "GET:/api/v1/pets/{petId}"} {
344+
if !paths[route] {
345+
t.Errorf("expected spec route %q to be registered when register_routes is not set (default true)", route)
346+
}
347+
}
348+
}
349+
274350
func TestOpenAPIModule_SwaggerUIRoute(t *testing.T) {
275351
specPath := writeTempSpec(t, ".yaml", petstoreYAML)
276352

plugins/openapi/plugin.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory {
103103
}
104104
}
105105

106+
if v, ok := cfg["register_routes"].(bool); ok {
107+
oacfg.RegisterRoutes = &v
108+
}
109+
106110
return module.NewOpenAPIModule(name, oacfg)
107111
},
108112
}
@@ -142,6 +146,13 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema {
142146
Placeholder: "my-router",
143147
InheritFrom: "dependency.name",
144148
},
149+
{
150+
Key: "register_routes",
151+
Label: "Register Routes",
152+
Type: schema.FieldTypeBool,
153+
Description: "When false, skip registering spec-path routes; only serve spec endpoints and Swagger UI (default: true)",
154+
DefaultValue: true,
155+
},
145156
{
146157
Key: "validation",
147158
Label: "Validation",
@@ -168,8 +179,9 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema {
168179
},
169180
},
170181
DefaultConfig: map[string]any{
171-
"validation": map[string]any{"request": true, "response": false},
172-
"swagger_ui": map[string]any{"enabled": false, "path": "/docs"},
182+
"register_routes": true,
183+
"validation": map[string]any{"request": true, "response": false},
184+
"swagger_ui": map[string]any{"enabled": false, "path": "/docs"},
173185
},
174186
},
175187
}

schema/module_schema.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,10 +837,12 @@ func (r *ModuleSchemaRegistry) registerBuiltins() {
837837
{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"},
838838
{Key: "base_path", Label: "Base Path", Type: FieldTypeString, Description: "Base path prefix for all generated routes", Placeholder: "/api/v1"},
839839
{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"},
840+
{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"},
840841
{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}"},
841842
{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\"}"},
843+
{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"},
842844
},
843-
DefaultConfig: map[string]any{"validation": map[string]any{"request": true, "response": false}, "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}},
845+
DefaultConfig: map[string]any{"register_routes": true, "validation": map[string]any{"request": true, "response": false}, "swagger_ui": map[string]any{"enabled": false, "path": "/docs"}},
844846
})
845847

846848
r.Register(&ModuleSchema{

0 commit comments

Comments
 (0)