refactor: apply go fix modernizers for Go 1.26
Automated fixes: interface{} → any, range-over-int, t.Context(),
wg.Go(), strings.SplitSeq, strings.Builder, slices.Contains,
maps helpers, min/max builtins.
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c317defc82
commit
1c19499bf0
13 changed files with 815 additions and 32 deletions
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
9
.idea/go-api.iml
generated
Normal file
9
.idea/go-api.iml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
11
.idea/go.imports.xml
generated
Normal file
11
.idea/go.imports.xml
generated
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="github.com/pkg/errors" />
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/material_theme_project_new.xml
generated
Normal file
10
.idea/material_theme_project_new.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="userId" value="-66485c34:19c7b9d8232:-7bdb" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KubernetesApiProvider">{}</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/go-api.iml" filepath="$PROJECT_DIR$/.idea/go-api.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
16
authentik.go
16
authentik.go
|
|
@ -5,6 +5,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -43,12 +44,7 @@ type AuthentikUser struct {
|
|||
|
||||
// HasGroup reports whether the user belongs to the named group.
|
||||
func (u *AuthentikUser) HasGroup(group string) bool {
|
||||
for _, g := range u.Groups {
|
||||
if g == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(u.Groups, group)
|
||||
}
|
||||
|
||||
// authentikUserKey is the Gin context key used to store the authenticated user.
|
||||
|
|
@ -112,10 +108,10 @@ func validateJWT(ctx context.Context, cfg AuthentikConfig, rawToken string) (*Au
|
|||
|
||||
var claims struct {
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Sub string `json:"sub"`
|
||||
Groups []string `json:"groups"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Sub string `json:"sub"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
5
cache.go
5
cache.go
|
|
@ -4,6 +4,7 @@ package api
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"maps"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -113,9 +114,7 @@ func cacheMiddleware(store *cacheStore, ttl time.Duration) gin.HandlerFunc {
|
|||
status := cw.ResponseWriter.Status()
|
||||
if status >= 200 && status < 300 {
|
||||
headers := make(http.Header)
|
||||
for k, vals := range cw.ResponseWriter.Header() {
|
||||
headers[k] = vals
|
||||
}
|
||||
maps.Copy(headers, cw.ResponseWriter.Header())
|
||||
store.set(key, &cacheEntry{
|
||||
status: status,
|
||||
headers: headers,
|
||||
|
|
|
|||
366
docs/plans/2026-02-21-openapi-spec-gen.md
Normal file
366
docs/plans/2026-02-21-openapi-spec-gen.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# go-api Phase 3: OpenAPI Spec Generation + SDK Codegen
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Turn MCP tool definitions into REST endpoints with auto-generated OpenAPI 3.1 specs and multi-language SDK codegen.
|
||||
|
||||
**Architecture:** Runtime OpenAPI generation (NOT swaggo annotations) — routes are dynamic via RouteGroup, Response[T] generics break swaggo, and MCP tools already carry JSON Schema at runtime. A `ToolBridge` converts tool descriptors into RouteGroup + OpenAPI metadata. A `SpecBuilder` constructs the full OpenAPI 3.1 spec from all registered groups. SDK codegen wraps `openapi-generator-cli`.
|
||||
|
||||
**Tech Stack:** go-api (Gin), OpenAPI 3.1, openapi-generator-cli, go-ai MCP SDK
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1+2 complete (143 tests, 21 With*() options). The "missing link" is: MCP tools are only accessible via JSON-RPC. This phase creates a REST bridge so any HTTP client (or generated SDK) can call the same services. The pipeline: **MCP tool → REST endpoint → OpenAPI spec → SDK codegen**.
|
||||
|
||||
Current swagger.go serves an empty spec (`paths: {}`). swaggo annotations don't exist anywhere and can't work with dynamic RouteGroups or generics. The solution is runtime spec generation.
|
||||
|
||||
## Critical Files
|
||||
|
||||
- `swagger.go` — Refactor from empty hardcoded spec to SpecBuilder
|
||||
- `group.go` — Add DescribableGroup interface
|
||||
- `api.go` — Pass groups to registerSwagger, Engine struct changes
|
||||
- `response.go` — Response[T], Error, Meta structs (read-only reference for envelope schema)
|
||||
- `options.go` — WithSwagger already exists, no changes needed
|
||||
- `go-ai/mcp/mcp.go` — Tool registration with typed Input/Output structs
|
||||
- `go-ai/mcp/subsystem.go` — Subsystem interface
|
||||
|
||||
## Wave 1: OpenAPI Runtime Spec (go-api only)
|
||||
|
||||
### Task 1: DescribableGroup interface + RouteDescription
|
||||
|
||||
**Files:**
|
||||
- Modify: `group.go`
|
||||
- Test: `group_test.go` (existing — add new tests)
|
||||
|
||||
Add a `DescribableGroup` interface that RouteGroups can optionally implement to provide OpenAPI metadata. This is the opt-in contract for spec generation.
|
||||
|
||||
```go
|
||||
// DescribableGroup extends RouteGroup with OpenAPI metadata.
|
||||
// RouteGroups that implement this will have their endpoints
|
||||
// included in the generated OpenAPI specification.
|
||||
type DescribableGroup interface {
|
||||
RouteGroup
|
||||
Describe() []RouteDescription
|
||||
}
|
||||
|
||||
// RouteDescription describes a single endpoint for OpenAPI generation.
|
||||
type RouteDescription struct {
|
||||
Method string // HTTP method: GET, POST, PUT, DELETE, PATCH
|
||||
Path string // Path relative to BasePath, e.g. "/generate"
|
||||
Summary string // Short summary
|
||||
Description string // Long description
|
||||
Tags []string // OpenAPI tags for grouping
|
||||
RequestBody map[string]any // JSON Schema for request body (nil for GET)
|
||||
Response map[string]any // JSON Schema for success response data
|
||||
}
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestDescribableGroup_Good_ImplementsRouteGroup` — A struct implementing both interfaces compiles and works
|
||||
- `TestDescribableGroup_Good_DescribeReturnsRoutes` — Verify Describe() returns correct RouteDescription list
|
||||
- `TestDescribableGroup_Good_EmptyDescribe` — Empty Describe() is valid (no endpoints documented)
|
||||
- `TestDescribableGroup_Good_MultipleVerbs` — GET, POST, DELETE on same base path
|
||||
- `TestDescribableGroup_Bad_NilSchemas` — RequestBody and Response can be nil
|
||||
|
||||
### Task 2: ToolBridge — tool descriptors to REST endpoints
|
||||
|
||||
**Files:**
|
||||
- Create: `bridge.go`
|
||||
- Create: `bridge_test.go`
|
||||
|
||||
The `ToolBridge` converts tool descriptors into a RouteGroup that also implements DescribableGroup. It creates `POST /{tool_name}` endpoints for each registered tool.
|
||||
|
||||
```go
|
||||
// ToolDescriptor describes a tool that can be exposed as a REST endpoint.
|
||||
type ToolDescriptor struct {
|
||||
Name string // Tool name, e.g. "file_read" (becomes POST path)
|
||||
Description string // Human-readable description
|
||||
Group string // OpenAPI tag group, e.g. "files"
|
||||
InputSchema map[string]any // JSON Schema for request body
|
||||
OutputSchema map[string]any // JSON Schema for response data (optional)
|
||||
}
|
||||
|
||||
// ToolBridge converts tool descriptors into REST endpoints and OpenAPI paths.
|
||||
// It implements both RouteGroup and DescribableGroup.
|
||||
type ToolBridge struct {
|
||||
basePath string
|
||||
tools []boundTool
|
||||
}
|
||||
|
||||
type boundTool struct {
|
||||
descriptor ToolDescriptor
|
||||
handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
func NewToolBridge(basePath string) *ToolBridge
|
||||
|
||||
// Add registers a tool with its HTTP handler.
|
||||
func (b *ToolBridge) Add(desc ToolDescriptor, handler gin.HandlerFunc)
|
||||
|
||||
// RouteGroup implementation
|
||||
func (b *ToolBridge) Name() string
|
||||
func (b *ToolBridge) BasePath() string
|
||||
func (b *ToolBridge) RegisterRoutes(rg *gin.RouterGroup)
|
||||
|
||||
// DescribableGroup implementation
|
||||
func (b *ToolBridge) Describe() []RouteDescription
|
||||
|
||||
// Tools returns registered tool descriptors.
|
||||
func (b *ToolBridge) Tools() []ToolDescriptor
|
||||
```
|
||||
|
||||
**Tests (6):**
|
||||
- `TestToolBridge_Good_RegisterAndServe` — Add 2 tools, verify POST endpoints respond
|
||||
- `TestToolBridge_Good_BasePath` — Custom base path `/api/v1/tools` works
|
||||
- `TestToolBridge_Good_Describe` — Describe() returns correct RouteDescription for each tool
|
||||
- `TestToolBridge_Good_ToolsAccessor` — Tools() returns all registered descriptors
|
||||
- `TestToolBridge_Bad_EmptyBridge` — Bridge with no tools is valid, RegisterRoutes is no-op
|
||||
- `TestToolBridge_Good_IntegrationWithEngine` — Register bridge with Engine, verify routes accessible via Handler()
|
||||
|
||||
### Task 3: SpecBuilder — OpenAPI 3.1 spec generation
|
||||
|
||||
**Files:**
|
||||
- Create: `openapi.go`
|
||||
- Create: `openapi_test.go`
|
||||
|
||||
Builds a complete OpenAPI 3.1 JSON document from registered RouteGroups. Groups implementing DescribableGroup contribute their endpoint metadata. All responses are wrapped in the Response[T] envelope.
|
||||
|
||||
```go
|
||||
// SpecBuilder constructs an OpenAPI 3.1 spec 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)
|
||||
```
|
||||
|
||||
Key behaviours:
|
||||
- Always includes `GET /health` (built-in)
|
||||
- For each DescribableGroup, generates path items from Describe()
|
||||
- Wraps all response schemas in the Response[T] envelope (success/data/error/meta)
|
||||
- Uses OpenAPI 3.1 (compatible with JSON Schema 2020-12 from MCP SDK)
|
||||
- Tags derived from RouteGroup.Name()
|
||||
- Output is valid, parseable OpenAPI 3.1 JSON
|
||||
|
||||
**Tests (6):**
|
||||
- `TestSpecBuilder_Good_EmptyGroups` — Produces valid spec with just /health
|
||||
- `TestSpecBuilder_Good_WithDescribableGroup` — Paths populated from Describe()
|
||||
- `TestSpecBuilder_Good_EnvelopeWrapping` — Response schema wraps data in Response[T] envelope
|
||||
- `TestSpecBuilder_Good_NonDescribableGroup` — Non-describable groups appear as tags only
|
||||
- `TestSpecBuilder_Good_ToolBridgeIntegration` — ToolBridge + SpecBuilder produces correct tool endpoints
|
||||
- `TestSpecBuilder_Bad_InfoFields` — Title, description, version appear in output
|
||||
|
||||
### Task 4: Refactor swagger.go to use SpecBuilder
|
||||
|
||||
**Files:**
|
||||
- Modify: `swagger.go`
|
||||
- Modify: `api.go` (pass groups to registerSwagger)
|
||||
- Modify: `swagger_test.go` (update tests)
|
||||
|
||||
Replace the hardcoded empty JSON spec with SpecBuilder-generated spec. The swaggerSpec struct now wraps SpecBuilder and caches the result via sync.Once. `registerSwagger` receives the engine's groups.
|
||||
|
||||
```go
|
||||
type swaggerSpec struct {
|
||||
builder *SpecBuilder
|
||||
groups []RouteGroup
|
||||
once sync.Once
|
||||
doc string
|
||||
}
|
||||
|
||||
func (s *swaggerSpec) ReadDoc() string {
|
||||
s.once.Do(func() {
|
||||
data, _ := s.builder.Build(s.groups)
|
||||
s.doc = string(data)
|
||||
})
|
||||
return s.doc
|
||||
}
|
||||
|
||||
func registerSwagger(g *gin.Engine, title, description, version string, groups []RouteGroup)
|
||||
```
|
||||
|
||||
In `api.go`, change `build()`:
|
||||
```go
|
||||
// Mount Swagger UI if enabled.
|
||||
if e.swaggerEnabled {
|
||||
registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.groups)
|
||||
}
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestSwagger_Good_SpecNotEmpty` — GET /swagger/doc.json returns spec with /health path
|
||||
- `TestSwagger_Good_WithToolBridge` — Register ToolBridge, verify tool endpoints appear in spec
|
||||
- `TestSwagger_Good_CachesSpec` — Multiple calls return same cached spec
|
||||
- `TestSwagger_Good_InfoFromOptions` — Title/desc/version from WithSwagger() appear in spec
|
||||
- `TestSwagger_Good_ValidOpenAPI` — Output parses as valid JSON with correct openapi version field
|
||||
|
||||
### Task 5: SpecExporter — export spec to file/stdout
|
||||
|
||||
**Files:**
|
||||
- Create: `export.go`
|
||||
- Create: `export_test.go`
|
||||
|
||||
A helper that exports the generated spec to a file or io.Writer. Supports JSON and YAML output. This is what `core api spec` will call.
|
||||
|
||||
```go
|
||||
// ExportSpec generates the OpenAPI spec and writes it to w.
|
||||
// Format can be "json" or "yaml".
|
||||
func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error
|
||||
|
||||
// ExportSpecToFile writes the spec to the given path.
|
||||
func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestExportSpec_Good_JSON` — Writes valid JSON to buffer
|
||||
- `TestExportSpec_Good_YAML` — Writes valid YAML to buffer
|
||||
- `TestExportSpec_Bad_InvalidFormat` — Returns error for unknown format
|
||||
- `TestExportSpecToFile_Good_CreatesFile` — Writes file to temp dir
|
||||
- `TestExportSpec_Good_WithToolBridge` — Full pipeline: tools → bridge → spec → export
|
||||
|
||||
## Wave 2: MCP-to-REST Bridge (go-ai)
|
||||
|
||||
### Task 6: Tool registry in go-ai Service
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-ai/mcp/mcp.go` — Add ToolRecord and registry
|
||||
- Create: `go-ai/mcp/registry.go` — ToolRecord type and helpers
|
||||
- Create: `go-ai/mcp/registry_test.go`
|
||||
|
||||
The MCP SDK's `mcp.Server.tools` is unexported. We maintain a parallel registry in `Service` that records tool metadata (name, description, input/output Go types) as tools are registered. This becomes the source of truth for both MCP and REST.
|
||||
|
||||
```go
|
||||
// ToolRecord captures metadata about a registered MCP tool.
|
||||
type ToolRecord struct {
|
||||
Name string
|
||||
Description string
|
||||
Group string // Subsystem group name
|
||||
InputSchema map[string]any // JSON Schema from Go struct tags
|
||||
OutputSchema map[string]any
|
||||
Handler any // The original handler func for type assertion
|
||||
}
|
||||
|
||||
// Tools returns all recorded tool metadata.
|
||||
func (s *Service) Tools() []ToolRecord
|
||||
```
|
||||
|
||||
Modify `registerTools` and subsystem registration to also record tools. Use `jsonschema-go` (already a transitive dep from MCP SDK) to extract schemas from the typed Input/Output parameters.
|
||||
|
||||
**Tests (5):**
|
||||
- `TestToolRegistry_Good_RecordsTools` — After New(), Tools() returns all registered tools
|
||||
- `TestToolRegistry_Good_IncludesSubsystems` — Subsystem tools included in registry
|
||||
- `TestToolRegistry_Good_SchemaExtraction` — InputSchema contains correct properties from struct tags
|
||||
- `TestToolRegistry_Good_ToolCount` — Count matches expected (10 built-in + subsystem tools)
|
||||
- `TestToolRegistry_Bad_EmptyService` — Service with no subsystems has only built-in tools
|
||||
|
||||
### Task 7: BridgeToAPI — MCP tools to go-api ToolBridge
|
||||
|
||||
**Files:**
|
||||
- Create: `go-ai/mcp/bridge.go`
|
||||
- Create: `go-ai/mcp/bridge_test.go`
|
||||
|
||||
Converts MCP tool records into go-api ToolBridge entries. Each tool becomes a POST endpoint. The handler unmarshals JSON from request body, calls the business logic, and wraps the response in `api.OK()`/`api.Fail()`.
|
||||
|
||||
```go
|
||||
// BridgeToAPI populates a go-api ToolBridge from recorded MCP tools.
|
||||
// Each tool becomes a POST endpoint at /{tool_name}.
|
||||
func BridgeToAPI(svc *Service, bridge *api.ToolBridge) error
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestBridgeToAPI_Good_FileRead` — Bridge file_read, POST JSON body, get Response[ReadFileOutput]
|
||||
- `TestBridgeToAPI_Good_AllTools` — All tools from registry appear in bridge
|
||||
- `TestBridgeToAPI_Good_DescribableGroup` — Bridge implements DescribableGroup, Describe() returns correct metadata
|
||||
- `TestBridgeToAPI_Bad_InvalidJSON` — POST malformed JSON returns 400 with error envelope
|
||||
- `TestBridgeToAPI_Good_EndToEnd` — New service → BridgeToAPI → register with Engine → GET /swagger/doc.json shows all tools
|
||||
|
||||
## Wave 3: SDK Codegen + CLI
|
||||
|
||||
### Task 8: Codegen wrapper
|
||||
|
||||
**Files:**
|
||||
- Create: `codegen.go`
|
||||
- Create: `codegen_test.go`
|
||||
|
||||
Wraps `openapi-generator-cli` for multi-language SDK generation. Checks if the tool is available and provides clear installation instructions if missing.
|
||||
|
||||
```go
|
||||
// SDKGenerator wraps openapi-generator-cli for SDK generation.
|
||||
type SDKGenerator struct {
|
||||
SpecPath string
|
||||
OutputDir string
|
||||
PackageName string
|
||||
}
|
||||
|
||||
// Generate creates an SDK for the given language.
|
||||
// Supported: "go", "typescript-fetch", "python", "java", "csharp"
|
||||
func (g *SDKGenerator) Generate(ctx context.Context, language string) error
|
||||
|
||||
// Available checks if openapi-generator-cli is installed.
|
||||
func (g *SDKGenerator) Available() bool
|
||||
|
||||
// SupportedLanguages returns the list of supported SDK languages.
|
||||
func SupportedLanguages() []string
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestSDKGenerator_Good_CommandConstruction` — Verify correct CLI arguments without executing
|
||||
- `TestSDKGenerator_Good_SupportedLanguages` — Returns expected language list
|
||||
- `TestSDKGenerator_Bad_UnsupportedLanguage` — Returns error for "brainfuck"
|
||||
- `TestSDKGenerator_Bad_MissingSpec` — Returns error when spec file doesn't exist
|
||||
- `TestSDKGenerator_Good_OutputDirCreated` — Creates output directory if missing
|
||||
|
||||
### Task 9: CLI commands — `core api spec` and `core api sdk`
|
||||
|
||||
**Files:**
|
||||
- Create: `cmd/core-cli/api.go` (or appropriate location in CLI tree)
|
||||
- Create test file alongside
|
||||
|
||||
New `api` command group in the core CLI:
|
||||
|
||||
```
|
||||
core api spec # Export OpenAPI spec JSON to stdout
|
||||
core api spec --output spec.json # Write to file
|
||||
core api spec --format yaml # YAML format
|
||||
|
||||
core api sdk --lang go # Generate Go SDK
|
||||
core api sdk --lang typescript-fetch,python --output ./sdk/
|
||||
core api sdk --spec spec.json --lang go --output ./sdk/go
|
||||
```
|
||||
|
||||
This depends on understanding the CLI registration pattern. The commands create an MCP service, bridge tools, build spec, and either export or generate SDKs.
|
||||
|
||||
**Tests (4):**
|
||||
- `TestAPISpecCmd_Good_JSON` — Outputs valid JSON to stdout
|
||||
- `TestAPISpecCmd_Good_YAML` — Outputs valid YAML with --format yaml
|
||||
- `TestAPISDKCmd_Bad_NoLang` — Returns error without --lang flag
|
||||
- `TestAPISDKCmd_Good_ValidatesLanguage` — Rejects unsupported language with helpful message
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks are complete:
|
||||
|
||||
1. **Unit tests**: `go test ./...` — all pass
|
||||
2. **go-ai tests**: `cd ../go-ai && go test ./...` — all pass
|
||||
3. **Spec roundtrip**: Register a ToolBridge with sample tools, build Engine with WithSwagger, GET `/swagger/doc.json` → verify paths are populated, not empty
|
||||
4. **Export**: `ExportSpec` to temp file in JSON and YAML, verify both parse correctly
|
||||
5. **SDK smoke test** (if openapi-generator-cli installed): Generate Go client from exported spec, verify it compiles
|
||||
|
||||
## Dependency Sequencing
|
||||
|
||||
```
|
||||
Task 1 (DescribableGroup) ← Task 2 (ToolBridge) ← Task 3 (SpecBuilder)
|
||||
Task 3 ← Task 4 (swagger refactor)
|
||||
Task 3 ← Task 5 (ExportSpec)
|
||||
Task 2 ← Task 6 (Tool registry) ← Task 7 (BridgeToAPI)
|
||||
Task 5 ← Task 8 (Codegen)
|
||||
Task 5 + Task 7 ← Task 9 (CLI commands)
|
||||
```
|
||||
|
||||
Wave 1 (Tasks 1-5) is self-contained in go-api. Wave 2 (Tasks 6-7) bridges go-ai. Wave 3 (Tasks 8-9) adds codegen and CLI.
|
||||
366
docs/plans/valiant-hatching-wave.md
Normal file
366
docs/plans/valiant-hatching-wave.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# go-api Phase 3: OpenAPI Spec Generation + SDK Codegen
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Turn MCP tool definitions into REST endpoints with auto-generated OpenAPI 3.1 specs and multi-language SDK codegen.
|
||||
|
||||
**Architecture:** Runtime OpenAPI generation (NOT swaggo annotations) — routes are dynamic via RouteGroup, Response[T] generics break swaggo, and MCP tools already carry JSON Schema at runtime. A `ToolBridge` converts tool descriptors into RouteGroup + OpenAPI metadata. A `SpecBuilder` constructs the full OpenAPI 3.1 spec from all registered groups. SDK codegen wraps `openapi-generator-cli`.
|
||||
|
||||
**Tech Stack:** go-api (Gin), OpenAPI 3.1, openapi-generator-cli, go-ai MCP SDK
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1+2 complete (143 tests, 21 With*() options). The "missing link" is: MCP tools are only accessible via JSON-RPC. This phase creates a REST bridge so any HTTP client (or generated SDK) can call the same services. The pipeline: **MCP tool → REST endpoint → OpenAPI spec → SDK codegen**.
|
||||
|
||||
Current swagger.go serves an empty spec (`paths: {}`). swaggo annotations don't exist anywhere and can't work with dynamic RouteGroups or generics. The solution is runtime spec generation.
|
||||
|
||||
## Critical Files
|
||||
|
||||
- `/Users/snider/Code/go-api/swagger.go` — Refactor from empty hardcoded spec to SpecBuilder
|
||||
- `/Users/snider/Code/go-api/group.go` — Add DescribableGroup interface
|
||||
- `/Users/snider/Code/go-api/api.go` — Pass groups to registerSwagger, Engine struct changes
|
||||
- `/Users/snider/Code/go-api/response.go` — Response[T], Error, Meta structs (read-only reference for envelope schema)
|
||||
- `/Users/snider/Code/go-api/options.go` — WithSwagger already exists, no changes needed
|
||||
- `/Users/snider/Code/go-ai/mcp/mcp.go` — Tool registration with typed Input/Output structs
|
||||
- `/Users/snider/Code/go-ai/mcp/subsystem.go` — Subsystem interface
|
||||
|
||||
## Wave 1: OpenAPI Runtime Spec (go-api only)
|
||||
|
||||
### Task 1: DescribableGroup interface + RouteDescription
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/go-api/group.go`
|
||||
- Test: `/Users/snider/Code/go-api/group_test.go` (existing — add new tests)
|
||||
|
||||
Add a `DescribableGroup` interface that RouteGroups can optionally implement to provide OpenAPI metadata. This is the opt-in contract for spec generation.
|
||||
|
||||
```go
|
||||
// DescribableGroup extends RouteGroup with OpenAPI metadata.
|
||||
// RouteGroups that implement this will have their endpoints
|
||||
// included in the generated OpenAPI specification.
|
||||
type DescribableGroup interface {
|
||||
RouteGroup
|
||||
Describe() []RouteDescription
|
||||
}
|
||||
|
||||
// RouteDescription describes a single endpoint for OpenAPI generation.
|
||||
type RouteDescription struct {
|
||||
Method string // HTTP method: GET, POST, PUT, DELETE, PATCH
|
||||
Path string // Path relative to BasePath, e.g. "/generate"
|
||||
Summary string // Short summary
|
||||
Description string // Long description
|
||||
Tags []string // OpenAPI tags for grouping
|
||||
RequestBody map[string]any // JSON Schema for request body (nil for GET)
|
||||
Response map[string]any // JSON Schema for success response data
|
||||
}
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestDescribableGroup_Good_ImplementsRouteGroup` — A struct implementing both interfaces compiles and works
|
||||
- `TestDescribableGroup_Good_DescribeReturnsRoutes` — Verify Describe() returns correct RouteDescription list
|
||||
- `TestDescribableGroup_Good_EmptyDescribe` — Empty Describe() is valid (no endpoints documented)
|
||||
- `TestDescribableGroup_Good_MultipleVerbs` — GET, POST, DELETE on same base path
|
||||
- `TestDescribableGroup_Bad_NilSchemas` — RequestBody and Response can be nil
|
||||
|
||||
### Task 2: ToolBridge — tool descriptors to REST endpoints
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/go-api/bridge.go`
|
||||
- Create: `/Users/snider/Code/go-api/bridge_test.go`
|
||||
|
||||
The `ToolBridge` converts tool descriptors into a RouteGroup that also implements DescribableGroup. It creates `POST /{tool_name}` endpoints for each registered tool.
|
||||
|
||||
```go
|
||||
// ToolDescriptor describes a tool that can be exposed as a REST endpoint.
|
||||
type ToolDescriptor struct {
|
||||
Name string // Tool name, e.g. "file_read" (becomes POST path)
|
||||
Description string // Human-readable description
|
||||
Group string // OpenAPI tag group, e.g. "files"
|
||||
InputSchema map[string]any // JSON Schema for request body
|
||||
OutputSchema map[string]any // JSON Schema for response data (optional)
|
||||
}
|
||||
|
||||
// ToolBridge converts tool descriptors into REST endpoints and OpenAPI paths.
|
||||
// It implements both RouteGroup and DescribableGroup.
|
||||
type ToolBridge struct {
|
||||
basePath string
|
||||
tools []boundTool
|
||||
}
|
||||
|
||||
type boundTool struct {
|
||||
descriptor ToolDescriptor
|
||||
handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
func NewToolBridge(basePath string) *ToolBridge
|
||||
|
||||
// Add registers a tool with its HTTP handler.
|
||||
func (b *ToolBridge) Add(desc ToolDescriptor, handler gin.HandlerFunc)
|
||||
|
||||
// RouteGroup implementation
|
||||
func (b *ToolBridge) Name() string
|
||||
func (b *ToolBridge) BasePath() string
|
||||
func (b *ToolBridge) RegisterRoutes(rg *gin.RouterGroup)
|
||||
|
||||
// DescribableGroup implementation
|
||||
func (b *ToolBridge) Describe() []RouteDescription
|
||||
|
||||
// Tools returns registered tool descriptors.
|
||||
func (b *ToolBridge) Tools() []ToolDescriptor
|
||||
```
|
||||
|
||||
**Tests (6):**
|
||||
- `TestToolBridge_Good_RegisterAndServe` — Add 2 tools, verify POST endpoints respond
|
||||
- `TestToolBridge_Good_BasePath` — Custom base path `/api/v1/tools` works
|
||||
- `TestToolBridge_Good_Describe` — Describe() returns correct RouteDescription for each tool
|
||||
- `TestToolBridge_Good_ToolsAccessor` — Tools() returns all registered descriptors
|
||||
- `TestToolBridge_Bad_EmptyBridge` — Bridge with no tools is valid, RegisterRoutes is no-op
|
||||
- `TestToolBridge_Good_IntegrationWithEngine` — Register bridge with Engine, verify routes accessible via Handler()
|
||||
|
||||
### Task 3: SpecBuilder — OpenAPI 3.1 spec generation
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/go-api/openapi.go`
|
||||
- Create: `/Users/snider/Code/go-api/openapi_test.go`
|
||||
|
||||
Builds a complete OpenAPI 3.1 JSON document from registered RouteGroups. Groups implementing DescribableGroup contribute their endpoint metadata. All responses are wrapped in the Response[T] envelope.
|
||||
|
||||
```go
|
||||
// SpecBuilder constructs an OpenAPI 3.1 spec 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)
|
||||
```
|
||||
|
||||
Key behaviours:
|
||||
- Always includes `GET /health` (built-in)
|
||||
- For each DescribableGroup, generates path items from Describe()
|
||||
- Wraps all response schemas in the Response[T] envelope (success/data/error/meta)
|
||||
- Uses OpenAPI 3.1 (compatible with JSON Schema 2020-12 from MCP SDK)
|
||||
- Tags derived from RouteGroup.Name()
|
||||
- Output is valid, parseable OpenAPI 3.1 JSON
|
||||
|
||||
**Tests (6):**
|
||||
- `TestSpecBuilder_Good_EmptyGroups` — Produces valid spec with just /health
|
||||
- `TestSpecBuilder_Good_WithDescribableGroup` — Paths populated from Describe()
|
||||
- `TestSpecBuilder_Good_EnvelopeWrapping` — Response schema wraps data in Response[T] envelope
|
||||
- `TestSpecBuilder_Good_NonDescribableGroup` — Non-describable groups appear as tags only
|
||||
- `TestSpecBuilder_Good_ToolBridgeIntegration` — ToolBridge + SpecBuilder produces correct tool endpoints
|
||||
- `TestSpecBuilder_Bad_InfoFields` — Title, description, version appear in output
|
||||
|
||||
### Task 4: Refactor swagger.go to use SpecBuilder
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/go-api/swagger.go`
|
||||
- Modify: `/Users/snider/Code/go-api/api.go` (pass groups to registerSwagger)
|
||||
- Modify: `/Users/snider/Code/go-api/swagger_test.go` (update tests)
|
||||
|
||||
Replace the hardcoded empty JSON spec with SpecBuilder-generated spec. The swaggerSpec struct now wraps SpecBuilder and caches the result via sync.Once. `registerSwagger` receives the engine's groups.
|
||||
|
||||
```go
|
||||
type swaggerSpec struct {
|
||||
builder *SpecBuilder
|
||||
groups []RouteGroup
|
||||
once sync.Once
|
||||
doc string
|
||||
}
|
||||
|
||||
func (s *swaggerSpec) ReadDoc() string {
|
||||
s.once.Do(func() {
|
||||
data, _ := s.builder.Build(s.groups)
|
||||
s.doc = string(data)
|
||||
})
|
||||
return s.doc
|
||||
}
|
||||
|
||||
func registerSwagger(g *gin.Engine, title, description, version string, groups []RouteGroup)
|
||||
```
|
||||
|
||||
In `api.go`, change `build()`:
|
||||
```go
|
||||
// Mount Swagger UI if enabled.
|
||||
if e.swaggerEnabled {
|
||||
registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.groups)
|
||||
}
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestSwagger_Good_SpecNotEmpty` — GET /swagger/doc.json returns spec with /health path
|
||||
- `TestSwagger_Good_WithToolBridge` — Register ToolBridge, verify tool endpoints appear in spec
|
||||
- `TestSwagger_Good_CachesSpec` — Multiple calls return same cached spec
|
||||
- `TestSwagger_Good_InfoFromOptions` — Title/desc/version from WithSwagger() appear in spec
|
||||
- `TestSwagger_Good_ValidOpenAPI` — Output parses as valid JSON with correct openapi version field
|
||||
|
||||
### Task 5: SpecExporter — export spec to file/stdout
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/go-api/export.go`
|
||||
- Create: `/Users/snider/Code/go-api/export_test.go`
|
||||
|
||||
A helper that exports the generated spec to a file or io.Writer. Supports JSON and YAML output. This is what `core api spec` will call.
|
||||
|
||||
```go
|
||||
// ExportSpec generates the OpenAPI spec and writes it to w.
|
||||
// Format can be "json" or "yaml".
|
||||
func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error
|
||||
|
||||
// ExportSpecToFile writes the spec to the given path.
|
||||
func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestExportSpec_Good_JSON` — Writes valid JSON to buffer
|
||||
- `TestExportSpec_Good_YAML` — Writes valid YAML to buffer
|
||||
- `TestExportSpec_Bad_InvalidFormat` — Returns error for unknown format
|
||||
- `TestExportSpecToFile_Good_CreatesFile` — Writes file to temp dir
|
||||
- `TestExportSpec_Good_WithToolBridge` — Full pipeline: tools → bridge → spec → export
|
||||
|
||||
## Wave 2: MCP-to-REST Bridge (go-ai)
|
||||
|
||||
### Task 6: Tool registry in go-ai Service
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/go-ai/mcp/mcp.go` — Add ToolRecord and registry
|
||||
- Create: `/Users/snider/Code/go-ai/mcp/registry.go` — ToolRecord type and helpers
|
||||
- Create: `/Users/snider/Code/go-ai/mcp/registry_test.go`
|
||||
|
||||
The MCP SDK's `mcp.Server.tools` is unexported. We maintain a parallel registry in `Service` that records tool metadata (name, description, input/output Go types) as tools are registered. This becomes the source of truth for both MCP and REST.
|
||||
|
||||
```go
|
||||
// ToolRecord captures metadata about a registered MCP tool.
|
||||
type ToolRecord struct {
|
||||
Name string
|
||||
Description string
|
||||
Group string // Subsystem group name
|
||||
InputSchema map[string]any // JSON Schema from Go struct tags
|
||||
OutputSchema map[string]any
|
||||
Handler any // The original handler func for type assertion
|
||||
}
|
||||
|
||||
// Tools returns all recorded tool metadata.
|
||||
func (s *Service) Tools() []ToolRecord
|
||||
```
|
||||
|
||||
Modify `registerTools` and subsystem registration to also record tools. Use `jsonschema-go` (already a transitive dep from MCP SDK) to extract schemas from the typed Input/Output parameters.
|
||||
|
||||
**Tests (5):**
|
||||
- `TestToolRegistry_Good_RecordsTools` — After New(), Tools() returns all registered tools
|
||||
- `TestToolRegistry_Good_IncludesSubsystems` — Subsystem tools included in registry
|
||||
- `TestToolRegistry_Good_SchemaExtraction` — InputSchema contains correct properties from struct tags
|
||||
- `TestToolRegistry_Good_ToolCount` — Count matches expected (10 built-in + subsystem tools)
|
||||
- `TestToolRegistry_Bad_EmptyService` — Service with no subsystems has only built-in tools
|
||||
|
||||
### Task 7: BridgeToAPI — MCP tools to go-api ToolBridge
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/go-ai/mcp/bridge.go`
|
||||
- Create: `/Users/snider/Code/go-ai/mcp/bridge_test.go`
|
||||
|
||||
Converts MCP tool records into go-api ToolBridge entries. Each tool becomes a POST endpoint. The handler unmarshals JSON from request body, calls the business logic, and wraps the response in `api.OK()`/`api.Fail()`.
|
||||
|
||||
```go
|
||||
// BridgeToAPI populates a go-api ToolBridge from recorded MCP tools.
|
||||
// Each tool becomes a POST endpoint at /{tool_name}.
|
||||
func BridgeToAPI(svc *Service, bridge *api.ToolBridge) error
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestBridgeToAPI_Good_FileRead` — Bridge file_read, POST JSON body, get Response[ReadFileOutput]
|
||||
- `TestBridgeToAPI_Good_AllTools` — All tools from registry appear in bridge
|
||||
- `TestBridgeToAPI_Good_DescribableGroup` — Bridge implements DescribableGroup, Describe() returns correct metadata
|
||||
- `TestBridgeToAPI_Bad_InvalidJSON` — POST malformed JSON returns 400 with error envelope
|
||||
- `TestBridgeToAPI_Good_EndToEnd` — New service → BridgeToAPI → register with Engine → GET /swagger/doc.json shows all tools
|
||||
|
||||
## Wave 3: SDK Codegen + CLI
|
||||
|
||||
### Task 8: Codegen wrapper
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/go-api/codegen.go`
|
||||
- Create: `/Users/snider/Code/go-api/codegen_test.go`
|
||||
|
||||
Wraps `openapi-generator-cli` for multi-language SDK generation. Checks if the tool is available and provides clear installation instructions if missing.
|
||||
|
||||
```go
|
||||
// SDKGenerator wraps openapi-generator-cli for SDK generation.
|
||||
type SDKGenerator struct {
|
||||
SpecPath string
|
||||
OutputDir string
|
||||
PackageName string
|
||||
}
|
||||
|
||||
// Generate creates an SDK for the given language.
|
||||
// Supported: "go", "typescript-fetch", "python", "java", "csharp"
|
||||
func (g *SDKGenerator) Generate(ctx context.Context, language string) error
|
||||
|
||||
// Available checks if openapi-generator-cli is installed.
|
||||
func (g *SDKGenerator) Available() bool
|
||||
|
||||
// SupportedLanguages returns the list of supported SDK languages.
|
||||
func SupportedLanguages() []string
|
||||
```
|
||||
|
||||
**Tests (5):**
|
||||
- `TestSDKGenerator_Good_CommandConstruction` — Verify correct CLI arguments without executing
|
||||
- `TestSDKGenerator_Good_SupportedLanguages` — Returns expected language list
|
||||
- `TestSDKGenerator_Bad_UnsupportedLanguage` — Returns error for "brainfuck"
|
||||
- `TestSDKGenerator_Bad_MissingSpec` — Returns error when spec file doesn't exist
|
||||
- `TestSDKGenerator_Good_OutputDirCreated` — Creates output directory if missing
|
||||
|
||||
### Task 9: CLI commands — `core api spec` and `core api sdk`
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/host-uk/core/cmd/core-cli/api.go` (or appropriate location in CLI tree)
|
||||
- Create test file alongside
|
||||
|
||||
New `api` command group in the core CLI:
|
||||
|
||||
```
|
||||
core api spec # Export OpenAPI spec JSON to stdout
|
||||
core api spec --output spec.json # Write to file
|
||||
core api spec --format yaml # YAML format
|
||||
|
||||
core api sdk --lang go # Generate Go SDK
|
||||
core api sdk --lang typescript-fetch,python --output ./sdk/
|
||||
core api sdk --spec spec.json --lang go --output ./sdk/go
|
||||
```
|
||||
|
||||
This depends on understanding the CLI registration pattern. The commands create an MCP service, bridge tools, build spec, and either export or generate SDKs.
|
||||
|
||||
**Tests (4):**
|
||||
- `TestAPISpecCmd_Good_JSON` — Outputs valid JSON to stdout
|
||||
- `TestAPISpecCmd_Good_YAML` — Outputs valid YAML with --format yaml
|
||||
- `TestAPISDKCmd_Bad_NoLang` — Returns error without --lang flag
|
||||
- `TestAPISDKCmd_Good_ValidatesLanguage` — Rejects unsupported language with helpful message
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks are complete:
|
||||
|
||||
1. **Unit tests**: `cd /Users/snider/Code/go-api && go test ./...` — all pass
|
||||
2. **go-ai tests**: `cd /Users/snider/Code/go-ai && go test ./...` — all pass
|
||||
3. **Spec roundtrip**: Register a ToolBridge with sample tools, build Engine with WithSwagger, GET `/swagger/doc.json` → verify paths are populated, not empty
|
||||
4. **Export**: `ExportSpec` to temp file in JSON and YAML, verify both parse correctly
|
||||
5. **SDK smoke test** (if openapi-generator-cli installed): Generate Go client from exported spec, verify it compiles
|
||||
|
||||
## Dependency Sequencing
|
||||
|
||||
```
|
||||
Task 1 (DescribableGroup) ← Task 2 (ToolBridge) ← Task 3 (SpecBuilder)
|
||||
Task 3 ← Task 4 (swagger refactor)
|
||||
Task 3 ← Task 5 (ExportSpec)
|
||||
Task 2 ← Task 6 (Tool registry) ← Task 7 (BridgeToAPI)
|
||||
Task 5 ← Task 8 (Codegen)
|
||||
Task 5 + Task 7 ← Task 9 (CLI commands)
|
||||
```
|
||||
|
||||
Wave 1 (Tasks 1-5) is self-contained in go-api. Wave 2 (Tasks 6-7) bridges go-ai. Wave 3 (Tasks 8-9) adds codegen and CLI.
|
||||
20
options.go
20
options.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"compress/gzip"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
|
@ -63,11 +64,8 @@ func WithCORS(allowOrigins ...string) Option {
|
|||
MaxAge: 12 * time.Hour,
|
||||
}
|
||||
|
||||
for _, o := range allowOrigins {
|
||||
if o == "*" {
|
||||
cfg.AllowAllOrigins = true
|
||||
break
|
||||
}
|
||||
if slices.Contains(allowOrigins, "*") {
|
||||
cfg.AllowAllOrigins = true
|
||||
}
|
||||
if !cfg.AllowAllOrigins {
|
||||
cfg.AllowOrigins = allowOrigins
|
||||
|
|
@ -156,12 +154,12 @@ func WithExpvar() Option {
|
|||
func WithSecure() Option {
|
||||
return func(e *Engine) {
|
||||
e.middlewares = append(e.middlewares, secure.New(secure.Config{
|
||||
STSSeconds: 31536000,
|
||||
STSIncludeSubdomains: true,
|
||||
FrameDeny: true,
|
||||
ContentTypeNosniff: true,
|
||||
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||
IsDevelopment: false,
|
||||
STSSeconds: 31536000,
|
||||
STSIncludeSubdomains: true,
|
||||
FrameDeny: true,
|
||||
ContentTypeNosniff: true,
|
||||
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||
IsDevelopment: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
sse_test.go
16
sse_test.go
|
|
@ -81,11 +81,11 @@ func TestWithSSE_Good_ReceivesPublishedEvent(t *testing.T) {
|
|||
defer close(done)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "event: ") {
|
||||
eventLine = strings.TrimPrefix(line, "event: ")
|
||||
if after, ok := strings.CutPrefix(line, "event: "); ok {
|
||||
eventLine = after
|
||||
}
|
||||
if strings.HasPrefix(line, "data: ") {
|
||||
dataLine = strings.TrimPrefix(line, "data: ")
|
||||
if after, ok := strings.CutPrefix(line, "data: "); ok {
|
||||
dataLine = after
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -144,8 +144,8 @@ func TestWithSSE_Good_ChannelFiltering(t *testing.T) {
|
|||
defer close(done)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "event: ") {
|
||||
eventLine = strings.TrimPrefix(line, "event: ")
|
||||
if after, ok := strings.CutPrefix(line, "event: "); ok {
|
||||
eventLine = after
|
||||
// Read past the data and blank line.
|
||||
scanner.Scan() // data line
|
||||
return
|
||||
|
|
@ -246,8 +246,8 @@ func TestWithSSE_Good_MultipleClients(t *testing.T) {
|
|||
go func() {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "event: ") {
|
||||
done <- strings.TrimPrefix(line, "event: ")
|
||||
if after, ok := strings.CutPrefix(line, "event: "); ok {
|
||||
done <- after
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue