feat: AX v0.8.0 — Core primitives, Result returns, zero disallowed imports
- api.go: errors.Is → core.Is - openapi.go: Build returns core.Result, encoding/json → core.JSONMarshal - export.go: rewrite with core.Fs, core.JSONUnmarshal, returns core.Result - codegen.go: rewrite with c.Process(), core.Fs, App.Find — no os/exec - sse.go: encoding/json → core.JSONMarshalString, fmt → core.Sprintf - swagger.go: fmt → core.Sprintf, Build caller updated for core.Result - middleware.go: strings → core.HasPrefix/SplitN/Lower - authentik.go: strings → core.HasPrefix/TrimPrefix/Split/Trim/Contains - brotli.go: strings → core.Contains Transport boundary files (net/http, io for compression) retained. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
ca9b495884
commit
de63217168
10 changed files with 75 additions and 116 deletions
5
api.go
5
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)
|
||||
|
|
|
|||
12
authentik.go
12
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
61
codegen.go
61
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())
|
||||
}
|
||||
|
|
|
|||
52
export.go
52
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)
|
||||
}
|
||||
|
|
|
|||
18
go.mod
18
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
12
sse.go
12
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
|
||||
}
|
||||
|
|
|
|||
10
swagger.go
10
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)))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue