Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65b48bde19 | ||
|
|
bca6e2c4cb | ||
|
|
de63217168 |
17 changed files with 169 additions and 221 deletions
5
api.go
5
api.go
|
|
@ -6,12 +6,13 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"iter"
|
"iter"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
|
||||||
"github.com/gin-contrib/expvar"
|
"github.com/gin-contrib/expvar"
|
||||||
"github.com/gin-contrib/pprof"
|
"github.com/gin-contrib/pprof"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -114,7 +115,7 @@ func (e *Engine) Serve(ctx context.Context) error {
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := srv.ListenAndServe(); err != nil && !core.Is(err, http.ErrServerClosed) {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
}
|
}
|
||||||
close(errCh)
|
close(errCh)
|
||||||
|
|
|
||||||
12
authentik.go
12
authentik.go
|
|
@ -4,9 +4,9 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
core "dappco.re/go/core"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
|
@ -148,7 +148,7 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc {
|
||||||
// Skip public paths.
|
// Skip public paths.
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
for p := range public {
|
for p := range public {
|
||||||
if strings.HasPrefix(path, p) {
|
if core.HasPrefix(path, p) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -167,10 +167,10 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
if groups := c.GetHeader("X-authentik-groups"); groups != "" {
|
if groups := c.GetHeader("X-authentik-groups"); groups != "" {
|
||||||
user.Groups = strings.Split(groups, "|")
|
user.Groups = core.Split(groups, "|")
|
||||||
}
|
}
|
||||||
if ent := c.GetHeader("X-authentik-entitlements"); ent != "" {
|
if ent := c.GetHeader("X-authentik-entitlements"); ent != "" {
|
||||||
user.Entitlements = strings.Split(ent, "|")
|
user.Entitlements = core.Split(ent, "|")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set(authentikUserKey, user)
|
c.Set(authentikUserKey, user)
|
||||||
|
|
@ -180,8 +180,8 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc {
|
||||||
// Block 2: Attempt JWT validation for direct API clients.
|
// Block 2: Attempt JWT validation for direct API clients.
|
||||||
// Only when OIDC is configured and no user was extracted from headers.
|
// Only when OIDC is configured and no user was extracted from headers.
|
||||||
if cfg.Issuer != "" && cfg.ClientID != "" && GetUser(c) == nil {
|
if cfg.Issuer != "" && cfg.ClientID != "" && GetUser(c) == nil {
|
||||||
if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
if auth := c.GetHeader("Authorization"); core.HasPrefix(auth, "Bearer ") {
|
||||||
rawToken := strings.TrimPrefix(auth, "Bearer ")
|
rawToken := core.TrimPrefix(auth, "Bearer ")
|
||||||
if user, err := validateJWT(c.Request.Context(), cfg, rawToken); err == nil {
|
if user, err := validateJWT(c.Request.Context(), cfg, rawToken); err == nil {
|
||||||
c.Set(authentikUserKey, user)
|
c.Set(authentikUserKey, user)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"iter"
|
"iter"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,7 +67,7 @@ func (b *ToolBridge) Describe() []RouteDescription {
|
||||||
}
|
}
|
||||||
descs = append(descs, RouteDescription{
|
descs = append(descs, RouteDescription{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/" + t.descriptor.Name,
|
Path: core.Concat("/", t.descriptor.Name),
|
||||||
Summary: t.descriptor.Description,
|
Summary: t.descriptor.Description,
|
||||||
Description: t.descriptor.Description,
|
Description: t.descriptor.Description,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
|
@ -87,7 +88,7 @@ func (b *ToolBridge) DescribeIter() iter.Seq[RouteDescription] {
|
||||||
}
|
}
|
||||||
rd := RouteDescription{
|
rd := RouteDescription{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/" + t.descriptor.Name,
|
Path: core.Concat("/", t.descriptor.Name),
|
||||||
Summary: t.descriptor.Description,
|
Summary: t.descriptor.Description,
|
||||||
Description: t.descriptor.Description,
|
Description: t.descriptor.Description,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
core "dappco.re/go/core"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
|
|
@ -47,7 +47,7 @@ func newBrotliHandler(level int) *brotliHandler {
|
||||||
|
|
||||||
// Handle is the Gin middleware function that compresses responses with Brotli.
|
// Handle is the Gin middleware function that compresses responses with Brotli.
|
||||||
func (h *brotliHandler) Handle(c *gin.Context) {
|
func (h *brotliHandler) Handle(c *gin.Context) {
|
||||||
if !strings.Contains(c.Request.Header.Get("Accept-Encoding"), "br") {
|
if !core.Contains(c.Request.Header.Get("Accept-Encoding"), "br") {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "forge.lthn.ai/core/cli/pkg/cli"
|
import core "dappco.re/go/core"
|
||||||
|
|
||||||
func init() {
|
// RegisterCommands adds API commands to Core's command tree.
|
||||||
cli.RegisterCommands(AddAPICommands)
|
//
|
||||||
}
|
// api.RegisterCommands(c)
|
||||||
|
func RegisterCommands(c *core.Core) {
|
||||||
// AddAPICommands registers the 'api' command group.
|
c.Command("api/spec", core.Command{
|
||||||
func AddAPICommands(root *cli.Command) {
|
Description: "Generate OpenAPI specification",
|
||||||
apiCmd := cli.NewGroup("api", "API specification and SDK generation", "")
|
Action: cmdSpec(c),
|
||||||
root.AddCommand(apiCmd)
|
})
|
||||||
|
c.Command("api/sdk", core.Command{
|
||||||
addSpecCommand(apiCmd)
|
Description: "Generate client SDKs from OpenAPI spec",
|
||||||
addSDKCommand(apiCmd)
|
Action: cmdSDK(c),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,54 +4,46 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
goapi "dappco.re/go/core/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addSDKCommand(parent *cli.Command) {
|
func cmdSDK(c *core.Core) core.CommandAction {
|
||||||
var (
|
return func(opts core.Options) core.Result {
|
||||||
lang string
|
lang := opts.String("lang")
|
||||||
output string
|
|
||||||
specFile string
|
|
||||||
packageName string
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd := cli.NewCommand("sdk", "Generate client SDKs from OpenAPI spec", "", func(cmd *cli.Command, args []string) error {
|
|
||||||
if 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 == "" {
|
if specFile == "" {
|
||||||
builder := &goapi.SpecBuilder{
|
builder := &goapi.SpecBuilder{
|
||||||
Title: "Lethean Core API",
|
Title: "Lethean Core API",
|
||||||
Description: "Lethean Core API",
|
Description: "Lethean Core API",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge := goapi.NewToolBridge("/tools")
|
bridge := goapi.NewToolBridge("/tools")
|
||||||
groups := []goapi.RouteGroup{bridge}
|
groups := []goapi.RouteGroup{bridge}
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "openapi-*.json")
|
tmpPath := core.JoinPath(c.Fs().TempDir("openapi"), "spec.json")
|
||||||
if err != nil {
|
r := goapi.ExportSpec(tmpPath, "json", builder, groups)
|
||||||
return coreerr.E("sdk.Generate", "create temp spec file", err)
|
if !r.OK {
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
defer coreio.Local.Delete(tmpFile.Name())
|
specFile = tmpPath
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gen := &goapi.SDKGenerator{
|
gen := &goapi.SDKGenerator{
|
||||||
|
|
@ -61,32 +53,25 @@ func addSDKCommand(parent *cli.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gen.Available() {
|
if !gen.Available() {
|
||||||
fmt.Fprintln(os.Stderr, "openapi-generator-cli not found. Install with:")
|
core.Print(nil, "openapi-generator-cli not found. Install with:")
|
||||||
fmt.Fprintln(os.Stderr, " brew install openapi-generator (macOS)")
|
core.Print(nil, " brew install openapi-generator (macOS)")
|
||||||
fmt.Fprintln(os.Stderr, " npm install @openapitools/openapi-generator-cli -g")
|
core.Print(nil, " npm install @openapitools/openapi-generator-cli -g")
|
||||||
return coreerr.E("sdk.Generate", "openapi-generator-cli not installed", nil)
|
return core.Result{OK: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate for each language.
|
for _, l := range core.Split(lang, ",") {
|
||||||
for l := range strings.SplitSeq(lang, ",") {
|
l = core.Trim(l)
|
||||||
l = strings.TrimSpace(l)
|
|
||||||
if l == "" {
|
if l == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Generating %s SDK...\n", l)
|
core.Print(nil, "generating %s SDK...", l)
|
||||||
if err := gen.Generate(context.Background(), l); err != nil {
|
r := gen.Generate(context.Background(), c, l)
|
||||||
return coreerr.E("sdk.Generate", "generate "+l, err)
|
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
|
return core.Result{OK: true}
|
||||||
})
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,52 +3,45 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
core "dappco.re/go/core"
|
||||||
"os"
|
|
||||||
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
|
||||||
|
|
||||||
goapi "dappco.re/go/core/api"
|
goapi "dappco.re/go/core/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addSpecCommand(parent *cli.Command) {
|
func cmdSpec(c *core.Core) core.CommandAction {
|
||||||
var (
|
return func(opts core.Options) core.Result {
|
||||||
output string
|
format := opts.String("format")
|
||||||
format string
|
if format == "" {
|
||||||
title string
|
format = "json"
|
||||||
version string
|
}
|
||||||
)
|
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{
|
builder := &goapi.SpecBuilder{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: "Lethean Core API",
|
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")
|
bridge := goapi.NewToolBridge("/tools")
|
||||||
groups := []goapi.RouteGroup{bridge}
|
groups := []goapi.RouteGroup{bridge}
|
||||||
|
|
||||||
if output != "" {
|
if output != "" {
|
||||||
if err := goapi.ExportSpecToFile(output, format, builder, groups); err != nil {
|
return goapi.ExportSpec(output, format, builder, groups)
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "Spec written to %s\n", output)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return goapi.ExportSpec(os.Stdout, format, builder, groups)
|
// No output file — build and print to stdout
|
||||||
})
|
r := builder.Build(groups)
|
||||||
|
if !r.OK {
|
||||||
cli.StringFlag(cmd, &output, "output", "o", "", "Write spec to file instead of stdout")
|
return r
|
||||||
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")
|
core.Println(string(r.Value.([]byte)))
|
||||||
cli.StringFlag(cmd, &version, "version", "V", "1.0.0", "API version in spec")
|
return core.Result{OK: true}
|
||||||
|
}
|
||||||
parent.AddCommand(cmd)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
codegen.go
57
codegen.go
|
|
@ -4,16 +4,11 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
coreio "dappco.re/go/core/io"
|
core "dappco.re/go/core"
|
||||||
coreerr "dappco.re/go/core/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Supported SDK target languages.
|
// Supported SDK target languages.
|
||||||
|
|
@ -33,43 +28,31 @@ var supportedLanguages = map[string]string{
|
||||||
|
|
||||||
// SDKGenerator wraps openapi-generator-cli for SDK generation.
|
// SDKGenerator wraps openapi-generator-cli for SDK generation.
|
||||||
type SDKGenerator struct {
|
type SDKGenerator struct {
|
||||||
// SpecPath is the path to the OpenAPI spec file (JSON or YAML).
|
|
||||||
SpecPath string
|
SpecPath string
|
||||||
|
|
||||||
// OutputDir is the base directory for generated SDK output.
|
|
||||||
OutputDir string
|
OutputDir string
|
||||||
|
|
||||||
// PackageName is the name used for the generated package/module.
|
|
||||||
PackageName string
|
PackageName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates an SDK for the given language using openapi-generator-cli.
|
// Generate creates an SDK for the given language using openapi-generator-cli.
|
||||||
// The language must be one of the supported languages returned by SupportedLanguages().
|
// Routes through c.Process() — requires go-process registered.
|
||||||
func (g *SDKGenerator) Generate(ctx context.Context, language string) error {
|
//
|
||||||
|
// r := gen.Generate(ctx, c, "go")
|
||||||
|
func (g *SDKGenerator) Generate(ctx context.Context, c *core.Core, language string) core.Result {
|
||||||
generator, ok := supportedLanguages[language]
|
generator, ok := supportedLanguages[language]
|
||||||
if !ok {
|
if !ok {
|
||||||
return coreerr.E("SDKGenerator.Generate", fmt.Sprintf("unsupported language %q: supported languages are %v", language, SupportedLanguages()), nil)
|
return core.Result{Value: core.E("SDKGenerator.Generate", core.Sprintf("unsupported language %q", language), nil), OK: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(g.SpecPath); os.IsNotExist(err) {
|
fs := c.Fs()
|
||||||
return coreerr.E("SDKGenerator.Generate", "spec file not found: "+g.SpecPath, nil)
|
if !fs.Exists(g.SpecPath) {
|
||||||
|
return core.Result{Value: core.E("SDKGenerator.Generate", core.Concat("spec file not found: ", g.SpecPath), nil), OK: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDir := filepath.Join(g.OutputDir, language)
|
outputDir := core.JoinPath(g.OutputDir, language)
|
||||||
if err := coreio.Local.EnsureDir(outputDir); err != nil {
|
fs.EnsureDir(outputDir)
|
||||||
return coreerr.E("SDKGenerator.Generate", "create output directory", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := g.buildArgs(generator, outputDir)
|
args := g.buildArgs(generator, outputDir)
|
||||||
cmd := exec.CommandContext(ctx, "openapi-generator-cli", args...)
|
return c.Process().Run(ctx, "openapi-generator-cli", args...)
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return coreerr.E("SDKGenerator.Generate", "openapi-generator-cli failed for "+language, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildArgs constructs the openapi-generator-cli command arguments.
|
// buildArgs constructs the openapi-generator-cli command arguments.
|
||||||
|
|
@ -81,24 +64,26 @@ func (g *SDKGenerator) buildArgs(generator, outputDir string) []string {
|
||||||
"-o", outputDir,
|
"-o", outputDir,
|
||||||
}
|
}
|
||||||
if g.PackageName != "" {
|
if g.PackageName != "" {
|
||||||
args = append(args, "--additional-properties", "packageName="+g.PackageName)
|
args = append(args, "--additional-properties", core.Concat("packageName=", g.PackageName))
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available checks if openapi-generator-cli is installed and accessible.
|
// Available checks if openapi-generator-cli is installed.
|
||||||
|
// Uses App.Find which searches PATH without importing os/exec.
|
||||||
|
//
|
||||||
|
// r := gen.Available()
|
||||||
func (g *SDKGenerator) Available() bool {
|
func (g *SDKGenerator) Available() bool {
|
||||||
_, err := exec.LookPath("openapi-generator-cli")
|
r := core.App{}.Find("openapi-generator-cli", "OpenAPI Generator")
|
||||||
return err == nil
|
return r.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportedLanguages returns the list of supported SDK target languages
|
// SupportedLanguages returns the list of supported SDK target languages.
|
||||||
// in sorted order for deterministic output.
|
|
||||||
func SupportedLanguages() []string {
|
func SupportedLanguages() []string {
|
||||||
return slices.Sorted(maps.Keys(supportedLanguages))
|
return slices.Sorted(maps.Keys(supportedLanguages))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportedLanguagesIter returns an iterator over supported SDK target languages in sorted order.
|
// SupportedLanguagesIter returns an iterator over supported SDK target languages.
|
||||||
func SupportedLanguagesIter() iter.Seq[string] {
|
func SupportedLanguagesIter() iter.Seq[string] {
|
||||||
return slices.Values(SupportedLanguages())
|
return slices.Values(SupportedLanguages())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
export.go
52
export.go
|
|
@ -3,56 +3,36 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
core "dappco.re/go/core"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
coreio "dappco.re/go/core/io"
|
|
||||||
coreerr "dappco.re/go/core/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExportSpec generates the OpenAPI spec and writes it to w.
|
// ExportSpec generates the OpenAPI spec and writes it to a core.Fs path.
|
||||||
// Format must be "json" or "yaml".
|
// Format must be "json" or "yaml".
|
||||||
func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error {
|
func ExportSpec(path, format string, builder *SpecBuilder, groups []RouteGroup) core.Result {
|
||||||
data, err := builder.Build(groups)
|
r := builder.Build(groups)
|
||||||
if err != nil {
|
if !r.OK {
|
||||||
return coreerr.E("ExportSpec", "build spec", err)
|
return r
|
||||||
}
|
}
|
||||||
|
data := r.Value.([]byte)
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case "json":
|
case "json":
|
||||||
_, err = w.Write(data)
|
return (&core.Fs{}).NewUnrestricted().Write(path, string(data))
|
||||||
return err
|
|
||||||
case "yaml":
|
case "yaml":
|
||||||
// Unmarshal JSON then re-marshal as YAML.
|
|
||||||
var obj any
|
var obj any
|
||||||
if err := json.Unmarshal(data, &obj); err != nil {
|
if ur := core.JSONUnmarshal(data, &obj); !ur.OK {
|
||||||
return coreerr.E("ExportSpec", "unmarshal spec", err)
|
return core.Result{Value: core.E("ExportSpec", "unmarshal spec", nil), OK: false}
|
||||||
}
|
}
|
||||||
enc := yaml.NewEncoder(w)
|
b := core.NewBuilder()
|
||||||
|
enc := yaml.NewEncoder(b)
|
||||||
enc.SetIndent(2)
|
enc.SetIndent(2)
|
||||||
if err := enc.Encode(obj); err != nil {
|
if err := enc.Encode(obj); err != nil {
|
||||||
return coreerr.E("ExportSpec", "encode yaml", err)
|
return core.Result{Value: core.E("ExportSpec", "encode yaml", err), OK: false}
|
||||||
}
|
}
|
||||||
return enc.Close()
|
enc.Close()
|
||||||
|
return (&core.Fs{}).NewUnrestricted().Write(path, b.String())
|
||||||
default:
|
default:
|
||||||
return coreerr.E("ExportSpec", "unsupported format "+format+": use \"json\" or \"yaml\"", nil)
|
return core.Result{Value: core.E("ExportSpec", core.Concat("unsupported format: ", format), nil), OK: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportSpecToFile writes the spec to the given path.
|
|
||||||
// The parent directory is created if it does not exist.
|
|
||||||
func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error {
|
|
||||||
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
|
|
||||||
return coreerr.E("ExportSpecToFile", "create directory", err)
|
|
||||||
}
|
|
||||||
f, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return coreerr.E("ExportSpecToFile", "create file", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return ExportSpec(f, format, builder, groups)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
17
go.mod
17
go.mod
|
|
@ -3,8 +3,8 @@ module dappco.re/go/core/api
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dappco.re/go/core/io v0.1.7
|
dappco.re/go/core/io v0.2.0
|
||||||
dappco.re/go/core/log v0.0.4
|
dappco.re/go/core/log v0.1.0
|
||||||
forge.lthn.ai/core/cli v0.3.7
|
forge.lthn.ai/core/cli v0.3.7
|
||||||
github.com/99designs/gqlgen v0.17.88
|
github.com/99designs/gqlgen v0.17.88
|
||||||
github.com/andybalholm/brotli v1.2.0
|
github.com/andybalholm/brotli v1.2.0
|
||||||
|
|
@ -38,6 +38,12 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
dappco.re/go/core v0.8.0-alpha.1
|
||||||
|
dappco.re/go/core/i18n v0.2.0
|
||||||
|
dappco.re/go/core/process v0.3.0
|
||||||
|
dappco.re/go/core/scm v0.4.0
|
||||||
|
dappco.re/go/core/store v0.2.0
|
||||||
|
dappco.re/go/core/ws v0.3.0
|
||||||
forge.lthn.ai/core/go v0.3.2 // indirect
|
forge.lthn.ai/core/go v0.3.2 // indirect
|
||||||
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
|
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
|
||||||
forge.lthn.ai/core/go-inference v0.1.7 // indirect
|
forge.lthn.ai/core/go-inference v0.1.7 // indirect
|
||||||
|
|
@ -128,10 +134,3 @@ require (
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
|
||||||
dappco.re/go/core => ../go
|
|
||||||
dappco.re/go/core/i18n => ../go-i18n
|
|
||||||
dappco.re/go/core/io => ../go-io
|
|
||||||
dappco.re/go/core/log => ../go-log
|
|
||||||
)
|
|
||||||
|
|
|
||||||
9
go.sum
9
go.sum
|
|
@ -1,3 +1,12 @@
|
||||||
|
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||||
|
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||||
|
dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok=
|
||||||
|
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||||
|
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||||
|
dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as=
|
||||||
|
dappco.re/go/core/scm v0.4.0/go.mod h1:ufb7si6HBkaT6zC8L67kLm8zzBaD1aQoTn4OsVAM1aI=
|
||||||
|
dappco.re/go/core/store v0.2.0/go.mod h1:QQGJiruayjna3nywbf0N2gcO502q/oEkPoSpBpSKbLM=
|
||||||
|
dappco.re/go/core/ws v0.3.0/go.mod h1:aLyXrJnbCOGL0SW9rC1EHAAIS83w3djO374gHIz4Nic=
|
||||||
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
||||||
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
||||||
forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
|
forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
"github.com/99designs/gqlgen/graphql/playground"
|
||||||
|
|
@ -49,7 +50,7 @@ func mountGraphQL(r *gin.Engine, cfg *graphqlConfig) {
|
||||||
r.Any(cfg.path, graphqlHandler)
|
r.Any(cfg.path, graphqlHandler)
|
||||||
|
|
||||||
if cfg.playground {
|
if cfg.playground {
|
||||||
playgroundPath := cfg.path + "/playground"
|
playgroundPath := core.Concat(cfg.path, "/playground")
|
||||||
playgroundHandler := playground.Handler("GraphQL", cfg.path)
|
playgroundHandler := playground.Handler("GraphQL", cfg.path)
|
||||||
r.GET(playgroundPath, wrapHTTPHandler(playgroundHandler))
|
r.GET(playgroundPath, wrapHTTPHandler(playgroundHandler))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
core "dappco.re/go/core"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -18,7 +18,7 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Check whether the request path should bypass authentication.
|
// Check whether the request path should bypass authentication.
|
||||||
for _, path := range skip {
|
for _, path := range skip {
|
||||||
if strings.HasPrefix(c.Request.URL.Path, path) {
|
if core.HasPrefix(c.Request.URL.Path, path) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -30,8 +30,8 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(header, " ", 2)
|
parts := core.SplitN(header, " ", 2)
|
||||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] != token {
|
if len(parts) != 2 || core.Lower(parts[0]) != "bearer" || parts[1] != token {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, Fail("unauthorised", "invalid bearer token"))
|
c.AbortWithStatusJSON(http.StatusUnauthorized, Fail("unauthorised", "invalid bearer token"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
openapi.go
11
openapi.go
|
|
@ -3,8 +3,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
core "dappco.re/go/core"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
|
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
|
||||||
|
|
@ -17,7 +16,7 @@ type SpecBuilder struct {
|
||||||
// Build generates the complete OpenAPI 3.1 JSON spec.
|
// Build generates the complete OpenAPI 3.1 JSON spec.
|
||||||
// Groups implementing DescribableGroup contribute endpoint documentation.
|
// Groups implementing DescribableGroup contribute endpoint documentation.
|
||||||
// Other groups are listed as tags only.
|
// Other groups are listed as tags only.
|
||||||
func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
|
func (sb *SpecBuilder) Build(groups []RouteGroup) core.Result {
|
||||||
spec := map[string]any{
|
spec := map[string]any{
|
||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": map[string]any{
|
"info": map[string]any{
|
||||||
|
|
@ -54,7 +53,7 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.MarshalIndent(spec, "", " ")
|
return core.JSONMarshal(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPaths generates the paths object from all DescribableGroups.
|
// buildPaths generates the paths object from all DescribableGroups.
|
||||||
|
|
@ -87,7 +86,7 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
||||||
}
|
}
|
||||||
for _, rd := range dg.Describe() {
|
for _, rd := range dg.Describe() {
|
||||||
fullPath := g.BasePath() + rd.Path
|
fullPath := g.BasePath() + rd.Path
|
||||||
method := strings.ToLower(rd.Method)
|
method := core.Lower(rd.Method)
|
||||||
|
|
||||||
operation := map[string]any{
|
operation := map[string]any{
|
||||||
"summary": rd.Summary,
|
"summary": rd.Summary,
|
||||||
|
|
@ -151,7 +150,7 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
||||||
if !seen[name] {
|
if !seen[name] {
|
||||||
tags = append(tags, map[string]any{
|
tags = append(tags, map[string]any{
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": name + " endpoints",
|
"description": core.Concat(name, " endpoints"),
|
||||||
})
|
})
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ type ProxyProvider struct {
|
||||||
func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
||||||
target, err := url.Parse(cfg.Upstream)
|
target, err := url.Parse(cfg.Upstream)
|
||||||
if err != nil {
|
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)
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
|
@ -54,16 +54,16 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider {
|
||||||
// Preserve the original Director but strip the base path so the
|
// 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).
|
// upstream receives clean paths (e.g. /items instead of /api/v1/cool-widget/items).
|
||||||
defaultDirector := proxy.Director
|
defaultDirector := proxy.Director
|
||||||
basePath := strings.TrimSuffix(cfg.BasePath, "/")
|
basePath := core.TrimSuffix(cfg.BasePath, "/")
|
||||||
|
|
||||||
proxy.Director = func(req *http.Request) {
|
proxy.Director = func(req *http.Request) {
|
||||||
defaultDirector(req)
|
defaultDirector(req)
|
||||||
// Strip the base path prefix from the request path.
|
// 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 == "" {
|
if req.URL.Path == "" {
|
||||||
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{
|
return &ProxyProvider{
|
||||||
|
|
|
||||||
12
sse.go
12
sse.go
|
|
@ -3,8 +3,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
core "dappco.re/go/core"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -44,14 +43,9 @@ func NewSSEBroker() *SSEBroker {
|
||||||
// Clients subscribed to an empty channel (no ?channel= param) receive
|
// Clients subscribed to an empty channel (no ?channel= param) receive
|
||||||
// events on every channel. The data value is JSON-encoded before sending.
|
// events on every channel. The data value is JSON-encoded before sending.
|
||||||
func (b *SSEBroker) Publish(channel, event string, data any) {
|
func (b *SSEBroker) Publish(channel, event string, data any) {
|
||||||
encoded, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := sseEvent{
|
msg := sseEvent{
|
||||||
Event: event,
|
Event: event,
|
||||||
Data: string(encoded),
|
Data: core.JSONMarshalString(data),
|
||||||
}
|
}
|
||||||
|
|
||||||
b.mu.RLock()
|
b.mu.RLock()
|
||||||
|
|
@ -109,7 +103,7 @@ func (b *SSEBroker) Handler() gin.HandlerFunc {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case evt := <-client.events:
|
case evt := <-client.events:
|
||||||
_, err := fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", evt.Event, evt.Data)
|
_, err := c.Writer.Write([]byte(core.Sprintf("event: %s\ndata: %s\n\n", evt.Event, evt.Data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
swagger.go
10
swagger.go
|
|
@ -3,7 +3,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
core "dappco.re/go/core"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
|
@ -29,12 +29,12 @@ type swaggerSpec struct {
|
||||||
// ReadDoc returns the OpenAPI 3.1 JSON document for this spec.
|
// ReadDoc returns the OpenAPI 3.1 JSON document for this spec.
|
||||||
func (s *swaggerSpec) ReadDoc() string {
|
func (s *swaggerSpec) ReadDoc() string {
|
||||||
s.once.Do(func() {
|
s.once.Do(func() {
|
||||||
data, err := s.builder.Build(s.groups)
|
r := s.builder.Build(s.groups)
|
||||||
if err != nil {
|
if !r.OK {
|
||||||
s.doc = `{"openapi":"3.1.0","info":{"title":"error","version":"0.0.0"},"paths":{}}`
|
s.doc = `{"openapi":"3.1.0","info":{"title":"error","version":"0.0.0"},"paths":{}}`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.doc = string(data)
|
s.doc = string(r.Value.([]byte))
|
||||||
})
|
})
|
||||||
return s.doc
|
return s.doc
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ func registerSwagger(g *gin.Engine, title, description, version string, groups [
|
||||||
},
|
},
|
||||||
groups: groups,
|
groups: groups,
|
||||||
}
|
}
|
||||||
name := fmt.Sprintf("swagger_%d", swaggerSeq.Add(1))
|
name := core.Sprintf("swagger_%d", swaggerSeq.Add(1))
|
||||||
swag.Register(name, spec)
|
swag.Register(name, spec)
|
||||||
g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(name)))
|
g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(name)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue