feat(openapi): support iterator-backed route descriptions
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7e4d8eb179
commit
eceda4e5c1
3 changed files with 103 additions and 7 deletions
14
group.go
14
group.go
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
package api
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RouteGroup registers API routes onto a Gin router group.
|
||||
// Subsystems implement this interface to declare their endpoints.
|
||||
|
|
@ -32,6 +36,14 @@ type DescribableGroup interface {
|
|||
Describe() []RouteDescription
|
||||
}
|
||||
|
||||
// DescribableGroupIter extends DescribableGroup with an iterator-based
|
||||
// description source for callers that want to avoid slice allocation.
|
||||
type DescribableGroupIter interface {
|
||||
DescribableGroup
|
||||
// DescribeIter returns endpoint descriptions for OpenAPI generation.
|
||||
DescribeIter() iter.Seq[RouteDescription]
|
||||
}
|
||||
|
||||
// RouteDescription describes a single endpoint for OpenAPI generation.
|
||||
type RouteDescription struct {
|
||||
Method string // HTTP method: GET, POST, PUT, DELETE, PATCH
|
||||
|
|
|
|||
33
openapi.go
33
openapi.go
|
|
@ -4,6 +4,7 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -169,11 +170,11 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
}
|
||||
|
||||
for _, g := range groups {
|
||||
dg, ok := g.(DescribableGroup)
|
||||
if !ok {
|
||||
descIter := routeDescriptions(g)
|
||||
if descIter == nil {
|
||||
continue
|
||||
}
|
||||
for _, rd := range dg.Describe() {
|
||||
for rd := range descIter {
|
||||
fullPath := joinOpenAPIPath(g.BasePath(), rd.Path)
|
||||
method := strings.ToLower(rd.Method)
|
||||
|
||||
|
|
@ -483,12 +484,12 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
seen[name] = true
|
||||
}
|
||||
|
||||
dg, ok := g.(DescribableGroup)
|
||||
if !ok {
|
||||
descIter := routeDescriptions(g)
|
||||
if descIter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rd := range dg.Describe() {
|
||||
for rd := range descIter {
|
||||
for _, tag := range rd.Tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" || seen[tag] {
|
||||
|
|
@ -506,6 +507,26 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
return tags
|
||||
}
|
||||
|
||||
// routeDescriptions returns OpenAPI route descriptions for a group.
|
||||
// Iterator-backed implementations are preferred when available so builders
|
||||
// can avoid slice allocation.
|
||||
func routeDescriptions(g RouteGroup) iter.Seq[RouteDescription] {
|
||||
if dg, ok := g.(DescribableGroupIter); ok {
|
||||
return dg.DescribeIter()
|
||||
}
|
||||
if dg, ok := g.(DescribableGroup); ok {
|
||||
descs := dg.Describe()
|
||||
return func(yield func(RouteDescription) bool) {
|
||||
for _, rd := range descs {
|
||||
if !yield(rd) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathParameters extracts unique OpenAPI path parameters from a path template.
|
||||
// Parameters are returned in the order they appear in the path.
|
||||
func pathParameters(path string) []map[string]any {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package api_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
|
|
@ -31,6 +32,26 @@ func (plainStubGroup) Name() string { return "plain" }
|
|||
func (plainStubGroup) BasePath() string { return "/plain" }
|
||||
func (plainStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
|
||||
type iterStubGroup struct {
|
||||
name string
|
||||
basePath string
|
||||
descs []api.RouteDescription
|
||||
}
|
||||
|
||||
func (s *iterStubGroup) Name() string { return s.name }
|
||||
func (s *iterStubGroup) BasePath() string { return s.basePath }
|
||||
func (s *iterStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (s *iterStubGroup) Describe() []api.RouteDescription { return nil }
|
||||
func (s *iterStubGroup) DescribeIter() iter.Seq[api.RouteDescription] {
|
||||
return func(yield func(api.RouteDescription) bool) {
|
||||
for _, rd := range s.descs {
|
||||
if !yield(rd) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SpecBuilder tests ─────────────────────────────────────────────────────
|
||||
|
||||
func TestSpecBuilder_Good_EmptyGroups(t *testing.T) {
|
||||
|
|
@ -380,6 +401,48 @@ func TestSpecBuilder_Good_WithDescribableGroup(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_DescribeIterGroup(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &iterStubGroup{
|
||||
name: "iter",
|
||||
basePath: "/api/iter",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/status",
|
||||
Summary: "Iter status",
|
||||
Tags: []string{"iter"},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{group})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
op := spec["paths"].(map[string]any)["/api/iter/status"].(map[string]any)["get"].(map[string]any)
|
||||
if op["summary"] != "Iter status" {
|
||||
t.Fatalf("expected summary='Iter status', got %v", op["summary"])
|
||||
}
|
||||
tags, ok := op["tags"].([]any)
|
||||
if !ok || len(tags) != 1 || tags[0] != "iter" {
|
||||
t.Fatalf("expected tags to be populated from DescribeIter, got %v", op["tags"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_SecuredResponses(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue