Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 24 additions & 21 deletions module/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}

Expand Down
76 changes: 76 additions & 0 deletions module/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 14 additions & 2 deletions plugins/openapi/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}
Expand Down Expand Up @@ -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",
Expand All @@ -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"},
},
},
}
Expand Down
4 changes: 3 additions & 1 deletion schema/module_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}"},
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The built-in OpenAPI ModuleSchema in this registry still doesn’t include the max_body_bytes config field, even though the OpenAPI module config struct and the OpenAPI plugin schema support it. This creates a divergence between built-in schema-driven tooling/validation and the plugin-provided schema/UI; consider adding max_body_bytes here for parity (and an appropriate DefaultValue if desired).

Suggested change
{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: "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"},

Copilot uses AI. Check for mistakes.
{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{
Expand Down
Loading