refactor: AX compliance sweep — replace banned stdlib imports with core primitives

Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-13 09:32:00 +01:00
parent 7bcb6d469c
commit d90a5be936
23 changed files with 285 additions and 248 deletions

5
api.go
View file

@ -6,13 +6,14 @@ package api
import (
"context"
"errors"
"iter"
"net/http"
"reflect"
"slices"
"time"
core "dappco.re/go/core"
"github.com/gin-contrib/expvar"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
@ -193,7 +194,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)

View file

@ -9,6 +9,8 @@ import (
"strings"
"sync"
core "dappco.re/go/core"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
)
@ -207,10 +209,10 @@ func authentikMiddleware(cfg AuthentikConfig, publicPaths func() []string) gin.H
}
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)
@ -220,8 +222,8 @@ func authentikMiddleware(cfg AuthentikConfig, publicPaths func() []string) gin.H
// 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)
}
@ -235,8 +237,8 @@ func authentikMiddleware(cfg AuthentikConfig, publicPaths func() []string) gin.H
func cloneAuthentikConfig(cfg AuthentikConfig) AuthentikConfig {
out := cfg
out.Issuer = strings.TrimSpace(out.Issuer)
out.ClientID = strings.TrimSpace(out.ClientID)
out.Issuer = core.Trim(out.Issuer)
out.ClientID = core.Trim(out.ClientID)
out.PublicPaths = normalisePublicPaths(cfg.PublicPaths)
return out
}
@ -252,11 +254,11 @@ func normalisePublicPaths(paths []string) []string {
seen := make(map[string]struct{}, len(paths))
for _, path := range paths {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
continue
}
if !strings.HasPrefix(path, "/") {
if !core.HasPrefix(path, "/") {
path = "/" + path
}
path = strings.TrimRight(path, "/")

View file

@ -6,7 +6,6 @@ import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"iter"
"net"
@ -17,9 +16,9 @@ import (
"strconv"
"unicode/utf8"
"github.com/gin-gonic/gin"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"github.com/gin-gonic/gin"
)
// ToolDescriptor describes a tool that can be exposed as a REST endpoint.
@ -267,7 +266,7 @@ func newToolInputValidator(schema map[string]any) *toolInputValidator {
func (v *toolInputValidator) Validate(body []byte) error {
if len(bytes.TrimSpace(body)) == 0 {
return coreerr.E("ToolBridge.Validate", "request body is required", nil)
return core.E("ToolBridge.Validate", "request body is required", nil)
}
dec := json.NewDecoder(bytes.NewReader(body))
@ -275,11 +274,11 @@ func (v *toolInputValidator) Validate(body []byte) error {
var payload any
if err := dec.Decode(&payload); err != nil {
return coreerr.E("ToolBridge.Validate", "invalid JSON", err)
return core.E("ToolBridge.Validate", "invalid JSON", err)
}
var extra any
if err := dec.Decode(&extra); err != io.EOF {
return coreerr.E("ToolBridge.Validate", "request body must contain a single JSON value", nil)
return core.E("ToolBridge.Validate", "request body must contain a single JSON value", nil)
}
return validateSchemaNode(payload, v.schema, "")
@ -298,12 +297,12 @@ func (v *toolInputValidator) ValidateResponse(body []byte) error {
envDec := json.NewDecoder(bytes.NewReader(body))
envDec.UseNumber()
if err := envDec.Decode(&envelope); err != nil {
return coreerr.E("ToolBridge.ValidateResponse", "invalid JSON response", err)
return core.E("ToolBridge.ValidateResponse", "invalid JSON response", err)
}
success, _ := envelope["success"].(bool)
if !success {
return coreerr.E("ToolBridge.ValidateResponse", "response is missing a successful envelope", nil)
return core.E("ToolBridge.ValidateResponse", "response is missing a successful envelope", nil)
}
// data is serialised with omitempty, so a nil/zero-value payload from
@ -316,14 +315,14 @@ func (v *toolInputValidator) ValidateResponse(body []byte) error {
encoded, err := json.Marshal(data)
if err != nil {
return coreerr.E("ToolBridge.ValidateResponse", "encode response data", err)
return core.E("ToolBridge.ValidateResponse", "encode response data", err)
}
var payload any
dec := json.NewDecoder(bytes.NewReader(encoded))
dec.UseNumber()
if err := dec.Decode(&payload); err != nil {
return coreerr.E("ToolBridge.ValidateResponse", "decode response data", err)
return core.E("ToolBridge.ValidateResponse", "decode response data", err)
}
return validateSchemaNode(payload, v.schema, "")
@ -345,7 +344,7 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
for _, name := range stringList(schema["required"]) {
if _, ok := obj[name]; !ok {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s is missing required field %q", displayPath(path), name), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s is missing required field %q", displayPath(path), name), nil)
}
}
@ -371,7 +370,7 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
continue
}
}
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s contains unknown field %q", displayPath(path), name), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s contains unknown field %q", displayPath(path), name), nil)
}
}
if err := validateObjectConstraints(obj, schema, path); err != nil {
@ -433,7 +432,7 @@ func validateSchemaNode(value any, schema map[string]any, path string) error {
if rawEnum, ok := schema["enum"]; ok {
if !enumContains(value, rawEnum) {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be one of the declared enum values", displayPath(path)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be one of the declared enum values", displayPath(path)), nil)
}
}
@ -459,7 +458,7 @@ func validateSchemaCombinators(value any, schema map[string]any, path string) er
goto anyOfMatched
}
}
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must match at least one schema in anyOf", displayPath(path)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must match at least one schema in anyOf", displayPath(path)), nil)
}
anyOfMatched:
@ -472,15 +471,15 @@ anyOfMatched:
}
if matches != 1 {
if matches == 0 {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must match exactly one schema in oneOf", displayPath(path)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must match exactly one schema in oneOf", displayPath(path)), nil)
}
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s matches multiple schemas in oneOf", displayPath(path)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s matches multiple schemas in oneOf", displayPath(path)), nil)
}
}
if subschema, ok := schema["not"].(map[string]any); ok && subschema != nil {
if err := validateSchemaNode(value, subschema, path); err == nil {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must not match the forbidden schema", displayPath(path)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must not match the forbidden schema", displayPath(path)), nil)
}
}
@ -490,18 +489,18 @@ anyOfMatched:
func validateStringConstraints(value string, schema map[string]any, path string) error {
length := utf8.RuneCountInString(value)
if minLength, ok := schemaInt(schema["minLength"]); ok && length < minLength {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be at least %d characters long", displayPath(path), minLength), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be at least %d characters long", displayPath(path), minLength), nil)
}
if maxLength, ok := schemaInt(schema["maxLength"]); ok && length > maxLength {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be at most %d characters long", displayPath(path), maxLength), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be at most %d characters long", displayPath(path), maxLength), nil)
}
if pattern, ok := schema["pattern"].(string); ok && pattern != "" {
re, err := regexp.Compile(pattern)
if err != nil {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s has an invalid pattern %q", displayPath(path), pattern), err)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s has an invalid pattern %q", displayPath(path), pattern), err)
}
if !re.MatchString(value) {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s does not match pattern %q", displayPath(path), pattern), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s does not match pattern %q", displayPath(path), pattern), nil)
}
}
return nil
@ -509,30 +508,30 @@ func validateStringConstraints(value string, schema map[string]any, path string)
func validateNumericConstraints(value any, schema map[string]any, path string) error {
if minimum, ok := schemaFloat(schema["minimum"]); ok && numericLessThan(value, minimum) {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be greater than or equal to %v", displayPath(path), minimum), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be greater than or equal to %v", displayPath(path), minimum), nil)
}
if maximum, ok := schemaFloat(schema["maximum"]); ok && numericGreaterThan(value, maximum) {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be less than or equal to %v", displayPath(path), maximum), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be less than or equal to %v", displayPath(path), maximum), nil)
}
return nil
}
func validateArrayConstraints(value []any, schema map[string]any, path string) error {
if minItems, ok := schemaInt(schema["minItems"]); ok && len(value) < minItems {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must contain at least %d items", displayPath(path), minItems), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at least %d items", displayPath(path), minItems), nil)
}
if maxItems, ok := schemaInt(schema["maxItems"]); ok && len(value) > maxItems {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must contain at most %d items", displayPath(path), maxItems), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at most %d items", displayPath(path), maxItems), nil)
}
return nil
}
func validateObjectConstraints(value map[string]any, schema map[string]any, path string) error {
if minProps, ok := schemaInt(schema["minProperties"]); ok && len(value) < minProps {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must contain at least %d properties", displayPath(path), minProps), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at least %d properties", displayPath(path), minProps), nil)
}
if maxProps, ok := schemaInt(schema["maxProperties"]); ok && len(value) > maxProps {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must contain at most %d properties", displayPath(path), maxProps), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at most %d properties", displayPath(path), maxProps), nil)
}
return nil
}
@ -685,7 +684,7 @@ func (w *toolResponseRecorder) Written() bool {
}
func (w *toolResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, coreerr.E("ToolBridge.ResponseRecorder", "response hijacking is not supported by ToolBridge output validation", nil)
return nil, nil, core.E("ToolBridge.ResponseRecorder", "response hijacking is not supported by ToolBridge output validation", nil)
}
func (w *toolResponseRecorder) commit() {
@ -733,7 +732,7 @@ func (w *toolResponseRecorder) writeErrorResponse(status int, resp Response[any]
}
func typeError(path, want string, value any) error {
return coreerr.E("ToolBridge.ValidateSchema", fmt.Sprintf("%s must be %s, got %s", displayPath(path), want, describeJSONValue(value)), nil)
return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be %s, got %s", displayPath(path), want, describeJSONValue(value)), nil)
}
func displayPath(path string) string {
@ -912,6 +911,6 @@ func describeJSONValue(value any) string {
case []any:
return "array"
default:
return fmt.Sprintf("%T", value)
return core.Sprintf("%T", value)
}
}

View file

@ -6,9 +6,10 @@ import (
"io"
"net/http"
"strconv"
"strings"
"sync"
core "dappco.re/go/core"
"github.com/andybalholm/brotli"
"github.com/gin-gonic/gin"
)
@ -47,7 +48,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
}

104
client.go
View file

@ -5,22 +5,18 @@ package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"iter"
"net/http"
"net/url"
"os"
"reflect"
"sort"
"slices"
"strings"
"sync"
"slices"
core "dappco.re/go/core"
"gopkg.in/yaml.v3"
coreerr "dappco.re/go/core/log"
)
// OpenAPIClient is a small runtime client that can call operations by their
@ -191,14 +187,26 @@ func (c *OpenAPIClient) Operations() ([]OpenAPIOperation, error) {
for operationID, op := range c.operations {
operations = append(operations, snapshotOpenAPIOperation(operationID, op))
}
sort.SliceStable(operations, func(i, j int) bool {
if operations[i].OperationID == operations[j].OperationID {
if operations[i].Method == operations[j].Method {
return operations[i].PathTemplate < operations[j].PathTemplate
slices.SortStableFunc(operations, func(a, b OpenAPIOperation) int {
if a.OperationID != b.OperationID {
if a.OperationID < b.OperationID {
return -1
}
return operations[i].Method < operations[j].Method
return 1
}
return operations[i].OperationID < operations[j].OperationID
if a.Method != b.Method {
if a.Method < b.Method {
return -1
}
return 1
}
if a.PathTemplate < b.PathTemplate {
return -1
}
if a.PathTemplate > b.PathTemplate {
return 1
}
return 0
})
return operations, nil
}
@ -290,7 +298,7 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) {
op, ok := c.operations[operationID]
if !ok {
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("operation %q not found in OpenAPI spec", operationID), nil)
return nil, core.E("OpenAPIClient.Call", core.Sprintf("operation %q not found in OpenAPI spec", operationID), nil)
}
merged, err := normaliseParams(params)
@ -343,7 +351,7 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) {
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s returned %s: %s", operationID, resp.Status, strings.TrimSpace(string(payload))), nil)
return nil, core.E("OpenAPIClient.Call", core.Sprintf("openapi call %s returned %s: %s", operationID, resp.Status, core.Trim(string(payload))), nil)
}
if op.responseSchema != nil && len(bytes.TrimSpace(payload)) > 0 {
@ -367,9 +375,9 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) {
if success, ok := envelope["success"].(bool); ok {
if !success {
if errObj, ok := envelope["error"].(map[string]any); ok {
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s failed: %v", operationID, errObj), nil)
return nil, core.E("OpenAPIClient.Call", core.Sprintf("openapi call %s failed: %v", operationID, errObj), nil)
}
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s failed", operationID), nil)
return nil, core.E("OpenAPIClient.Call", core.Sprintf("openapi call %s failed", operationID), nil)
}
if data, ok := envelope["data"]; ok {
return data, nil
@ -397,23 +405,23 @@ func (c *OpenAPIClient) loadSpec() error {
case c.specReader != nil:
data, err = io.ReadAll(c.specReader)
case c.specPath != "":
f, openErr := os.Open(c.specPath)
if openErr != nil {
return coreerr.E("OpenAPIClient.loadSpec", "read spec", openErr)
cfs := (&core.Fs{}).NewUnrestricted()
r := cfs.Read(c.specPath)
if !r.OK {
return core.E("OpenAPIClient.loadSpec", "read spec", r.Value.(error))
}
defer f.Close()
data, err = io.ReadAll(f)
data = []byte(r.Value.(string))
default:
return coreerr.E("OpenAPIClient.loadSpec", "spec path or reader is required", nil)
return core.E("OpenAPIClient.loadSpec", "spec path or reader is required", nil)
}
if err != nil {
return coreerr.E("OpenAPIClient.loadSpec", "read spec", err)
return core.E("OpenAPIClient.loadSpec", "read spec", err)
}
var spec map[string]any
if err := yaml.Unmarshal(data, &spec); err != nil {
return coreerr.E("OpenAPIClient.loadSpec", "parse spec", err)
return core.E("OpenAPIClient.loadSpec", "parse spec", err)
}
operations := make(map[string]openAPIOperation)
@ -434,7 +442,7 @@ func (c *OpenAPIClient) loadSpec() error {
}
params := parseOperationParameters(operation)
operations[operationID] = openAPIOperation{
method: strings.ToUpper(method),
method: core.Upper(method),
pathTemplate: pathTemplate,
hasRequestBody: operation["requestBody"] != nil,
parameters: params,
@ -484,7 +492,7 @@ func snapshotOpenAPIOperation(operationID string, op openAPIOperation) OpenAPIOp
return OpenAPIOperation{
OperationID: operationID,
Method: strings.ToUpper(op.method),
Method: core.Upper(op.method),
PathTemplate: op.pathTemplate,
HasRequestBody: op.hasRequestBody,
Parameters: parameters,
@ -494,7 +502,7 @@ func snapshotOpenAPIOperation(operationID string, op openAPIOperation) OpenAPIOp
func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (string, error) {
base := strings.TrimRight(c.baseURL, "/")
if base == "" {
return "", coreerr.E("OpenAPIClient.buildURL", "base URL is required", nil)
return "", core.E("OpenAPIClient.buildURL", "base URL is required", nil)
}
path := op.pathTemplate
@ -516,12 +524,12 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st
for _, key := range pathKeys {
if value, ok := pathValues[key]; ok {
placeholder := "{" + key + "}"
path = strings.ReplaceAll(path, placeholder, url.PathEscape(fmt.Sprint(value)))
path = core.Replace(path, placeholder, url.PathEscape(core.Sprint(value)))
}
}
if strings.Contains(path, "{") {
return "", coreerr.E("OpenAPIClient.buildURL", fmt.Sprintf("missing path parameters for %q", op.pathTemplate), nil)
if core.Contains(path, "{") {
return "", core.E("OpenAPIClient.buildURL", core.Sprintf("missing path parameters for %q", op.pathTemplate), nil)
}
fullURL, err := url.JoinPath(base, path)
@ -670,7 +678,7 @@ func applyHeaderValue(headers http.Header, key string, value any) {
return
case []any:
for _, item := range v {
headers.Add(key, fmt.Sprint(item))
headers.Add(key, core.Sprint(item))
}
return
}
@ -678,12 +686,12 @@ func applyHeaderValue(headers http.Header, key string, value any) {
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) {
for i := 0; i < rv.Len(); i++ {
headers.Add(key, fmt.Sprint(rv.Index(i).Interface()))
headers.Add(key, core.Sprint(rv.Index(i).Interface()))
}
return
}
headers.Set(key, fmt.Sprint(value))
headers.Set(key, core.Sprint(value))
}
func applyCookieValues(req *http.Request, values map[string]any) {
@ -703,7 +711,7 @@ func applyCookieValue(req *http.Request, key string, value any) {
return
case []any:
for _, item := range v {
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(item)})
req.AddCookie(&http.Cookie{Name: key, Value: core.Sprint(item)})
}
return
}
@ -711,12 +719,12 @@ func applyCookieValue(req *http.Request, key string, value any) {
rv := reflect.ValueOf(value)
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) {
for i := 0; i < rv.Len(); i++ {
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(rv.Index(i).Interface())})
req.AddCookie(&http.Cookie{Name: key, Value: core.Sprint(rv.Index(i).Interface())})
}
return
}
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(value)})
req.AddCookie(&http.Cookie{Name: key, Value: core.Sprint(value)})
}
func parseOperationParameters(operation map[string]any) []openAPIParameter {
@ -784,9 +792,9 @@ func validateParameterValue(param openAPIParameter, value any) error {
data, err := json.Marshal(value)
if err != nil {
return coreerr.E("OpenAPIClient.validateParameterValue", fmt.Sprintf("marshal %s parameter %q", param.in, param.name), err)
return core.E("OpenAPIClient.validateParameterValue", core.Sprintf("marshal %s parameter %q", param.in, param.name), err)
}
if err := validateOpenAPISchema(data, param.schema, fmt.Sprintf("%s parameter %q", param.in, param.name)); err != nil {
if err := validateOpenAPISchema(data, param.schema, core.Sprintf("%s parameter %q", param.in, param.name)); err != nil {
return err
}
return nil
@ -800,7 +808,7 @@ func validateRequiredParameters(op openAPIOperation, params map[string]any, path
if parameterProvided(params, param.name, param.in) {
continue
}
return coreerr.E("OpenAPIClient.buildURL", fmt.Sprintf("missing required %s parameter %q", param.in, param.name), nil)
return core.E("OpenAPIClient.buildURL", core.Sprintf("missing required %s parameter %q", param.in, param.name), nil)
}
return nil
}
@ -840,12 +848,12 @@ func normaliseParams(params any) (map[string]any, error) {
data, err := json.Marshal(params)
if err != nil {
return nil, coreerr.E("OpenAPIClient.normaliseParams", "marshal params", err)
return nil, core.E("OpenAPIClient.normaliseParams", "marshal params", err)
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
return nil, coreerr.E("OpenAPIClient.normaliseParams", "decode params", err)
return nil, core.E("OpenAPIClient.normaliseParams", "decode params", err)
}
return out, nil
@ -936,7 +944,7 @@ func appendQueryValue(query url.Values, key string, value any) {
return
}
query.Add(key, fmt.Sprint(value))
query.Add(key, core.Sprint(value))
}
func isAbsoluteBaseURL(raw string) bool {
@ -1004,15 +1012,15 @@ func validateOpenAPISchema(body []byte, schema map[string]any, label string) err
dec := json.NewDecoder(bytes.NewReader(body))
dec.UseNumber()
if err := dec.Decode(&payload); err != nil {
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s: invalid JSON", label), err)
return core.E("OpenAPIClient.validateOpenAPISchema", core.Sprintf("validate %s: invalid JSON", label), err)
}
var extra any
if err := dec.Decode(&extra); err != io.EOF {
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s: expected a single JSON value", label), nil)
return core.E("OpenAPIClient.validateOpenAPISchema", core.Sprintf("validate %s: expected a single JSON value", label), nil)
}
if err := validateSchemaNode(payload, schema, ""); err != nil {
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s", label), err)
return core.E("OpenAPIClient.validateOpenAPISchema", core.Sprintf("validate %s", label), err)
}
return nil
@ -1023,15 +1031,15 @@ func validateOpenAPIResponse(payload []byte, schema map[string]any, operationID
dec := json.NewDecoder(bytes.NewReader(payload))
dec.UseNumber()
if err := dec.Decode(&decoded); err != nil {
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s returned invalid JSON", operationID), err)
return core.E("OpenAPIClient.validateOpenAPIResponse", core.Sprintf("openapi call %s returned invalid JSON", operationID), err)
}
var extra any
if err := dec.Decode(&extra); err != io.EOF {
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s returned multiple JSON values", operationID), nil)
return core.E("OpenAPIClient.validateOpenAPIResponse", core.Sprintf("openapi call %s returned multiple JSON values", operationID), nil)
}
if err := validateSchemaNode(decoded, schema, ""); err != nil {
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s response does not match spec", operationID), err)
return core.E("OpenAPIClient.validateOpenAPIResponse", core.Sprintf("openapi call %s response does not match spec", operationID), err)
}
return nil

View file

@ -2,7 +2,11 @@
package api
import "strings"
import (
"strings"
core "dappco.re/go/core"
)
// splitUniqueCSV trims and deduplicates a comma-separated list while
// preserving the first occurrence of each value.
@ -11,12 +15,12 @@ func splitUniqueCSV(raw string) []string {
return nil
}
parts := strings.Split(raw, ",")
parts := core.Split(raw, ",")
values := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
value := core.Trim(part)
if value == "" {
continue
}
@ -41,11 +45,11 @@ func normalisePublicPaths(paths []string) []string {
seen := make(map[string]struct{}, len(paths))
for _, path := range paths {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
continue
}
if !strings.HasPrefix(path, "/") {
if !core.HasPrefix(path, "/") {
path = "/" + path
}
path = strings.TrimRight(path, "/")

View file

@ -4,11 +4,11 @@ package api
import (
"encoding/json"
"fmt"
"os"
"strings"
"forge.lthn.ai/core/cli/pkg/cli"
core "dappco.re/go/core"
"dappco.re/go/core/cli/pkg/cli"
goapi "dappco.re/go/core/api"
)
@ -38,7 +38,7 @@ func addSpecCommand(parent *cli.Command) {
if err := goapi.ExportSpecToFileIter(output, format, builder, groups); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Spec written to %s\n", output)
_, _ = os.Stderr.Write([]byte(core.Sprintf("Spec written to %s\n", output)))
return nil
}
@ -57,7 +57,7 @@ func parseServers(raw string) []string {
}
func parseSecuritySchemes(raw string) (map[string]any, error) {
raw = strings.TrimSpace(raw)
raw = core.Trim(raw)
if raw == "" {
return nil, nil
}

View file

@ -3,9 +3,10 @@
package api
import (
"strings"
"time"
core "dappco.re/go/core"
goapi "dappco.re/go/core/api"
)
@ -45,24 +46,24 @@ type specBuilderConfig struct {
}
func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) {
swaggerPath := strings.TrimSpace(cfg.swaggerPath)
graphqlPath := strings.TrimSpace(cfg.graphqlPath)
ssePath := strings.TrimSpace(cfg.ssePath)
wsPath := strings.TrimSpace(cfg.wsPath)
cacheTTL := strings.TrimSpace(cfg.cacheTTL)
swaggerPath := core.Trim(cfg.swaggerPath)
graphqlPath := core.Trim(cfg.graphqlPath)
ssePath := core.Trim(cfg.ssePath)
wsPath := core.Trim(cfg.wsPath)
cacheTTL := core.Trim(cfg.cacheTTL)
cacheTTLValid := parsePositiveDuration(cacheTTL)
builder := &goapi.SpecBuilder{
Title: strings.TrimSpace(cfg.title),
Summary: strings.TrimSpace(cfg.summary),
Description: strings.TrimSpace(cfg.description),
Version: strings.TrimSpace(cfg.version),
Title: core.Trim(cfg.title),
Summary: core.Trim(cfg.summary),
Description: core.Trim(cfg.description),
Version: core.Trim(cfg.version),
SwaggerEnabled: swaggerPath != "",
SwaggerPath: swaggerPath,
GraphQLEnabled: graphqlPath != "" || cfg.graphqlPlayground,
GraphQLPath: graphqlPath,
GraphQLPlayground: cfg.graphqlPlayground,
GraphQLPlaygroundPath: strings.TrimSpace(cfg.graphqlPlaygroundPath),
GraphQLPlaygroundPath: core.Trim(cfg.graphqlPlaygroundPath),
SSEEnabled: ssePath != "",
SSEPath: ssePath,
WSEnabled: wsPath != "",
@ -73,18 +74,18 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) {
CacheTTL: cacheTTL,
CacheMaxEntries: cfg.cacheMaxEntries,
CacheMaxBytes: cfg.cacheMaxBytes,
I18nDefaultLocale: strings.TrimSpace(cfg.i18nDefaultLocale),
TermsOfService: strings.TrimSpace(cfg.termsURL),
ContactName: strings.TrimSpace(cfg.contactName),
ContactURL: strings.TrimSpace(cfg.contactURL),
ContactEmail: strings.TrimSpace(cfg.contactEmail),
I18nDefaultLocale: core.Trim(cfg.i18nDefaultLocale),
TermsOfService: core.Trim(cfg.termsURL),
ContactName: core.Trim(cfg.contactName),
ContactURL: core.Trim(cfg.contactURL),
ContactEmail: core.Trim(cfg.contactEmail),
Servers: parseServers(cfg.servers),
LicenseName: strings.TrimSpace(cfg.licenseName),
LicenseURL: strings.TrimSpace(cfg.licenseURL),
ExternalDocsDescription: strings.TrimSpace(cfg.externalDocsDescription),
ExternalDocsURL: strings.TrimSpace(cfg.externalDocsURL),
AuthentikIssuer: strings.TrimSpace(cfg.authentikIssuer),
AuthentikClientID: strings.TrimSpace(cfg.authentikClientID),
LicenseName: core.Trim(cfg.licenseName),
LicenseURL: core.Trim(cfg.licenseURL),
ExternalDocsDescription: core.Trim(cfg.externalDocsDescription),
ExternalDocsURL: core.Trim(cfg.externalDocsURL),
AuthentikIssuer: core.Trim(cfg.authentikIssuer),
AuthentikClientID: core.Trim(cfg.authentikClientID),
AuthentikTrustedProxy: cfg.authentikTrustedProxy,
AuthentikPublicPaths: normalisePublicPaths(splitUniqueCSV(cfg.authentikPublicPaths)),
}
@ -110,7 +111,7 @@ func parseLocales(raw string) []string {
}
func parsePositiveDuration(raw string) bool {
raw = strings.TrimSpace(raw)
raw = core.Trim(raw)
if raw == "" {
return false
}

View file

@ -6,6 +6,8 @@ import (
"net/http"
"strings"
core "dappco.re/go/core"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
@ -114,7 +116,7 @@ func mountGraphQL(r *gin.Engine, cfg *graphqlConfig) {
// normaliseGraphQLPath coerces custom GraphQL paths into a stable form.
// The path always begins with a single slash and never ends with one.
func normaliseGraphQLPath(path string) string {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
return defaultGraphQLPath
}

View file

@ -4,7 +4,8 @@ package api
import (
"slices"
"strings"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
"golang.org/x/text/language"
@ -200,19 +201,19 @@ func GetMessage(c *gin.Context, key string) (string, bool) {
// most specific to least specific. For example, "fr-CA" yields
// ["fr-CA", "fr"] and "zh-Hant-TW" yields ["zh-Hant-TW", "zh-Hant", "zh"].
func localeFallbacks(locale string) []string {
locale = strings.TrimSpace(strings.ReplaceAll(locale, "_", "-"))
locale = core.Trim(core.Replace(locale, "_", "-"))
if locale == "" {
return nil
}
parts := strings.Split(locale, "-")
parts := core.Split(locale, "-")
if len(parts) == 0 {
return []string{locale}
}
fallbacks := make([]string, 0, len(parts))
for i := len(parts); i >= 1; i-- {
fallbacks = append(fallbacks, strings.Join(parts[:i], "-"))
fallbacks = append(fallbacks, core.Join("-", parts[:i]...))
}
return fallbacks

View file

@ -5,12 +5,13 @@ package api
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"runtime/debug"
"strings"
"time"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -26,7 +27,7 @@ const requestStartContextKey = "request_start"
// and avoids Gin's default plain-text 500 response.
func recoveryMiddleware() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered any) {
fmt.Fprintf(gin.DefaultErrorWriter, "[Recovery] panic recovered: %v\n", recovered)
_, _ = gin.DefaultErrorWriter.Write([]byte(core.Sprintf("[Recovery] panic recovered: %v\n", recovered)))
debug.PrintStack()
c.AbortWithStatusJSON(http.StatusInternalServerError, Fail(
"internal_server_error",
@ -54,8 +55,8 @@ func bearerAuthMiddleware(token string, skip func() []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
}
@ -85,7 +86,7 @@ func isPublicPath(requestPath, publicPath string) bool {
return true
}
return strings.HasPrefix(requestPath, normalized+"/")
return core.HasPrefix(requestPath, normalized+"/")
}
// requestIDMiddleware ensures every response carries an X-Request-ID header.

View file

@ -6,13 +6,13 @@ import (
"encoding/json"
"iter"
"net/http"
"sort"
"slices"
"strconv"
"strings"
"time"
"unicode"
"slices"
core "dappco.re/go/core"
)
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
@ -162,16 +162,16 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
if sb.CacheMaxBytes > 0 {
spec["x-cache-max-bytes"] = sb.CacheMaxBytes
}
if locale := strings.TrimSpace(sb.I18nDefaultLocale); locale != "" {
if locale := core.Trim(sb.I18nDefaultLocale); locale != "" {
spec["x-i18n-default-locale"] = locale
}
if len(sb.I18nSupportedLocales) > 0 {
spec["x-i18n-supported-locales"] = slices.Clone(sb.I18nSupportedLocales)
}
if issuer := strings.TrimSpace(sb.AuthentikIssuer); issuer != "" {
if issuer := core.Trim(sb.AuthentikIssuer); issuer != "" {
spec["x-authentik-issuer"] = issuer
}
if clientID := strings.TrimSpace(sb.AuthentikClientID); clientID != "" {
if clientID := core.Trim(sb.AuthentikClientID); clientID != "" {
spec["x-authentik-client-id"] = clientID
}
if sb.AuthentikTrustedProxy {
@ -353,8 +353,8 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
for _, g := range groups {
for _, rd := range g.descs {
fullPath := joinOpenAPIPath(g.basePath, rd.Path)
method := strings.ToLower(rd.Method)
deprecated := rd.Deprecated || strings.TrimSpace(rd.SunsetDate) != "" || strings.TrimSpace(rd.Replacement) != ""
method := core.Lower(rd.Method)
deprecated := rd.Deprecated || core.Trim(rd.SunsetDate) != "" || core.Trim(rd.Replacement) != ""
deprecationHeaders := deprecationResponseHeaders(deprecated, rd.SunsetDate, rd.Replacement)
isPublic := isPublicPathForList(fullPath, publicPaths)
security := rd.Security
@ -443,8 +443,8 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
// OpenAPI path without duplicate or missing separators. Gin-style parameters
// such as :id and *path are converted to OpenAPI template parameters.
func joinOpenAPIPath(basePath, routePath string) string {
basePath = strings.TrimSpace(basePath)
routePath = strings.TrimSpace(routePath)
basePath = core.Trim(basePath)
routePath = core.Trim(routePath)
if basePath == "" {
basePath = "/"
@ -460,28 +460,28 @@ func joinOpenAPIPath(basePath, routePath string) string {
return routePath
}
return strings.TrimRight(basePath, "/") + "/" + strings.TrimPrefix(routePath, "/")
return strings.TrimRight(basePath, "/") + "/" + core.TrimPrefix(routePath, "/")
}
// normaliseOpenAPIPath trims whitespace and collapses trailing separators
// while preserving the root path and converting Gin-style path parameters.
func normaliseOpenAPIPath(path string) string {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
return "/"
}
segments := strings.Split(path, "/")
segments := core.Split(path, "/")
cleaned := make([]string, 0, len(segments))
for _, segment := range segments {
segment = strings.TrimSpace(segment)
segment = core.Trim(segment)
if segment == "" {
continue
}
switch {
case strings.HasPrefix(segment, ":") && len(segment) > 1:
case core.HasPrefix(segment, ":") && len(segment) > 1:
segment = "{" + segment[1:] + "}"
case strings.HasPrefix(segment, "*") && len(segment) > 1:
case core.HasPrefix(segment, "*") && len(segment) > 1:
segment = "{" + segment[1:] + "}"
}
cleaned = append(cleaned, segment)
@ -491,7 +491,7 @@ func normaliseOpenAPIPath(path string) string {
return "/"
}
return "/" + strings.Join(cleaned, "/")
return "/" + core.Join("/", cleaned...)
}
// operationResponses builds the standard response set for a documented API
@ -570,7 +570,7 @@ func operationResponses(method string, statusCode int, dataSchema map[string]any
},
}
if deprecated && (strings.TrimSpace(sunsetDate) != "" || strings.TrimSpace(replacement) != "") {
if deprecated && (core.Trim(sunsetDate) != "" || core.Trim(replacement) != "") {
responses["410"] = map[string]any{
"description": "Gone",
"content": map[string]any{
@ -686,8 +686,8 @@ func healthResponses() map[string]any {
// deprecationResponseHeaders documents the standard deprecation headers for
// deprecated or sunsetted operations.
func deprecationResponseHeaders(deprecated bool, sunsetDate, replacement string) map[string]any {
sunsetDate = strings.TrimSpace(sunsetDate)
replacement = strings.TrimSpace(replacement)
sunsetDate = core.Trim(sunsetDate)
replacement = core.Trim(replacement)
if !deprecated && sunsetDate == "" && replacement == "" {
return nil
@ -835,7 +835,7 @@ func securitySchemeComponents(overrides map[string]any) map[string]any {
}
for name, scheme := range overrides {
name = strings.TrimSpace(name)
name = core.Trim(name)
if name == "" || scheme == nil {
continue
}
@ -877,7 +877,7 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
}
for _, g := range groups {
name := strings.TrimSpace(g.name)
name := core.Trim(g.name)
if name != "" && !seen[name] {
tags = append(tags, map[string]any{
"name": name,
@ -888,7 +888,7 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
for _, rd := range g.descs {
for _, tag := range rd.Tags {
tag = strings.TrimSpace(tag)
tag = core.Trim(tag)
if tag == "" || seen[tag] {
continue
}
@ -913,17 +913,21 @@ func sortTags(tags []map[string]any) {
return
}
sort.SliceStable(tags, func(i, j int) bool {
left, _ := tags[i]["name"].(string)
right, _ := tags[j]["name"].(string)
slices.SortStableFunc(tags, func(a, b map[string]any) int {
left, _ := a["name"].(string)
right, _ := b["name"].(string)
switch {
case left == "system":
return true
return -1
case right == "system":
return false
return 1
case left < right:
return -1
case left > right:
return 1
default:
return left < right
return 0
}
})
}
@ -1753,7 +1757,7 @@ func resolvedOperationTags(groupName string, rd RouteDescription) []string {
return tags
}
if name := strings.TrimSpace(groupName); name != "" {
if name := core.Trim(groupName); name != "" {
return []string{name}
}
@ -1770,7 +1774,7 @@ func cleanTags(tags []string) []string {
cleaned := make([]string, 0, len(tags))
seen := make(map[string]struct{}, len(tags))
for _, tag := range tags {
tag = strings.TrimSpace(tag)
tag = core.Trim(tag)
if tag == "" {
continue
}
@ -1916,7 +1920,7 @@ func (sb *SpecBuilder) effectiveGraphQLPath() string {
if !sb.GraphQLEnabled && !sb.GraphQLPlayground {
return ""
}
graphqlPath := strings.TrimSpace(sb.GraphQLPath)
graphqlPath := core.Trim(sb.GraphQLPath)
if graphqlPath == "" {
return defaultGraphQLPath
}
@ -1930,7 +1934,7 @@ func (sb *SpecBuilder) effectiveGraphQLPlaygroundPath() string {
return ""
}
path := strings.TrimSpace(sb.GraphQLPlaygroundPath)
path := core.Trim(sb.GraphQLPlaygroundPath)
if path != "" {
return path
}
@ -1950,7 +1954,7 @@ func (sb *SpecBuilder) effectiveSwaggerPath() string {
if !sb.SwaggerEnabled {
return ""
}
swaggerPath := strings.TrimSpace(sb.SwaggerPath)
swaggerPath := core.Trim(sb.SwaggerPath)
if swaggerPath == "" {
return defaultSwaggerPath
}
@ -1964,7 +1968,7 @@ func (sb *SpecBuilder) effectiveWSPath() string {
if !sb.WSEnabled {
return ""
}
wsPath := strings.TrimSpace(sb.WSPath)
wsPath := core.Trim(sb.WSPath)
if wsPath == "" {
return defaultWSPath
}
@ -1978,7 +1982,7 @@ func (sb *SpecBuilder) effectiveSSEPath() string {
if !sb.SSEEnabled {
return ""
}
ssePath := strings.TrimSpace(sb.SSEPath)
ssePath := core.Trim(sb.SSEPath)
if ssePath == "" {
return defaultSSEPath
}
@ -1988,7 +1992,7 @@ func (sb *SpecBuilder) effectiveSSEPath() string {
// effectiveCacheTTL returns a normalised cache TTL when it parses to a
// positive duration.
func (sb *SpecBuilder) effectiveCacheTTL() string {
ttl := strings.TrimSpace(sb.CacheTTL)
ttl := core.Trim(sb.CacheTTL)
if ttl == "" {
return ""
}
@ -2024,25 +2028,25 @@ func (sb *SpecBuilder) snapshot() *SpecBuilder {
}
out := *sb
out.Title = strings.TrimSpace(out.Title)
out.Summary = strings.TrimSpace(out.Summary)
out.Description = strings.TrimSpace(out.Description)
out.Version = strings.TrimSpace(out.Version)
out.SwaggerPath = strings.TrimSpace(out.SwaggerPath)
out.GraphQLPath = strings.TrimSpace(out.GraphQLPath)
out.GraphQLPlaygroundPath = strings.TrimSpace(out.GraphQLPlaygroundPath)
out.WSPath = strings.TrimSpace(out.WSPath)
out.SSEPath = strings.TrimSpace(out.SSEPath)
out.TermsOfService = strings.TrimSpace(out.TermsOfService)
out.ContactName = strings.TrimSpace(out.ContactName)
out.ContactURL = strings.TrimSpace(out.ContactURL)
out.ContactEmail = strings.TrimSpace(out.ContactEmail)
out.LicenseName = strings.TrimSpace(out.LicenseName)
out.LicenseURL = strings.TrimSpace(out.LicenseURL)
out.ExternalDocsDescription = strings.TrimSpace(out.ExternalDocsDescription)
out.ExternalDocsURL = strings.TrimSpace(out.ExternalDocsURL)
out.CacheTTL = strings.TrimSpace(out.CacheTTL)
out.I18nDefaultLocale = strings.TrimSpace(out.I18nDefaultLocale)
out.Title = core.Trim(out.Title)
out.Summary = core.Trim(out.Summary)
out.Description = core.Trim(out.Description)
out.Version = core.Trim(out.Version)
out.SwaggerPath = core.Trim(out.SwaggerPath)
out.GraphQLPath = core.Trim(out.GraphQLPath)
out.GraphQLPlaygroundPath = core.Trim(out.GraphQLPlaygroundPath)
out.WSPath = core.Trim(out.WSPath)
out.SSEPath = core.Trim(out.SSEPath)
out.TermsOfService = core.Trim(out.TermsOfService)
out.ContactName = core.Trim(out.ContactName)
out.ContactURL = core.Trim(out.ContactURL)
out.ContactEmail = core.Trim(out.ContactEmail)
out.LicenseName = core.Trim(out.LicenseName)
out.LicenseURL = core.Trim(out.LicenseURL)
out.ExternalDocsDescription = core.Trim(out.ExternalDocsDescription)
out.ExternalDocsURL = core.Trim(out.ExternalDocsURL)
out.CacheTTL = core.Trim(out.CacheTTL)
out.I18nDefaultLocale = core.Trim(out.I18nDefaultLocale)
out.Servers = slices.Clone(sb.Servers)
out.I18nSupportedLocales = slices.Clone(sb.I18nSupportedLocales)
out.AuthentikPublicPaths = normalisePublicPaths(sb.AuthentikPublicPaths)
@ -2064,8 +2068,8 @@ func (sb *SpecBuilder) hasAuthentikMetadata() bool {
return false
}
return strings.TrimSpace(sb.AuthentikIssuer) != "" ||
strings.TrimSpace(sb.AuthentikClientID) != "" ||
return core.Trim(sb.AuthentikIssuer) != "" ||
core.Trim(sb.AuthentikClientID) != "" ||
sb.AuthentikTrustedProxy ||
len(sb.AuthentikPublicPaths) > 0
}
@ -2109,7 +2113,7 @@ func documentedResponseHeaders(headers map[string]string) map[string]any {
out := make(map[string]any, len(headers))
for name, description := range headers {
name = strings.TrimSpace(name)
name = core.Trim(name)
if name == "" {
continue
}
@ -2153,7 +2157,7 @@ func mergeHeaders(sets ...map[string]any) map[string]any {
// operationID builds a stable OpenAPI operationId from the HTTP method and path.
// The generated identifier is lower snake_case and preserves path parameter names.
func operationID(method, path string, operationIDs map[string]int) string {
var b strings.Builder
b := core.NewBuilder()
b.Grow(len(method) + len(path) + 1)
lastUnderscore := false

View file

@ -7,9 +7,10 @@ import (
"log/slog"
"net/http"
"slices"
"strings"
"time"
core "dappco.re/go/core"
"github.com/99designs/gqlgen/graphql"
"github.com/casbin/casbin/v2"
"github.com/gin-contrib/authz"
@ -204,9 +205,9 @@ func WithSunset(sunsetDate, replacement string) Option {
// api.New(api.WithSwagger("Service", "Public API", "1.0.0"))
func WithSwagger(title, description, version string) Option {
return func(e *Engine) {
e.swaggerTitle = strings.TrimSpace(title)
e.swaggerDesc = strings.TrimSpace(description)
e.swaggerVersion = strings.TrimSpace(version)
e.swaggerTitle = core.Trim(title)
e.swaggerDesc = core.Trim(description)
e.swaggerVersion = core.Trim(version)
e.swaggerEnabled = true
}
}
@ -218,7 +219,7 @@ func WithSwagger(title, description, version string) Option {
// api.WithSwaggerSummary("Service overview")
func WithSwaggerSummary(summary string) Option {
return func(e *Engine) {
if summary = strings.TrimSpace(summary); summary != "" {
if summary = core.Trim(summary); summary != "" {
e.swaggerSummary = summary
}
}
@ -244,7 +245,7 @@ func WithSwaggerPath(path string) Option {
// api.WithSwaggerTermsOfService("https://example.com/terms")
func WithSwaggerTermsOfService(url string) Option {
return func(e *Engine) {
if url = strings.TrimSpace(url); url != "" {
if url = core.Trim(url); url != "" {
e.swaggerTermsOfService = url
}
}
@ -258,13 +259,13 @@ func WithSwaggerTermsOfService(url string) Option {
// api.WithSwaggerContact("API Support", "https://example.com/support", "support@example.com")
func WithSwaggerContact(name, url, email string) Option {
return func(e *Engine) {
if name = strings.TrimSpace(name); name != "" {
if name = core.Trim(name); name != "" {
e.swaggerContactName = name
}
if url = strings.TrimSpace(url); url != "" {
if url = core.Trim(url); url != "" {
e.swaggerContactURL = url
}
if email = strings.TrimSpace(email); email != "" {
if email = core.Trim(email); email != "" {
e.swaggerContactEmail = email
}
}
@ -291,10 +292,10 @@ func WithSwaggerServers(servers ...string) Option {
// api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/")
func WithSwaggerLicense(name, url string) Option {
return func(e *Engine) {
if name = strings.TrimSpace(name); name != "" {
if name = core.Trim(name); name != "" {
e.swaggerLicenseName = name
}
if url = strings.TrimSpace(url); url != "" {
if url = core.Trim(url); url != "" {
e.swaggerLicenseURL = url
}
}
@ -322,7 +323,7 @@ func WithSwaggerSecuritySchemes(schemes map[string]any) Option {
e.swaggerSecuritySchemes = make(map[string]any, len(schemes))
}
for name, scheme := range schemes {
name = strings.TrimSpace(name)
name = core.Trim(name)
if name == "" || scheme == nil {
continue
}
@ -340,10 +341,10 @@ func WithSwaggerSecuritySchemes(schemes map[string]any) Option {
// api.WithSwaggerExternalDocs("Developer guide", "https://example.com/docs")
func WithSwaggerExternalDocs(description, url string) Option {
return func(e *Engine) {
if description = strings.TrimSpace(description); description != "" {
if description = core.Trim(description); description != "" {
e.swaggerExternalDocsDescription = description
}
if url = strings.TrimSpace(url); url != "" {
if url = core.Trim(url); url != "" {
e.swaggerExternalDocsURL = url
}
}

View file

@ -3,11 +3,11 @@
package provider
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
core "dappco.re/go/core"
coreapi "dappco.re/go/core/api"
"github.com/gin-gonic/gin"
@ -63,7 +63,7 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider {
if target.Scheme == "" || target.Host == "" {
return &ProxyProvider{
config: cfg,
err: fmt.Errorf("upstream %q must include a scheme and host (e.g. http://127.0.0.1:9901)", cfg.Upstream),
err: core.E("ProxyProvider.New", core.Sprintf("upstream %q must include a scheme and host (e.g. http://127.0.0.1:9901)", cfg.Upstream), nil),
}
}
@ -72,7 +72,7 @@ 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)
@ -102,7 +102,7 @@ func (p *ProxyProvider) Err() error {
// It only strips when the path matches the base path itself or lives under
// the base path boundary, so "/api" will not accidentally trim "/api-v2".
func stripBasePath(path, basePath string) string {
basePath = strings.TrimSuffix(strings.TrimSpace(basePath), "/")
basePath = core.TrimSuffix(core.Trim(basePath), "/")
if basePath == "" || basePath == "/" {
if path == "" {
return "/"
@ -115,8 +115,8 @@ func stripBasePath(path, basePath string) string {
}
prefix := basePath + "/"
if strings.HasPrefix(path, prefix) {
trimmed := strings.TrimPrefix(path, basePath)
if core.HasPrefix(path, prefix) {
trimmed := core.TrimPrefix(path, basePath)
if trimmed == "" {
return "/"
}

View file

@ -8,10 +8,11 @@ import (
"math"
"net/http"
"strconv"
"strings"
"sync"
"time"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -241,7 +242,7 @@ func clientRateLimitKey(c *gin.Context) string {
// Fall back to credential headers before the IP so that different API
// keys coming from the same NAT address are bucketed independently. The
// raw secret is never stored — it is hashed with SHA-256 first.
if apiKey := strings.TrimSpace(c.GetHeader("X-API-Key")); apiKey != "" {
if apiKey := core.Trim(c.GetHeader("X-API-Key")); apiKey != "" {
h := sha256.Sum256([]byte(apiKey))
return "cred:sha256:" + hex.EncodeToString(h[:])
}
@ -262,15 +263,15 @@ func clientRateLimitKey(c *gin.Context) string {
}
func bearerTokenFromHeader(header string) string {
header = strings.TrimSpace(header)
header = core.Trim(header)
if header == "" {
return ""
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
parts := core.SplitN(header, " ", 2)
if len(parts) != 2 || core.Lower(parts[0]) != "bearer" {
return ""
}
return strings.TrimSpace(parts[1])
return core.Trim(parts[1])
}

View file

@ -11,9 +11,10 @@ import (
"net"
"net/http"
"strconv"
"strings"
"time"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -270,17 +271,17 @@ func shouldAttachResponseMeta(contentType string, body []byte) bool {
}
func isJSONContentType(contentType string) bool {
if strings.TrimSpace(contentType) == "" {
if core.Trim(contentType) == "" {
return false
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
mediaType = strings.TrimSpace(contentType)
mediaType = core.Trim(contentType)
}
mediaType = strings.ToLower(mediaType)
mediaType = core.Lower(mediaType)
return mediaType == "application/json" ||
strings.HasSuffix(mediaType, "+json") ||
strings.HasSuffix(mediaType, "/json")
core.HasSuffix(mediaType, "+json") ||
core.HasSuffix(mediaType, "/json")
}

View file

@ -2,7 +2,11 @@
package api
import "strings"
import (
"strings"
core "dappco.re/go/core"
)
// normaliseServers trims whitespace, removes empty entries, and preserves
// the first occurrence of each server URL.
@ -36,7 +40,7 @@ func normaliseServers(servers []string) []string {
// normaliseServer trims surrounding whitespace and removes a trailing slash
// from non-root server URLs so equivalent metadata collapses to one entry.
func normaliseServer(server string) string {
server = strings.TrimSpace(server)
server = core.Trim(server)
if server == "" {
return ""
}

View file

@ -5,7 +5,8 @@ package api
import (
"reflect"
"slices"
"strings"
core "dappco.re/go/core"
)
// SwaggerConfig captures the configured Swagger/OpenAPI metadata for an Engine.
@ -125,7 +126,7 @@ func (e *Engine) SwaggerConfig() SwaggerConfig {
ExternalDocsURL: e.swaggerExternalDocsURL,
}
if strings.TrimSpace(e.swaggerPath) != "" {
if core.Trim(e.swaggerPath) != "" {
cfg.Path = normaliseSwaggerPath(e.swaggerPath)
}

9
sse.go
View file

@ -4,11 +4,12 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -59,7 +60,7 @@ func NewSSEBroker() *SSEBroker {
// normaliseSSEPath coerces custom SSE paths into a stable form.
// The path always begins with a single slash and never ends with one.
func normaliseSSEPath(path string) string {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
return defaultSSEPath
}
@ -75,7 +76,7 @@ func normaliseSSEPath(path string) string {
// resolveSSEPath returns the configured SSE path or the default path when
// no override has been provided.
func resolveSSEPath(path string) string {
if strings.TrimSpace(path) == "" {
if core.Trim(path) == "" {
return defaultSSEPath
}
return normaliseSSEPath(path)
@ -178,7 +179,7 @@ func (b *SSEBroker) Handler() gin.HandlerFunc {
if !ok {
return
}
_, 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
}

View file

@ -4,9 +4,10 @@ package api
import (
"net/http"
"strings"
"time"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -21,8 +22,8 @@ import (
//
// rg.Use(api.ApiSunset("2025-06-01", "/api/v2/users"))
func ApiSunset(sunsetDate, replacement string) gin.HandlerFunc {
sunsetDate = strings.TrimSpace(sunsetDate)
replacement = strings.TrimSpace(replacement)
sunsetDate = core.Trim(sunsetDate)
replacement = core.Trim(replacement)
formatted := formatSunsetDate(sunsetDate)
warning := "This endpoint is deprecated."
if sunsetDate != "" {
@ -44,11 +45,11 @@ func ApiSunset(sunsetDate, replacement string) gin.HandlerFunc {
}
func formatSunsetDate(sunsetDate string) string {
sunsetDate = strings.TrimSpace(sunsetDate)
sunsetDate = core.Trim(sunsetDate)
if sunsetDate == "" {
return ""
}
if strings.Contains(sunsetDate, ",") {
if core.Contains(sunsetDate, ",") {
return sunsetDate
}

View file

@ -3,12 +3,13 @@
package api
import (
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
@ -58,7 +59,7 @@ func (s *swaggerSpec) ReadDoc() string {
func registerSwagger(g *gin.Engine, e *Engine, groups []RouteGroup) {
swaggerPath := resolveSwaggerPath(e.swaggerPath)
spec := newSwaggerSpec(e.OpenAPISpecBuilder(), groups)
name := fmt.Sprintf("swagger_%d", swaggerSeq.Add(1))
name := core.Sprintf("swagger_%d", swaggerSeq.Add(1))
swag.Register(name, spec)
g.GET(swaggerPath, func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, swaggerPath+"/")
@ -69,7 +70,7 @@ func registerSwagger(g *gin.Engine, e *Engine, groups []RouteGroup) {
// normaliseSwaggerPath coerces custom Swagger paths into a stable form.
// The path always begins with a single slash and never ends with one.
func normaliseSwaggerPath(path string) string {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
return defaultSwaggerPath
}
@ -85,7 +86,7 @@ func normaliseSwaggerPath(path string) string {
// resolveSwaggerPath returns the configured Swagger path or the default path
// when no override has been provided.
func resolveSwaggerPath(path string) string {
if strings.TrimSpace(path) == "" {
if core.Trim(path) == "" {
return defaultSwaggerPath
}
return normaliseSwaggerPath(path)

View file

@ -2,7 +2,7 @@
package api
import "strings"
import core "dappco.re/go/core"
// TransportConfig captures the configured transport endpoints and flags for an Engine.
//
@ -52,16 +52,16 @@ func (e *Engine) TransportConfig() TransportConfig {
cfg.GraphQLPlayground = gql.Playground
cfg.GraphQLPlaygroundPath = gql.PlaygroundPath
if e.swaggerEnabled || strings.TrimSpace(e.swaggerPath) != "" {
if e.swaggerEnabled || core.Trim(e.swaggerPath) != "" {
cfg.SwaggerPath = resolveSwaggerPath(e.swaggerPath)
}
if gql.Path != "" {
cfg.GraphQLPath = gql.Path
}
if e.wsHandler != nil || strings.TrimSpace(e.wsPath) != "" {
if e.wsHandler != nil || core.Trim(e.wsPath) != "" {
cfg.WSPath = resolveWSPath(e.wsPath)
}
if e.sseBroker != nil || strings.TrimSpace(e.ssePath) != "" {
if e.sseBroker != nil || core.Trim(e.ssePath) != "" {
cfg.SSEPath = resolveSSEPath(e.ssePath)
}

View file

@ -6,6 +6,8 @@ import (
"net/http"
"strings"
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
)
@ -23,7 +25,7 @@ func wrapWSHandler(h http.Handler) gin.HandlerFunc {
// normaliseWSPath coerces custom WebSocket paths into a stable form.
// The path always begins with a single slash and never ends with one.
func normaliseWSPath(path string) string {
path = strings.TrimSpace(path)
path = core.Trim(path)
if path == "" {
return defaultWSPath
}
@ -39,7 +41,7 @@ func normaliseWSPath(path string) string {
// resolveWSPath returns the configured WebSocket path or the default path
// when no override has been provided.
func resolveWSPath(path string) string {
if strings.TrimSpace(path) == "" {
if core.Trim(path) == "" {
return defaultWSPath
}
return normaliseWSPath(path)