api/openapi.go
Claude 43abce034e
chore(api): AX compliance sweep — banned imports, naming, test coverage
Replace fmt/errors/strings/encoding/json/os/os/exec/path/filepath with
core primitives; rename abbreviated variables; add Ugly test variants to
all test files; rename integration tests to TestFilename_Function_{Good,Bad,Ugly}.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 09:27:41 +01:00

185 lines
4.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import "dappco.re/go/core"
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
type SpecBuilder struct {
Title string
Description string
Version string
}
// Build generates the complete OpenAPI 3.1 JSON spec.
// Groups implementing DescribableGroup contribute endpoint documentation.
// Other groups are listed as tags only.
func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
spec := map[string]any{
"openapi": "3.1.0",
"info": map[string]any{
"title": sb.Title,
"description": sb.Description,
"version": sb.Version,
},
"paths": sb.buildPaths(groups),
"tags": sb.buildTags(groups),
}
// Add component schemas for the response envelope.
spec["components"] = map[string]any{
"schemas": map[string]any{
"Error": map[string]any{
"type": "object",
"properties": map[string]any{
"code": map[string]any{"type": "string"},
"message": map[string]any{"type": "string"},
"details": map[string]any{},
},
"required": []string{"code", "message"},
},
"Meta": map[string]any{
"type": "object",
"properties": map[string]any{
"request_id": map[string]any{"type": "string"},
"duration": map[string]any{"type": "string"},
"page": map[string]any{"type": "integer"},
"per_page": map[string]any{"type": "integer"},
"total": map[string]any{"type": "integer"},
},
},
},
}
result := core.JSONMarshal(spec)
if !result.OK {
return nil, result.Value.(error)
}
return result.Value.([]byte), nil
}
// buildPaths generates the paths object from all DescribableGroups.
func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
paths := map[string]any{
// Built-in health endpoint.
"/health": map[string]any{
"get": map[string]any{
"summary": "Health check",
"description": "Returns server health status",
"tags": []string{"system"},
"responses": map[string]any{
"200": map[string]any{
"description": "Server is healthy",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(map[string]any{"type": "string"}),
},
},
},
},
},
},
}
for _, group := range groups {
describableGroup, ok := group.(DescribableGroup)
if !ok {
continue
}
for _, rd := range describableGroup.Describe() {
fullPath := group.BasePath() + rd.Path
method := core.Lower(rd.Method)
operation := map[string]any{
"summary": rd.Summary,
"description": rd.Description,
"tags": rd.Tags,
"responses": map[string]any{
"200": map[string]any{
"description": "Successful response",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(rd.Response),
},
},
},
"400": map[string]any{
"description": "Bad request",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
},
},
}
// Add request body for methods that accept one.
if rd.RequestBody != nil && (method == "post" || method == "put" || method == "patch") {
operation["requestBody"] = map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": rd.RequestBody,
},
},
}
}
// Create or extend path item.
if existing, exists := paths[fullPath]; exists {
existing.(map[string]any)[method] = operation
} else {
paths[fullPath] = map[string]any{
method: operation,
}
}
}
}
return paths
}
// buildTags generates the tags array from all RouteGroups.
func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
tags := []map[string]any{
{"name": "system", "description": "System endpoints"},
}
seen := map[string]bool{"system": true}
for _, group := range groups {
name := group.Name()
if !seen[name] {
tags = append(tags, map[string]any{
"name": name,
"description": name + " endpoints",
})
seen[name] = true
}
}
return tags
}
// envelopeSchema wraps a data schema in the standard Response[T] envelope.
func envelopeSchema(dataSchema map[string]any) map[string]any {
properties := map[string]any{
"success": map[string]any{"type": "boolean"},
"error": map[string]any{
"$ref": "#/components/schemas/Error",
},
"meta": map[string]any{
"$ref": "#/components/schemas/Meta",
},
}
if dataSchema != nil {
properties["data"] = dataSchema
}
return map[string]any{
"type": "object",
"properties": properties,
"required": []string{"success"},
}
}