feat: complete AX pass — cmd/ rewrite, string concat, proxy, bridge
- cmd/api: rewrite from Cobra CLI → Core command tree (c.Command) - cmd/api/cmd_spec.go: uses SpecBuilder.Build → core.Result - cmd/api/cmd_sdk.go: uses c.Process() for openapi-generator, core.Fs - bridge.go: string concat → core.Concat - graphql.go: string concat → core.Concat - openapi.go: string concat → core.Concat - proxy.go: strings → core.TrimPrefix/TrimSuffix, panic concat → core.Concat - Zero disallowed imports, zero string concat, zero old paths Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
de63217168
commit
bca6e2c4cb
7 changed files with 86 additions and 105 deletions
|
|
@ -5,6 +5,7 @@ package api
|
|||
import (
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ func (b *ToolBridge) Describe() []RouteDescription {
|
|||
}
|
||||
descs = append(descs, RouteDescription{
|
||||
Method: "POST",
|
||||
Path: "/" + t.descriptor.Name,
|
||||
Path: core.Concat("/", t.descriptor.Name),
|
||||
Summary: t.descriptor.Description,
|
||||
Description: t.descriptor.Description,
|
||||
Tags: tags,
|
||||
|
|
@ -87,7 +88,7 @@ func (b *ToolBridge) DescribeIter() iter.Seq[RouteDescription] {
|
|||
}
|
||||
rd := RouteDescription{
|
||||
Method: "POST",
|
||||
Path: "/" + t.descriptor.Name,
|
||||
Path: core.Concat("/", t.descriptor.Name),
|
||||
Summary: t.descriptor.Description,
|
||||
Description: t.descriptor.Description,
|
||||
Tags: tags,
|
||||
|
|
|
|||
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
package api
|
||||
|
||||
import "forge.lthn.ai/core/cli/pkg/cli"
|
||||
import core "dappco.re/go/core"
|
||||
|
||||
func init() {
|
||||
cli.RegisterCommands(AddAPICommands)
|
||||
}
|
||||
|
||||
// AddAPICommands registers the 'api' command group.
|
||||
func AddAPICommands(root *cli.Command) {
|
||||
apiCmd := cli.NewGroup("api", "API specification and SDK generation", "")
|
||||
root.AddCommand(apiCmd)
|
||||
|
||||
addSpecCommand(apiCmd)
|
||||
addSDKCommand(apiCmd)
|
||||
// RegisterCommands adds API commands to Core's command tree.
|
||||
//
|
||||
// api.RegisterCommands(c)
|
||||
func RegisterCommands(c *core.Core) {
|
||||
c.Command("api/spec", core.Command{
|
||||
Description: "Generate OpenAPI specification",
|
||||
Action: cmdSpec(c),
|
||||
})
|
||||
c.Command("api/sdk", core.Command{
|
||||
Description: "Generate client SDKs from OpenAPI spec",
|
||||
Action: cmdSDK(c),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,54 +4,46 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
goapi "dappco.re/go/core/api"
|
||||
)
|
||||
|
||||
func addSDKCommand(parent *cli.Command) {
|
||||
var (
|
||||
lang string
|
||||
output string
|
||||
specFile string
|
||||
packageName string
|
||||
)
|
||||
|
||||
cmd := cli.NewCommand("sdk", "Generate client SDKs from OpenAPI spec", "", func(cmd *cli.Command, args []string) error {
|
||||
func cmdSDK(c *core.Core) core.CommandAction {
|
||||
return func(opts core.Options) core.Result {
|
||||
lang := opts.String("lang")
|
||||
if lang == "" {
|
||||
return coreerr.E("sdk.Generate", "--lang is required. Supported: "+strings.Join(goapi.SupportedLanguages(), ", "), nil)
|
||||
core.Print(nil, "usage: api sdk --lang=go,python [--output=./sdk] [--spec=openapi.json]")
|
||||
core.Print(nil, "supported: %s", core.Join(", ", goapi.SupportedLanguages()...))
|
||||
return core.Result{OK: false}
|
||||
}
|
||||
|
||||
// If no spec file provided, generate one to a temp file.
|
||||
output := opts.String("output")
|
||||
if output == "" {
|
||||
output = "./sdk"
|
||||
}
|
||||
specFile := opts.String("spec")
|
||||
packageName := opts.String("package")
|
||||
if packageName == "" {
|
||||
packageName = "lethean"
|
||||
}
|
||||
|
||||
// If no spec file, generate one to temp
|
||||
if specFile == "" {
|
||||
builder := &goapi.SpecBuilder{
|
||||
Title: "Lethean Core API",
|
||||
Description: "Lethean Core API",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
bridge := goapi.NewToolBridge("/tools")
|
||||
groups := []goapi.RouteGroup{bridge}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "openapi-*.json")
|
||||
if err != nil {
|
||||
return coreerr.E("sdk.Generate", "create temp spec file", err)
|
||||
tmpPath := core.JoinPath(c.Fs().TempDir("openapi"), "spec.json")
|
||||
r := goapi.ExportSpec(tmpPath, "json", builder, groups)
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
defer coreio.Local.Delete(tmpFile.Name())
|
||||
|
||||
if err := goapi.ExportSpec(tmpFile, "json", builder, groups); err != nil {
|
||||
tmpFile.Close()
|
||||
return coreerr.E("sdk.Generate", "generate spec", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
specFile = tmpFile.Name()
|
||||
specFile = tmpPath
|
||||
}
|
||||
|
||||
gen := &goapi.SDKGenerator{
|
||||
|
|
@ -61,32 +53,25 @@ func addSDKCommand(parent *cli.Command) {
|
|||
}
|
||||
|
||||
if !gen.Available() {
|
||||
fmt.Fprintln(os.Stderr, "openapi-generator-cli not found. Install with:")
|
||||
fmt.Fprintln(os.Stderr, " brew install openapi-generator (macOS)")
|
||||
fmt.Fprintln(os.Stderr, " npm install @openapitools/openapi-generator-cli -g")
|
||||
return coreerr.E("sdk.Generate", "openapi-generator-cli not installed", nil)
|
||||
core.Print(nil, "openapi-generator-cli not found. Install with:")
|
||||
core.Print(nil, " brew install openapi-generator (macOS)")
|
||||
core.Print(nil, " npm install @openapitools/openapi-generator-cli -g")
|
||||
return core.Result{OK: false}
|
||||
}
|
||||
|
||||
// Generate for each language.
|
||||
for l := range strings.SplitSeq(lang, ",") {
|
||||
l = strings.TrimSpace(l)
|
||||
for _, l := range core.Split(lang, ",") {
|
||||
l = core.Trim(l)
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Generating %s SDK...\n", l)
|
||||
if err := gen.Generate(context.Background(), l); err != nil {
|
||||
return coreerr.E("sdk.Generate", "generate "+l, err)
|
||||
core.Print(nil, "generating %s SDK...", l)
|
||||
r := gen.Generate(context.Background(), c, l)
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " Done: %s/%s/\n", output, l)
|
||||
core.Print(nil, " done: %s/%s/", output, l)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
cli.StringFlag(cmd, &lang, "lang", "l", "", "Target language(s), comma-separated (e.g. go,python,typescript-fetch)")
|
||||
cli.StringFlag(cmd, &output, "output", "o", "./sdk", "Output directory for generated SDKs")
|
||||
cli.StringFlag(cmd, &specFile, "spec", "s", "", "Path to existing OpenAPI spec (generates from MCP tools if not provided)")
|
||||
cli.StringFlag(cmd, &packageName, "package", "p", "lethean", "Package name for generated SDK")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,52 +3,45 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
goapi "dappco.re/go/core/api"
|
||||
)
|
||||
|
||||
func addSpecCommand(parent *cli.Command) {
|
||||
var (
|
||||
output string
|
||||
format string
|
||||
title string
|
||||
version string
|
||||
)
|
||||
func cmdSpec(c *core.Core) core.CommandAction {
|
||||
return func(opts core.Options) core.Result {
|
||||
format := opts.String("format")
|
||||
if format == "" {
|
||||
format = "json"
|
||||
}
|
||||
output := opts.String("output")
|
||||
title := opts.String("title")
|
||||
if title == "" {
|
||||
title = "Lethean Core API"
|
||||
}
|
||||
ver := opts.String("version")
|
||||
if ver == "" {
|
||||
ver = "1.0.0"
|
||||
}
|
||||
|
||||
cmd := cli.NewCommand("spec", "Generate OpenAPI specification", "", func(cmd *cli.Command, args []string) error {
|
||||
// Build spec from registered route groups.
|
||||
// Additional groups can be added here as the platform grows.
|
||||
builder := &goapi.SpecBuilder{
|
||||
Title: title,
|
||||
Description: "Lethean Core API",
|
||||
Version: version,
|
||||
Version: ver,
|
||||
}
|
||||
|
||||
// Start with the default tool bridge — future versions will
|
||||
// auto-populate from the MCP tool registry once the bridge
|
||||
// integration lands in the local go-ai module.
|
||||
bridge := goapi.NewToolBridge("/tools")
|
||||
groups := []goapi.RouteGroup{bridge}
|
||||
|
||||
if output != "" {
|
||||
if err := goapi.ExportSpecToFile(output, format, builder, groups); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Spec written to %s\n", output)
|
||||
return nil
|
||||
return goapi.ExportSpec(output, format, builder, groups)
|
||||
}
|
||||
|
||||
return goapi.ExportSpec(os.Stdout, format, builder, groups)
|
||||
})
|
||||
|
||||
cli.StringFlag(cmd, &output, "output", "o", "", "Write spec to file instead of stdout")
|
||||
cli.StringFlag(cmd, &format, "format", "f", "json", "Output format: json or yaml")
|
||||
cli.StringFlag(cmd, &title, "title", "t", "Lethean Core API", "API title in spec")
|
||||
cli.StringFlag(cmd, &version, "version", "V", "1.0.0", "API version in spec")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
// No output file — build and print to stdout
|
||||
r := builder.Build(groups)
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
core.Println(string(r.Value.([]byte)))
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
|
|
@ -49,7 +50,7 @@ func mountGraphQL(r *gin.Engine, cfg *graphqlConfig) {
|
|||
r.Any(cfg.path, graphqlHandler)
|
||||
|
||||
if cfg.playground {
|
||||
playgroundPath := cfg.path + "/playground"
|
||||
playgroundPath := core.Concat(cfg.path, "/playground")
|
||||
playgroundHandler := playground.Handler("GraphQL", cfg.path)
|
||||
r.GET(playgroundPath, wrapHTTPHandler(playgroundHandler))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
if !seen[name] {
|
||||
tags = append(tags, map[string]any{
|
||||
"name": name,
|
||||
"description": name + " endpoints",
|
||||
"description": core.Concat(name, " endpoints"),
|
||||
})
|
||||
seen[name] = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ type ProxyProvider struct {
|
|||
func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
||||
target, err := url.Parse(cfg.Upstream)
|
||||
if err != nil {
|
||||
panic("provider.NewProxy: invalid upstream URL: " + err.Error())
|
||||
panic(core.Concat("provider.NewProxy: invalid upstream URL: ", err.Error()))
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
|
|
@ -54,16 +54,16 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
|||
// Preserve the original Director but strip the base path so the
|
||||
// upstream receives clean paths (e.g. /items instead of /api/v1/cool-widget/items).
|
||||
defaultDirector := proxy.Director
|
||||
basePath := strings.TrimSuffix(cfg.BasePath, "/")
|
||||
basePath := core.TrimSuffix(cfg.BasePath, "/")
|
||||
|
||||
proxy.Director = func(req *http.Request) {
|
||||
defaultDirector(req)
|
||||
// Strip the base path prefix from the request path.
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, basePath)
|
||||
req.URL.Path = core.TrimPrefix(req.URL.Path, basePath)
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path = "/"
|
||||
}
|
||||
req.URL.RawPath = strings.TrimPrefix(req.URL.RawPath, basePath)
|
||||
req.URL.RawPath = core.TrimPrefix(req.URL.RawPath, basePath)
|
||||
}
|
||||
|
||||
return &ProxyProvider{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue