mirror of
https://github.com/aykhans/sarin.git
synced 2026-02-28 06:49:13 +00:00
Introduce structured error types and bump Go/linter versions
Replace ad-hoc fmt.Errorf/errors.New calls with typed error structs across config, sarin, and script packages to enable type-based error handling. Add script-specific error handlers in CLI entry point. Fix variable shadowing bug in Worker for scriptTransformer. Bump Go to 1.25.7 and golangci-lint to v2.8.0.
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
)
|
||||
|
||||
// Chain holds the loaded script sources and can create engine instances.
|
||||
@@ -36,6 +35,8 @@ type Transformer struct {
|
||||
|
||||
// NewTransformer creates engine instances from the chain's sources.
|
||||
// Call this once per worker goroutine.
|
||||
// It can return the following errors:
|
||||
// - types.ScriptChainError
|
||||
func (c *Chain) NewTransformer() (*Transformer, error) {
|
||||
if c.IsEmpty() {
|
||||
return &Transformer{}, nil
|
||||
@@ -51,7 +52,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
||||
engine, err := NewLuaEngine(src.Content)
|
||||
if err != nil {
|
||||
t.Close() // Clean up already created engines
|
||||
return nil, fmt.Errorf("lua script[%d]: %w", i, err)
|
||||
return nil, types.NewScriptChainError("lua", i, err)
|
||||
}
|
||||
t.luaEngines = append(t.luaEngines, engine)
|
||||
}
|
||||
@@ -61,7 +62,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
||||
engine, err := NewJsEngine(src.Content)
|
||||
if err != nil {
|
||||
t.Close() // Clean up already created engines
|
||||
return nil, fmt.Errorf("js script[%d]: %w", i, err)
|
||||
return nil, types.NewScriptChainError("js", i, err)
|
||||
}
|
||||
t.jsEngines = append(t.jsEngines, engine)
|
||||
}
|
||||
@@ -71,18 +72,20 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
||||
|
||||
// Transform applies all scripts to the request data.
|
||||
// Lua scripts run first, then JavaScript scripts.
|
||||
// It can return the following errors:
|
||||
// - types.ScriptChainError
|
||||
func (t *Transformer) Transform(req *RequestData) error {
|
||||
// Run Lua scripts
|
||||
for i, engine := range t.luaEngines {
|
||||
if err := engine.Transform(req); err != nil {
|
||||
return fmt.Errorf("lua script[%d]: %w", i, err)
|
||||
return types.NewScriptChainError("lua", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run JS scripts
|
||||
for i, engine := range t.jsEngines {
|
||||
if err := engine.Transform(req); err != nil {
|
||||
return fmt.Errorf("js script[%d]: %w", i, err)
|
||||
return types.NewScriptChainError("js", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package script
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
)
|
||||
|
||||
// JsEngine implements the Engine interface using goja (JavaScript).
|
||||
@@ -20,27 +20,31 @@ type JsEngine struct {
|
||||
// Example JavaScript script:
|
||||
//
|
||||
// function transform(req) {
|
||||
// req.headers["X-Custom"] = "value";
|
||||
// req.headers["X-Custom"] = ["value"];
|
||||
// return req;
|
||||
// }
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptTransformMissing
|
||||
// - types.ScriptExecutionError
|
||||
func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
||||
vm := goja.New()
|
||||
|
||||
// Execute the script to define the transform function
|
||||
_, err := vm.RunString(scriptContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute JavaScript script: %w", err)
|
||||
return nil, types.NewScriptExecutionError("JavaScript", err)
|
||||
}
|
||||
|
||||
// Get the transform function
|
||||
transformVal := vm.Get("transform")
|
||||
if transformVal == nil || goja.IsUndefined(transformVal) || goja.IsNull(transformVal) {
|
||||
return nil, errors.New("script must define a global 'transform' function")
|
||||
return nil, types.ErrScriptTransformMissing
|
||||
}
|
||||
|
||||
transform, ok := goja.AssertFunction(transformVal)
|
||||
if !ok {
|
||||
return nil, errors.New("'transform' must be a function")
|
||||
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
|
||||
}
|
||||
|
||||
return &JsEngine{
|
||||
@@ -50,6 +54,8 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
||||
}
|
||||
|
||||
// Transform executes the JavaScript transform function with the given request data.
|
||||
// It can return the following errors:
|
||||
// - types.ScriptExecutionError
|
||||
func (e *JsEngine) Transform(req *RequestData) error {
|
||||
// Convert RequestData to JavaScript object
|
||||
reqObj := e.requestDataToObject(req)
|
||||
@@ -57,12 +63,12 @@ func (e *JsEngine) Transform(req *RequestData) error {
|
||||
// Call transform(req)
|
||||
result, err := e.transform(goja.Undefined(), reqObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JavaScript transform error: %w", err)
|
||||
return types.NewScriptExecutionError("JavaScript", err)
|
||||
}
|
||||
|
||||
// Update RequestData from the returned object
|
||||
if err := e.objectToRequestData(result, req); err != nil {
|
||||
return fmt.Errorf("failed to parse transform result: %w", err)
|
||||
return types.NewScriptExecutionError("JavaScript", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -111,12 +117,12 @@ func (e *JsEngine) requestDataToObject(req *RequestData) goja.Value {
|
||||
// objectToRequestData updates RequestData from a JavaScript object.
|
||||
func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
||||
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
return errors.New("transform function must return an object")
|
||||
return types.ErrScriptTransformReturnObject
|
||||
}
|
||||
|
||||
obj := val.ToObject(e.runtime)
|
||||
if obj == nil {
|
||||
return errors.New("transform function must return an object")
|
||||
return types.ErrScriptTransformReturnObject
|
||||
}
|
||||
|
||||
// Method
|
||||
@@ -159,7 +165,7 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
||||
|
||||
// stringSliceToArray converts a Go []string to a JavaScript array.
|
||||
func (e *JsEngine) stringSliceToArray(values []string) *goja.Object {
|
||||
ifaces := make([]interface{}, len(values))
|
||||
ifaces := make([]any, len(values))
|
||||
for i, v := range values {
|
||||
ifaces[i] = v
|
||||
}
|
||||
@@ -181,7 +187,7 @@ func (e *JsEngine) objectToStringSliceMap(obj *goja.Object) map[string][]string
|
||||
}
|
||||
|
||||
// Check if it's an array
|
||||
if arr, ok := v.Export().([]interface{}); ok {
|
||||
if arr, ok := v.Export().([]any); ok {
|
||||
var values []string
|
||||
for _, item := range arr {
|
||||
if s, ok := item.(string); ok {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
)
|
||||
|
||||
// LuaEngine implements the Engine interface using gopher-lua.
|
||||
@@ -20,23 +20,27 @@ type LuaEngine struct {
|
||||
// Example Lua script:
|
||||
//
|
||||
// function transform(req)
|
||||
// req.headers["X-Custom"] = "value"
|
||||
// req.headers["X-Custom"] = {"value"}
|
||||
// return req
|
||||
// end
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptTransformMissing
|
||||
// - types.ScriptExecutionError
|
||||
func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
|
||||
L := lua.NewState()
|
||||
|
||||
// Execute the script to define the transform function
|
||||
if err := L.DoString(scriptContent); err != nil {
|
||||
L.Close()
|
||||
return nil, fmt.Errorf("failed to execute Lua script: %w", err)
|
||||
return nil, types.NewScriptExecutionError("Lua", err)
|
||||
}
|
||||
|
||||
// Get the transform function
|
||||
transform := L.GetGlobal("transform")
|
||||
if transform.Type() != lua.LTFunction {
|
||||
L.Close()
|
||||
return nil, errors.New("script must define a global 'transform' function")
|
||||
return nil, types.ErrScriptTransformMissing
|
||||
}
|
||||
|
||||
return &LuaEngine{
|
||||
@@ -46,6 +50,8 @@ func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
|
||||
}
|
||||
|
||||
// Transform executes the Lua transform function with the given request data.
|
||||
// It can return the following errors:
|
||||
// - types.ScriptExecutionError
|
||||
func (e *LuaEngine) Transform(req *RequestData) error {
|
||||
// Convert RequestData to Lua table
|
||||
reqTable := e.requestDataToTable(req)
|
||||
@@ -54,7 +60,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
|
||||
e.state.Push(e.transform)
|
||||
e.state.Push(reqTable)
|
||||
if err := e.state.PCall(1, 1, nil); err != nil {
|
||||
return fmt.Errorf("lua transform error: %w", err)
|
||||
return types.NewScriptExecutionError("Lua", err)
|
||||
}
|
||||
|
||||
// Get the result
|
||||
@@ -62,7 +68,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
|
||||
e.state.Pop(1)
|
||||
|
||||
if result.Type() != lua.LTTable {
|
||||
return fmt.Errorf("transform function must return a table, got %s", result.Type())
|
||||
return types.NewScriptExecutionError("Lua", fmt.Errorf("transform function must return a table, got %s", result.Type()))
|
||||
}
|
||||
|
||||
// Update RequestData from the returned table
|
||||
|
||||
@@ -2,14 +2,14 @@ package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
)
|
||||
|
||||
// RequestData represents the request data passed to scripts for transformation.
|
||||
@@ -56,9 +56,13 @@ type Source struct {
|
||||
// - Escaped "@": strings starting with "@@" (literal "@" at start, returns string without first @)
|
||||
// - File reference: "@/path/to/file" or "@./relative/path"
|
||||
// - URL reference: "@http://..." or "@https://..."
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptEmpty
|
||||
// - types.ScriptLoadError
|
||||
func LoadSource(ctx context.Context, source string, engineType EngineType) (*Source, error) {
|
||||
if source == "" {
|
||||
return nil, errors.New("script source cannot be empty")
|
||||
return nil, types.ErrScriptEmpty
|
||||
}
|
||||
|
||||
var content string
|
||||
@@ -77,7 +81,7 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
|
||||
content, err = readFile(ref)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load script from %q: %w", ref, err)
|
||||
return nil, types.NewScriptLoadError(ref, err)
|
||||
}
|
||||
default:
|
||||
// Inline script
|
||||
@@ -91,12 +95,15 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
|
||||
}
|
||||
|
||||
// LoadSources loads multiple script sources.
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptEmpty
|
||||
// - types.ScriptLoadError
|
||||
func LoadSources(ctx context.Context, sources []string, engineType EngineType) ([]*Source, error) {
|
||||
loaded := make([]*Source, 0, len(sources))
|
||||
for i, src := range sources {
|
||||
for _, src := range sources {
|
||||
source, err := LoadSource(ctx, src, engineType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("script[%d]: %w", i, err)
|
||||
return nil, err
|
||||
}
|
||||
loaded = append(loaded, source)
|
||||
}
|
||||
@@ -106,6 +113,12 @@ func LoadSources(ctx context.Context, sources []string, engineType EngineType) (
|
||||
// ValidateScript validates a script source by loading it and checking syntax.
|
||||
// It loads the script (from file/URL/inline), parses it, and verifies
|
||||
// that a 'transform' function is defined.
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptEmpty
|
||||
// - types.ErrScriptTransformMissing
|
||||
// - types.ScriptLoadError
|
||||
// - types.ScriptExecutionError
|
||||
// - types.ScriptUnknownEngineError
|
||||
func ValidateScript(ctx context.Context, source string, engineType EngineType) error {
|
||||
// Load the script source
|
||||
src, err := LoadSource(ctx, source, engineType)
|
||||
@@ -121,7 +134,7 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
|
||||
case EngineTypeJavaScript:
|
||||
engine, err = NewJsEngine(src.Content)
|
||||
default:
|
||||
return fmt.Errorf("unknown engine type: %s", engineType)
|
||||
return types.NewScriptUnknownEngineError(string(engineType))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -134,56 +147,67 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
|
||||
}
|
||||
|
||||
// ValidateScripts validates multiple script sources.
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptEmpty
|
||||
// - types.ErrScriptTransformMissing
|
||||
// - types.ScriptLoadError
|
||||
// - types.ScriptExecutionError
|
||||
// - types.ScriptUnknownEngineError
|
||||
func ValidateScripts(ctx context.Context, sources []string, engineType EngineType) error {
|
||||
for i, src := range sources {
|
||||
for _, src := range sources {
|
||||
if err := ValidateScript(ctx, src, engineType); err != nil {
|
||||
return fmt.Errorf("script[%d]: %w", i, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchURL downloads content from an HTTP/HTTPS URL.
|
||||
// It can return the following errors:
|
||||
// - types.HTTPFetchError
|
||||
// - types.HTTPStatusError
|
||||
func fetchURL(ctx context.Context, url string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
return "", types.NewHTTPFetchError(url, err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch: %w", err)
|
||||
return "", types.NewHTTPFetchError(url, err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d %s", resp.StatusCode, resp.Status)
|
||||
return "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
return "", types.NewHTTPFetchError(url, err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// readFile reads content from a local file.
|
||||
// It can return the following errors:
|
||||
// - types.FileReadError
|
||||
func readFile(path string) (string, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get working directory: %w", err)
|
||||
return "", types.NewFileReadError(path, err)
|
||||
}
|
||||
path = filepath.Join(pwd, path)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path) //nolint:gosec
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read file: %w", err)
|
||||
return "", types.NewFileReadError(path, err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
|
||||
Reference in New Issue
Block a user