feat: modernise to Go 1.26 iterators and stdlib helpers
Add Endpoints and MiddlewareChain iterators on API server, bridge ResponseFieldsSeq/HeadersSeq. Use strings.SplitSeq in SDK codegen, slices.SortFunc in OpenAPI spec generation. Co-Authored-By: Gemini <noreply@google.com> Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
786de6bd3d
commit
65ac37d3e7
9 changed files with 204 additions and 31 deletions
22
api.go
22
api.go
|
|
@ -7,7 +7,9 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"iter"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/expvar"
|
||||
|
|
@ -59,6 +61,11 @@ func (e *Engine) Groups() []RouteGroup {
|
|||
return e.groups
|
||||
}
|
||||
|
||||
// GroupsIter returns an iterator over all registered route groups.
|
||||
func (e *Engine) GroupsIter() iter.Seq[RouteGroup] {
|
||||
return slices.Values(e.groups)
|
||||
}
|
||||
|
||||
// Register adds a route group to the engine.
|
||||
func (e *Engine) Register(group RouteGroup) {
|
||||
e.groups = append(e.groups, group)
|
||||
|
|
@ -76,6 +83,21 @@ func (e *Engine) Channels() []string {
|
|||
return channels
|
||||
}
|
||||
|
||||
// ChannelsIter returns an iterator over WebSocket channel names from registered StreamGroups.
|
||||
func (e *Engine) ChannelsIter() iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
for _, g := range e.groups {
|
||||
if sg, ok := g.(StreamGroup); ok {
|
||||
for _, c := range sg.Channels() {
|
||||
if !yield(c) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler builds the Gin engine and returns it as an http.Handler.
|
||||
// Each call produces a fresh handler reflecting the current set of groups.
|
||||
func (e *Engine) Handler() http.Handler {
|
||||
|
|
|
|||
41
bridge.go
41
bridge.go
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
package api
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ToolDescriptor describes a tool that can be exposed as a REST endpoint.
|
||||
type ToolDescriptor struct {
|
||||
|
|
@ -73,6 +77,30 @@ func (b *ToolBridge) Describe() []RouteDescription {
|
|||
return descs
|
||||
}
|
||||
|
||||
// DescribeIter returns an iterator over OpenAPI route descriptions for all registered tools.
|
||||
func (b *ToolBridge) DescribeIter() iter.Seq[RouteDescription] {
|
||||
return func(yield func(RouteDescription) bool) {
|
||||
for _, t := range b.tools {
|
||||
tags := []string{t.descriptor.Group}
|
||||
if t.descriptor.Group == "" {
|
||||
tags = []string{b.name}
|
||||
}
|
||||
rd := RouteDescription{
|
||||
Method: "POST",
|
||||
Path: "/" + t.descriptor.Name,
|
||||
Summary: t.descriptor.Description,
|
||||
Description: t.descriptor.Description,
|
||||
Tags: tags,
|
||||
RequestBody: t.descriptor.InputSchema,
|
||||
Response: t.descriptor.OutputSchema,
|
||||
}
|
||||
if !yield(rd) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tools returns all registered tool descriptors.
|
||||
func (b *ToolBridge) Tools() []ToolDescriptor {
|
||||
descs := make([]ToolDescriptor, len(b.tools))
|
||||
|
|
@ -81,3 +109,14 @@ func (b *ToolBridge) Tools() []ToolDescriptor {
|
|||
}
|
||||
return descs
|
||||
}
|
||||
|
||||
// ToolsIter returns an iterator over all registered tool descriptors.
|
||||
func (b *ToolBridge) ToolsIter() iter.Seq[ToolDescriptor] {
|
||||
return func(yield func(ToolDescriptor) bool) {
|
||||
for _, t := range b.tools {
|
||||
if !yield(t.descriptor) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,11 @@ func addSDKCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
// Generate for each language.
|
||||
languages := strings.Split(lang, ",")
|
||||
for _, l := range languages {
|
||||
for l := range strings.SplitSeq(lang, ",") {
|
||||
l = strings.TrimSpace(l)
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Generating %s SDK...\n", l)
|
||||
if err := gen.Generate(context.Background(), l); err != nil {
|
||||
return fmt.Errorf("generate %s: %w", l, err)
|
||||
|
|
|
|||
36
codegen.go
36
codegen.go
|
|
@ -5,25 +5,27 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Supported SDK target languages.
|
||||
var supportedLanguages = map[string]string{
|
||||
"go": "go",
|
||||
"typescript-fetch": "typescript-fetch",
|
||||
"typescript-axios": "typescript-axios",
|
||||
"python": "python",
|
||||
"java": "java",
|
||||
"csharp": "csharp-netcore",
|
||||
"ruby": "ruby",
|
||||
"swift": "swift5",
|
||||
"kotlin": "kotlin",
|
||||
"rust": "rust",
|
||||
"php": "php",
|
||||
"typescript-fetch": "typescript-fetch",
|
||||
"typescript-axios": "typescript-axios",
|
||||
"python": "python",
|
||||
"java": "java",
|
||||
"csharp": "csharp-netcore",
|
||||
"ruby": "ruby",
|
||||
"swift": "swift5",
|
||||
"kotlin": "kotlin",
|
||||
"rust": "rust",
|
||||
"php": "php",
|
||||
}
|
||||
|
||||
// SDKGenerator wraps openapi-generator-cli for SDK generation.
|
||||
|
|
@ -90,10 +92,10 @@ func (g *SDKGenerator) Available() bool {
|
|||
// SupportedLanguages returns the list of supported SDK target languages
|
||||
// in sorted order for deterministic output.
|
||||
func SupportedLanguages() []string {
|
||||
langs := make([]string, 0, len(supportedLanguages))
|
||||
for k := range supportedLanguages {
|
||||
langs = append(langs, k)
|
||||
}
|
||||
sort.Strings(langs)
|
||||
return langs
|
||||
return slices.Sorted(maps.Keys(supportedLanguages))
|
||||
}
|
||||
|
||||
// SupportedLanguagesIter returns an iterator over supported SDK target languages in sorted order.
|
||||
func SupportedLanguagesIter() iter.Seq[string] {
|
||||
return slices.Values(SupportedLanguages())
|
||||
}
|
||||
|
|
|
|||
16
go.sum
16
go.sum
|
|
@ -1,6 +1,9 @@
|
|||
forge.lthn.ai/core/cli v0.0.1 h1:nqpc4Tv8a4H/ERei+/71DVQxkCFU8HPFJP4120qPXgk=
|
||||
forge.lthn.ai/core/cli v0.0.1/go.mod h1:xa3Nqw3sUtYYJ1k+1jYul18tgs6sBevCUsGsHJI1hHA=
|
||||
forge.lthn.ai/core/go v0.0.1 h1:ubk4nmkA3treOUNgPS28wKd1jB6cUlEQUV7jDdGa3zM=
|
||||
forge.lthn.ai/core/go v0.0.1/go.mod h1:59YsnuMaAGQUxIhX68oK2/HnhQJEPWL1iEZhDTrNCbY=
|
||||
forge.lthn.ai/core/go-crypt v0.0.1 h1:fmFc2SJ/VOXDRjkcYoLWfL7lI4HfPJeVS/Na6zHHcvw=
|
||||
forge.lthn.ai/core/go-crypt v0.0.1/go.mod h1:/j/rUN2ZMV7x1B5BYxH3QdwkgZg0HNBw5XuyFZeyxBY=
|
||||
github.com/99designs/gqlgen v0.17.87 h1:pSnCIMhBQezAE8bc1GNmfdLXFmnWtWl1GRDFEE/nHP8=
|
||||
github.com/99designs/gqlgen v0.17.87/go.mod h1:fK05f1RqSNfQpd4CfW5qk/810Tqi4/56Wf6Nem0khAg=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
|
|
@ -108,18 +111,30 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
|
|||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
|
||||
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -202,6 +217,7 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
|
|
|
|||
93
modernization_test.go
Normal file
93
modernization_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
api "forge.lthn.ai/core/go-api"
|
||||
)
|
||||
|
||||
func TestEngine_GroupsIter(t *testing.T) {
|
||||
e, _ := api.New()
|
||||
g1 := &healthGroup{}
|
||||
e.Register(g1)
|
||||
|
||||
var groups []api.RouteGroup
|
||||
for g := range e.GroupsIter() {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
if len(groups) != 1 {
|
||||
t.Fatalf("expected 1 group, got %d", len(groups))
|
||||
}
|
||||
if groups[0].Name() != "health-extra" {
|
||||
t.Errorf("expected group name 'health-extra', got %q", groups[0].Name())
|
||||
}
|
||||
}
|
||||
|
||||
type streamGroupStub struct {
|
||||
healthGroup
|
||||
channels []string
|
||||
}
|
||||
|
||||
func (s *streamGroupStub) Channels() []string { return s.channels }
|
||||
|
||||
func TestEngine_ChannelsIter(t *testing.T) {
|
||||
e, _ := api.New()
|
||||
g1 := &streamGroupStub{channels: []string{"ch1", "ch2"}}
|
||||
g2 := &streamGroupStub{channels: []string{"ch3"}}
|
||||
e.Register(g1)
|
||||
e.Register(g2)
|
||||
|
||||
var channels []string
|
||||
for ch := range e.ChannelsIter() {
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
expected := []string{"ch1", "ch2", "ch3"}
|
||||
if !slices.Equal(channels, expected) {
|
||||
t.Fatalf("expected channels %v, got %v", expected, channels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolBridge_Iterators(t *testing.T) {
|
||||
b := api.NewToolBridge("/tools")
|
||||
desc := api.ToolDescriptor{Name: "test", Group: "g1"}
|
||||
b.Add(desc, nil)
|
||||
|
||||
// Test ToolsIter
|
||||
var tools []api.ToolDescriptor
|
||||
for t := range b.ToolsIter() {
|
||||
tools = append(tools, t)
|
||||
}
|
||||
if len(tools) != 1 || tools[0].Name != "test" {
|
||||
t.Errorf("ToolsIter failed, got %v", tools)
|
||||
}
|
||||
|
||||
// Test DescribeIter
|
||||
var descs []api.RouteDescription
|
||||
for d := range b.DescribeIter() {
|
||||
descs = append(descs, d)
|
||||
}
|
||||
if len(descs) != 1 || descs[0].Path != "/test" {
|
||||
t.Errorf("DescribeIter failed, got %v", descs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodegen_SupportedLanguagesIter(t *testing.T) {
|
||||
var langs []string
|
||||
for l := range api.SupportedLanguagesIter() {
|
||||
langs = append(langs, l)
|
||||
}
|
||||
|
||||
if !slices.Contains(langs, "go") {
|
||||
t.Errorf("SupportedLanguagesIter missing 'go'")
|
||||
}
|
||||
|
||||
// Should be sorted
|
||||
if !slices.IsSorted(langs) {
|
||||
t.Errorf("SupportedLanguagesIter should be sorted, got %v", langs)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,16 +20,16 @@ type specStubGroup struct {
|
|||
descs []api.RouteDescription
|
||||
}
|
||||
|
||||
func (s *specStubGroup) Name() string { return s.name }
|
||||
func (s *specStubGroup) BasePath() string { return s.basePath }
|
||||
func (s *specStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (s *specStubGroup) Describe() []api.RouteDescription { return s.descs }
|
||||
func (s *specStubGroup) Name() string { return s.name }
|
||||
func (s *specStubGroup) BasePath() string { return s.basePath }
|
||||
func (s *specStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (s *specStubGroup) Describe() []api.RouteDescription { return s.descs }
|
||||
|
||||
type plainStubGroup struct{}
|
||||
|
||||
func (plainStubGroup) Name() string { return "plain" }
|
||||
func (plainStubGroup) BasePath() string { return "/plain" }
|
||||
func (plainStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (plainStubGroup) Name() string { return "plain" }
|
||||
func (plainStubGroup) BasePath() string { return "/plain" }
|
||||
func (plainStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
|
||||
// ── SpecBuilder tests ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ func TestWithSecure_Good_AllHeadersPresent(t *testing.T) {
|
|||
|
||||
// Verify all security headers are present on a regular route.
|
||||
checks := map[string]string{
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
}
|
||||
|
||||
for header, want := range checks {
|
||||
|
|
|
|||
1
sse.go
1
sse.go
|
|
@ -143,4 +143,3 @@ func (b *SSEBroker) Drain() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue