diff --git a/api.go b/api.go index d391726..5239f75 100644 --- a/api.go +++ b/api.go @@ -6,12 +6,13 @@ package api import ( "context" - "errors" "iter" "net/http" "slices" "time" + core "dappco.re/go/core" + "github.com/gin-contrib/expvar" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" @@ -114,7 +115,7 @@ func (e *Engine) Serve(ctx context.Context) error { errCh := make(chan error, 1) 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 } close(errCh) diff --git a/authentik.go b/authentik.go index fa08217..6771eb9 100644 --- a/authentik.go +++ b/authentik.go @@ -4,9 +4,9 @@ package api import ( "context" + core "dappco.re/go/core" "net/http" "slices" - "strings" "sync" "github.com/coreos/go-oidc/v3/oidc" @@ -148,7 +148,7 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc { // Skip public paths. path := c.Request.URL.Path for p := range public { - if strings.HasPrefix(path, p) { + if core.HasPrefix(path, p) { c.Next() return } @@ -167,10 +167,10 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc { } 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 != "" { - user.Entitlements = strings.Split(ent, "|") + user.Entitlements = core.Split(ent, "|") } c.Set(authentikUserKey, user) @@ -180,8 +180,8 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc { // Block 2: Attempt JWT validation for direct API clients. // Only when OIDC is configured and no user was extracted from headers. if cfg.Issuer != "" && cfg.ClientID != "" && GetUser(c) == nil { - if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") { - rawToken := strings.TrimPrefix(auth, "Bearer ") + if auth := c.GetHeader("Authorization"); core.HasPrefix(auth, "Bearer ") { + rawToken := core.TrimPrefix(auth, "Bearer ") if user, err := validateJWT(c.Request.Context(), cfg, rawToken); err == nil { c.Set(authentikUserKey, user) } diff --git a/brotli.go b/brotli.go index b203cf2..a54f588 100644 --- a/brotli.go +++ b/brotli.go @@ -3,10 +3,10 @@ package api import ( + core "dappco.re/go/core" "io" "net/http" "strconv" - "strings" "sync" "github.com/andybalholm/brotli" @@ -47,7 +47,7 @@ func newBrotliHandler(level int) *brotliHandler { // Handle is the Gin middleware function that compresses responses with Brotli. 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() return } diff --git a/codegen.go b/codegen.go index b8cb12e..902dc44 100644 --- a/codegen.go +++ b/codegen.go @@ -4,16 +4,11 @@ package api import ( "context" - "fmt" "iter" "maps" - "os" - "os/exec" - "path/filepath" "slices" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // Supported SDK target languages. @@ -33,43 +28,31 @@ var supportedLanguages = map[string]string{ // SDKGenerator wraps openapi-generator-cli for SDK generation. type SDKGenerator struct { - // SpecPath is the path to the OpenAPI spec file (JSON or YAML). - SpecPath string - - // OutputDir is the base directory for generated SDK output. - OutputDir string - - // PackageName is the name used for the generated package/module. + SpecPath string + OutputDir string PackageName string } // Generate creates an SDK for the given language using openapi-generator-cli. -// The language must be one of the supported languages returned by SupportedLanguages(). -func (g *SDKGenerator) Generate(ctx context.Context, language string) error { +// Routes through c.Process() — requires go-process registered. +// +// r := gen.Generate(ctx, c, "go") +func (g *SDKGenerator) Generate(ctx context.Context, c *core.Core, language string) core.Result { generator, ok := supportedLanguages[language] 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) { - return coreerr.E("SDKGenerator.Generate", "spec file not found: "+g.SpecPath, nil) + fs := c.Fs() + 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) - if err := coreio.Local.EnsureDir(outputDir); err != nil { - return coreerr.E("SDKGenerator.Generate", "create output directory", err) - } + outputDir := core.JoinPath(g.OutputDir, language) + fs.EnsureDir(outputDir) args := g.buildArgs(generator, outputDir) - cmd := exec.CommandContext(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 + return c.Process().Run(ctx, "openapi-generator-cli", args...) } // buildArgs constructs the openapi-generator-cli command arguments. @@ -81,24 +64,26 @@ func (g *SDKGenerator) buildArgs(generator, outputDir string) []string { "-o", outputDir, } if g.PackageName != "" { - args = append(args, "--additional-properties", "packageName="+g.PackageName) + args = append(args, "--additional-properties", core.Concat("packageName=", g.PackageName)) } 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 { - _, err := exec.LookPath("openapi-generator-cli") - return err == nil + r := core.App{}.Find("openapi-generator-cli", "OpenAPI Generator") + return r.OK } -// SupportedLanguages returns the list of supported SDK target languages -// in sorted order for deterministic output. +// SupportedLanguages returns the list of supported SDK target languages. func SupportedLanguages() []string { 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] { return slices.Values(SupportedLanguages()) } diff --git a/export.go b/export.go index f514ed9..9e3d01c 100644 --- a/export.go +++ b/export.go @@ -3,56 +3,36 @@ package api import ( - "encoding/json" - "io" - "os" - "path/filepath" - + core "dappco.re/go/core" "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". -func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error { - data, err := builder.Build(groups) - if err != nil { - return coreerr.E("ExportSpec", "build spec", err) +func ExportSpec(path, format string, builder *SpecBuilder, groups []RouteGroup) core.Result { + r := builder.Build(groups) + if !r.OK { + return r } + data := r.Value.([]byte) switch format { case "json": - _, err = w.Write(data) - return err + return (&core.Fs{}).NewUnrestricted().Write(path, string(data)) case "yaml": - // Unmarshal JSON then re-marshal as YAML. var obj any - if err := json.Unmarshal(data, &obj); err != nil { - return coreerr.E("ExportSpec", "unmarshal spec", err) + if ur := core.JSONUnmarshal(data, &obj); !ur.OK { + 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) 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: - 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) -} diff --git a/go.mod b/go.mod index 50f8b1d..339386e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module dappco.re/go/core/api go 1.26.0 require ( - dappco.re/go/core/io v0.1.7 - dappco.re/go/core/log v0.0.4 + dappco.re/go/core/io v0.2.0 + dappco.re/go/core/log v0.1.0 forge.lthn.ai/core/cli v0.3.7 github.com/99designs/gqlgen v0.17.88 github.com/andybalholm/brotli v1.2.0 @@ -38,6 +38,13 @@ require ( ) require ( + dappco.re/go/core v0.5.0 + dappco.re/go/core/api v0.2.0 + 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-i18n v0.1.7 // indirect forge.lthn.ai/core/go-inference v0.1.7 // indirect @@ -128,10 +135,3 @@ require ( golang.org/x/tools v0.43.0 // 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 -) diff --git a/middleware.go b/middleware.go index 55fe8ae..2294eaf 100644 --- a/middleware.go +++ b/middleware.go @@ -6,7 +6,7 @@ import ( "crypto/rand" "encoding/hex" "net/http" - "strings" + core "dappco.re/go/core" "github.com/gin-gonic/gin" ) @@ -18,7 +18,7 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc { return func(c *gin.Context) { // Check whether the request path should bypass authentication. for _, path := range skip { - if strings.HasPrefix(c.Request.URL.Path, path) { + if core.HasPrefix(c.Request.URL.Path, path) { c.Next() return } @@ -30,8 +30,8 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc { return } - parts := strings.SplitN(header, " ", 2) - if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] != token { + parts := core.SplitN(header, " ", 2) + if len(parts) != 2 || core.Lower(parts[0]) != "bearer" || parts[1] != token { c.AbortWithStatusJSON(http.StatusUnauthorized, Fail("unauthorised", "invalid bearer token")) return } diff --git a/openapi.go b/openapi.go index b98d8d1..b29cf7a 100644 --- a/openapi.go +++ b/openapi.go @@ -3,8 +3,7 @@ package api import ( - "encoding/json" - "strings" + core "dappco.re/go/core" ) // 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. // Groups implementing DescribableGroup contribute endpoint documentation. // 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{ "openapi": "3.1.0", "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. @@ -87,7 +86,7 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any { } for _, rd := range dg.Describe() { fullPath := g.BasePath() + rd.Path - method := strings.ToLower(rd.Method) + method := core.Lower(rd.Method) operation := map[string]any{ "summary": rd.Summary, diff --git a/sse.go b/sse.go index 9adf7ee..ac326e5 100644 --- a/sse.go +++ b/sse.go @@ -3,8 +3,7 @@ package api import ( - "encoding/json" - "fmt" + core "dappco.re/go/core" "net/http" "sync" @@ -44,14 +43,9 @@ func NewSSEBroker() *SSEBroker { // Clients subscribed to an empty channel (no ?channel= param) receive // events on every channel. The data value is JSON-encoded before sending. func (b *SSEBroker) Publish(channel, event string, data any) { - encoded, err := json.Marshal(data) - if err != nil { - return - } - msg := sseEvent{ Event: event, - Data: string(encoded), + Data: core.JSONMarshalString(data), } b.mu.RLock() @@ -109,7 +103,7 @@ func (b *SSEBroker) Handler() gin.HandlerFunc { case <-ctx.Done(): return 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 { return } diff --git a/swagger.go b/swagger.go index 65b45c5..c56140c 100644 --- a/swagger.go +++ b/swagger.go @@ -3,7 +3,7 @@ package api import ( - "fmt" + core "dappco.re/go/core" "sync" "sync/atomic" @@ -29,12 +29,12 @@ type swaggerSpec struct { // ReadDoc returns the OpenAPI 3.1 JSON document for this spec. func (s *swaggerSpec) ReadDoc() string { s.once.Do(func() { - data, err := s.builder.Build(s.groups) - if err != nil { + r := s.builder.Build(s.groups) + if !r.OK { s.doc = `{"openapi":"3.1.0","info":{"title":"error","version":"0.0.0"},"paths":{}}` return } - s.doc = string(data) + s.doc = string(r.Value.([]byte)) }) return s.doc } @@ -49,7 +49,7 @@ func registerSwagger(g *gin.Engine, title, description, version string, groups [ }, groups: groups, } - name := fmt.Sprintf("swagger_%d", swaggerSeq.Add(1)) + name := core.Sprintf("swagger_%d", swaggerSeq.Add(1)) swag.Register(name, spec) g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(name))) }