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:
2026-02-08 02:54:54 +04:00
parent e83eacf380
commit 6dafc082ed
20 changed files with 473 additions and 106 deletions

View File

@@ -16,8 +16,8 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version: 1.25.5 go-version: 1.25.7
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.7.2 version: v2.8.0

View File

@@ -35,7 +35,7 @@ jobs:
run: | run: |
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
echo "GO_VERSION=1.25.5" >> $GITHUB_ENV echo "GO_VERSION=1.25.7" >> $GITHUB_ENV
- name: Set up Go - name: Set up Go
if: github.event_name == 'release' || inputs.build_binaries if: github.event_name == 'release' || inputs.build_binaries

View File

@@ -1,4 +1,4 @@
ARG GO_VERSION=1.25.5 ARG GO_VERSION=1.25.7
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder

View File

@@ -3,7 +3,7 @@ version: "3"
vars: vars:
BIN_DIR: ./bin BIN_DIR: ./bin
GOLANGCI_LINT_VERSION: v2.7.2 GOLANGCI_LINT_VERSION: v2.8.0
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}" GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
tasks: tasks:

View File

@@ -55,16 +55,22 @@ func main() {
*combinedConfig.DryRun, *combinedConfig.DryRun,
combinedConfig.Lua, combinedConfig.Js, combinedConfig.Lua, combinedConfig.Js,
) )
if err != nil {
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[ERROR] ")+err.Error())
os.Exit(1)
}
_ = utilsErr.MustHandle(err, _ = utilsErr.MustHandle(err,
utilsErr.OnType(func(err types.ProxyDialError) error { utilsErr.OnType(func(err types.ProxyDialError) error {
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[PROXY] ")+err.Error()) fmt.Fprintln(os.Stderr, config.StyleRed.Render("[PROXY] ")+err.Error())
os.Exit(1) os.Exit(1)
return nil return nil
}), }),
utilsErr.OnSentinel(types.ErrScriptEmpty, func(err error) error {
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[SCRIPT] ")+err.Error())
os.Exit(1)
return nil
}),
utilsErr.OnType(func(err types.ScriptLoadError) error {
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[SCRIPT] ")+err.Error())
os.Exit(1)
return nil
}),
) )
srn.Start(ctx) srn.Start(ctx)

2
go.mod
View File

@@ -1,6 +1,6 @@
module go.aykhans.me/sarin module go.aykhans.me/sarin
go 1.25.5 go 1.25.7
require ( require (
github.com/brianvoe/gofakeit/v7 v7.14.0 github.com/brianvoe/gofakeit/v7 v7.14.0

View File

@@ -638,10 +638,16 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error)
// - Escaped "@": strings starting with "@@" (literal "@" at start) // - Escaped "@": strings starting with "@@" (literal "@" at start)
// - File reference: "@/path/to/file" or "@./relative/path" // - File reference: "@/path/to/file" or "@./relative/path"
// - URL reference: "@http://..." or "@https://..." // - URL reference: "@http://..." or "@https://..."
//
// It can return the following errors:
// - types.ErrScriptEmpty
// - types.ErrScriptSourceEmpty
// - types.ErrScriptURLNoHost
// - types.URLParseError
func validateScriptSource(script string) error { func validateScriptSource(script string) error {
// Empty script is invalid // Empty script is invalid
if script == "" { if script == "" {
return errors.New("script cannot be empty") return types.ErrScriptEmpty
} }
// Not a file/URL reference - it's an inline script // Not a file/URL reference - it's an inline script
@@ -658,17 +664,17 @@ func validateScriptSource(script string) error {
source := script[1:] // Remove the @ prefix source := script[1:] // Remove the @ prefix
if source == "" { if source == "" {
return errors.New("script source cannot be empty after @") return types.ErrScriptSourceEmpty
} }
// Check if it's a URL // Check if it's a http(s) URL
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
parsedURL, err := url.Parse(source) parsedURL, err := url.Parse(source)
if err != nil { if err != nil {
return fmt.Errorf("invalid URL: %w", err) return types.NewURLParseError(source, err)
} }
if parsedURL.Host == "" { if parsedURL.Host == "" {
return errors.New("URL must have a host") return types.ErrScriptURLNoHost
} }
return nil return nil
} }

View File

@@ -49,6 +49,10 @@ func (parser ConfigFileParser) Parse() (*Config, error) {
} }
// fetchFile retrieves file contents from a local path or HTTP/HTTPS URL. // fetchFile retrieves file contents from a local path or HTTP/HTTPS URL.
// It can return the following errors:
// - types.FileReadError
// - types.HTTPFetchError
// - types.HTTPStatusError
func fetchFile(ctx context.Context, src string) ([]byte, error) { func fetchFile(ctx context.Context, src string) ([]byte, error) {
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
return fetchHTTP(ctx, src) return fetchHTTP(ctx, src)
@@ -57,25 +61,28 @@ func fetchFile(ctx context.Context, src string) ([]byte, error) {
} }
// fetchHTTP downloads file contents from an HTTP/HTTPS URL. // fetchHTTP downloads file contents from an HTTP/HTTPS URL.
// It can return the following errors:
// - types.HTTPFetchError
// - types.HTTPStatusError
func fetchHTTP(ctx context.Context, url string) ([]byte, error) { func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err) return nil, types.NewHTTPFetchError(url, err)
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch file: %w", err) return nil, types.NewHTTPFetchError(url, err)
} }
defer resp.Body.Close() //nolint:errcheck defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch file: HTTP %d %s", resp.StatusCode, resp.Status) return nil, types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err) return nil, types.NewHTTPFetchError(url, err)
} }
return data, nil return data, nil
@@ -83,19 +90,21 @@ func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
// fetchLocal reads file contents from the local filesystem. // fetchLocal reads file contents from the local filesystem.
// It resolves relative paths from the current working directory. // It resolves relative paths from the current working directory.
// It can return the following errors:
// - types.FileReadError
func fetchLocal(src string) ([]byte, error) { func fetchLocal(src string) ([]byte, error) {
path := src path := src
if !filepath.IsAbs(src) { if !filepath.IsAbs(src) {
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err) return nil, types.NewFileReadError(src, err)
} }
path = filepath.Join(pwd, src) path = filepath.Join(pwd, src)
} }
data, err := os.ReadFile(path) //nolint:gosec data, err := os.ReadFile(path) //nolint:gosec
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err) return nil, types.NewFileReadError(path, err)
} }
return data, nil return data, nil

View File

@@ -8,6 +8,8 @@ import (
"go.aykhans.me/sarin/internal/types" "go.aykhans.me/sarin/internal/types"
) )
// It can return the following errors:
// - types.TemplateParseError
func validateTemplateString(value string, funcMap template.FuncMap) error { func validateTemplateString(value string, funcMap template.FuncMap) error {
if value == "" { if value == "" {
return nil return nil
@@ -15,7 +17,7 @@ func validateTemplateString(value string, funcMap template.FuncMap) error {
_, err := template.New("").Funcs(funcMap).Parse(value) _, err := template.New("").Funcs(funcMap).Parse(value)
if err != nil { if err != nil {
return fmt.Errorf("template parse error: %w", err) return types.NewTemplateParseError(err)
} }
return nil return nil

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors"
"math" "math"
"net" "net"
"net/http" "net/http"
@@ -95,6 +94,9 @@ func NewHostClients(
return []*fasthttp.HostClient{client}, nil return []*fasthttp.HostClient{client}, nil
} }
// NewProxyDialFunc creates a dial function for the given proxy URL.
// It can return the following errors:
// - types.ProxyUnsupportedSchemeError
func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
var ( var (
dialer fasthttp.DialFunc dialer fasthttp.DialFunc
@@ -117,16 +119,14 @@ func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Durat
case "https": case "https":
dialer = fasthttpHTTPSDialerDualStackTimeout(proxyURL, timeout) dialer = fasthttpHTTPSDialerDualStackTimeout(proxyURL, timeout)
default: default:
return nil, errors.New("unsupported proxy scheme") return nil, types.NewProxyUnsupportedSchemeError(proxyURL.Scheme)
}
if dialer == nil {
return nil, errors.New("internal error: proxy dialer is nil")
} }
return dialer, nil return dialer, nil
} }
// The returned dial function can return the following errors:
// - types.ProxyDialError
func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL, timeout time.Duration, resolveLocally bool) (fasthttp.DialFunc, error) { func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL, timeout time.Duration, resolveLocally bool) (fasthttp.DialFunc, error) {
netDialer := &net.Dialer{} netDialer := &net.Dialer{}
@@ -147,12 +147,18 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
return nil, err return nil, err
} }
proxyStr := proxyURL.String()
// Assert to ContextDialer for timeout support // Assert to ContextDialer for timeout support
contextDialer, ok := socksDialer.(proxy.ContextDialer) contextDialer, ok := socksDialer.(proxy.ContextDialer)
if !ok { if !ok {
// Fallback without timeout (should not happen with net.Dialer) // Fallback without timeout (should not happen with net.Dialer)
return func(addr string) (net.Conn, error) { return func(addr string) (net.Conn, error) {
return socksDialer.Dial("tcp", addr) conn, err := socksDialer.Dial("tcp", addr)
if err != nil {
return nil, types.NewProxyDialError(proxyStr, err)
}
return conn, nil
}, nil }, nil
} }
@@ -163,7 +169,7 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
if resolveLocally { if resolveLocally {
host, port, err := net.SplitHostPort(addr) host, port, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
// Cap DNS resolution to half the timeout to reserve time for dial // Cap DNS resolution to half the timeout to reserve time for dial
@@ -171,10 +177,10 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host) ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
dnsCancel() dnsCancel()
if err != nil { if err != nil {
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
if len(ips) == 0 { if len(ips) == 0 {
return nil, errors.New("no IP addresses found for host: " + host) return nil, types.NewProxyDialError(proxyStr, types.NewProxyResolveError(host))
} }
// Use the first resolved IP // Use the first resolved IP
@@ -184,16 +190,22 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
// Use remaining time for dial // Use remaining time for dial
remaining := time.Until(deadline) remaining := time.Until(deadline)
if remaining <= 0 { if remaining <= 0 {
return nil, context.DeadlineExceeded return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
} }
dialCtx, dialCancel := context.WithTimeout(ctx, remaining) dialCtx, dialCancel := context.WithTimeout(ctx, remaining)
defer dialCancel() defer dialCancel()
return contextDialer.DialContext(dialCtx, "tcp", addr) conn, err := contextDialer.DialContext(dialCtx, "tcp", addr)
if err != nil {
return nil, types.NewProxyDialError(proxyStr, err)
}
return conn, nil
}, nil }, nil
} }
// The returned dial function can return the following errors:
// - types.ProxyDialError
func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duration) fasthttp.DialFunc { func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duration) fasthttp.DialFunc {
proxyAddr := proxyURL.Host proxyAddr := proxyURL.Host
if proxyURL.Port() == "" { if proxyURL.Port() == "" {
@@ -209,24 +221,26 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials)) proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
} }
proxyStr := proxyURL.String()
return func(addr string) (net.Conn, error) { return func(addr string) (net.Conn, error) {
// Establish TCP connection to proxy with timeout // Establish TCP connection to proxy with timeout
start := time.Now() start := time.Now()
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout) conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
if err != nil { if err != nil {
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
remaining := timeout - time.Since(start) remaining := timeout - time.Since(start)
if remaining <= 0 { if remaining <= 0 {
conn.Close() //nolint:errcheck,gosec conn.Close() //nolint:errcheck,gosec
return nil, context.DeadlineExceeded return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
} }
// Set deadline for the TLS handshake and CONNECT request // Set deadline for the TLS handshake and CONNECT request
if err := conn.SetDeadline(time.Now().Add(remaining)); err != nil { if err := conn.SetDeadline(time.Now().Add(remaining)); err != nil {
conn.Close() //nolint:errcheck,gosec conn.Close() //nolint:errcheck,gosec
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
// Upgrade to TLS // Upgrade to TLS
@@ -235,7 +249,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
}) })
if err := tlsConn.Handshake(); err != nil { if err := tlsConn.Handshake(); err != nil {
tlsConn.Close() //nolint:errcheck,gosec tlsConn.Close() //nolint:errcheck,gosec
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
// Build and send CONNECT request // Build and send CONNECT request
@@ -251,7 +265,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
if err := connectReq.Write(tlsConn); err != nil { if err := connectReq.Write(tlsConn); err != nil {
tlsConn.Close() //nolint:errcheck,gosec tlsConn.Close() //nolint:errcheck,gosec
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
// Read response using buffered reader, but return wrapped connection // Read response using buffered reader, but return wrapped connection
@@ -260,19 +274,19 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
resp, err := http.ReadResponse(bufReader, connectReq) resp, err := http.ReadResponse(bufReader, connectReq)
if err != nil { if err != nil {
tlsConn.Close() //nolint:errcheck,gosec tlsConn.Close() //nolint:errcheck,gosec
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
resp.Body.Close() //nolint:errcheck,gosec resp.Body.Close() //nolint:errcheck,gosec
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
tlsConn.Close() //nolint:errcheck,gosec tlsConn.Close() //nolint:errcheck,gosec
return nil, errors.New("proxy CONNECT failed: " + resp.Status) return nil, types.NewProxyDialError(proxyStr, types.NewProxyConnectError(resp.Status))
} }
// Clear deadline for the tunneled connection // Clear deadline for the tunneled connection
if err := tlsConn.SetDeadline(time.Time{}); err != nil { if err := tlsConn.SetDeadline(time.Time{}); err != nil {
tlsConn.Close() //nolint:errcheck,gosec tlsConn.Close() //nolint:errcheck,gosec
return nil, err return nil, types.NewProxyDialError(proxyStr, err)
} }
// Return wrapped connection that uses the buffered reader // Return wrapped connection that uses the buffered reader

View File

@@ -1,7 +1,6 @@
package sarin package sarin
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@@ -10,6 +9,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"go.aykhans.me/sarin/internal/types"
) )
// CachedFile holds the cached content and metadata of a file. // CachedFile holds the cached content and metadata of a file.
@@ -31,6 +32,10 @@ func NewFileCache(requestTimeout time.Duration) *FileCache {
// GetOrLoad retrieves a file from cache or loads it using the provided source. // GetOrLoad retrieves a file from cache or loads it using the provided source.
// The source can be a local file path or an HTTP/HTTPS URL. // The source can be a local file path or an HTTP/HTTPS URL.
// It can return the following errors:
// - types.FileReadError
// - types.HTTPFetchError
// - types.HTTPStatusError
func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) { func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) {
if val, ok := fc.cache.Load(source); ok { if val, ok := fc.cache.Load(source); ok {
return val.(*CachedFile), nil return val.(*CachedFile), nil
@@ -59,14 +64,21 @@ func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) {
return actual.(*CachedFile), nil return actual.(*CachedFile), nil
} }
// readLocalFile reads a file from the local filesystem and returns its content and filename.
// It can return the following errors:
// - types.FileReadError
func (fc *FileCache) readLocalFile(filePath string) ([]byte, string, error) { func (fc *FileCache) readLocalFile(filePath string) ([]byte, string, error) {
content, err := os.ReadFile(filePath) //nolint:gosec content, err := os.ReadFile(filePath) //nolint:gosec
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to read file %s: %w", filePath, err) return nil, "", types.NewFileReadError(filePath, err)
} }
return content, filepath.Base(filePath), nil return content, filepath.Base(filePath), nil
} }
// fetchURL downloads file contents from an HTTP/HTTPS URL.
// It can return the following errors:
// - types.HTTPFetchError
// - types.HTTPStatusError
func (fc *FileCache) fetchURL(url string) ([]byte, string, error) { func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
client := &http.Client{ client := &http.Client{
Timeout: fc.requestTimeout, Timeout: fc.requestTimeout,
@@ -74,17 +86,17 @@ func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to fetch URL %s: %w", url, err) return nil, "", types.NewHTTPFetchError(url, err)
} }
defer resp.Body.Close() //nolint:errcheck defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("failed to fetch URL %s: HTTP %d", url, resp.StatusCode) return nil, "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
} }
content, err := io.ReadAll(resp.Body) content, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to read response body from %s: %w", url, err) return nil, "", types.NewHTTPFetchError(url, err)
} }
// Extract filename from URL path // Extract filename from URL path

View File

@@ -2,7 +2,6 @@ package sarin
import ( import (
"bytes" "bytes"
"fmt"
"maps" "maps"
"math/rand/v2" "math/rand/v2"
"net/url" "net/url"
@@ -261,12 +260,12 @@ func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap)
for _, generator := range generators { for _, generator := range generators {
rendered, err = generator(nil) rendered, err = generator(nil)
if err != nil { if err != nil {
return valuesData{}, fmt.Errorf("values rendering: %w", err) return valuesData{}, types.NewTemplateRenderError(err)
} }
data, err = godotenv.Unmarshal(rendered) data, err = godotenv.Unmarshal(rendered)
if err != nil { if err != nil {
return valuesData{}, fmt.Errorf("values rendering: %w", err) return valuesData{}, types.NewTemplateRenderError(err)
} }
maps.Copy(result, data) maps.Copy(result, data)
@@ -283,7 +282,7 @@ func createTemplateFunc(value string, templateFunctions template.FuncMap) (func(
return func(data any) (string, error) { return func(data any) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err = tmpl.Execute(&buf, data); err != nil { if err = tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("template rendering: %w", err) return "", types.NewTemplateRenderError(err)
} }
return buf.String(), nil return buf.String(), nil
}, true }, true

View File

@@ -58,8 +58,9 @@ type sarin struct {
// NewSarin creates a new sarin instance for load testing. // NewSarin creates a new sarin instance for load testing.
// It can return the following errors: // It can return the following errors:
// - types.ProxyDialError // - types.ProxyDialError
// - script loading errors // - types.ErrScriptEmpty
// - types.ScriptLoadError
func NewSarin( func NewSarin(
ctx context.Context, ctx context.Context,
methods []string, methods []string,
@@ -216,7 +217,8 @@ func (q sarin) Worker(
// Scripts are pre-validated in NewSarin, so this should not fail // Scripts are pre-validated in NewSarin, so this should not fail
var scriptTransformer *script.Transformer var scriptTransformer *script.Transformer
if !q.scriptChain.IsEmpty() { if !q.scriptChain.IsEmpty() {
scriptTransformer, err := q.scriptChain.NewTransformer() var err error
scriptTransformer, err = q.scriptChain.NewTransformer()
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -3,7 +3,6 @@ package sarin
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"math/rand/v2" "math/rand/v2"
"mime/multipart" "mime/multipart"
"strings" "strings"
@@ -12,6 +11,7 @@ import (
"time" "time"
"github.com/brianvoe/gofakeit/v7" "github.com/brianvoe/gofakeit/v7"
"go.aykhans.me/sarin/internal/types"
) )
func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) template.FuncMap { func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) template.FuncMap {
@@ -90,7 +90,7 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
// {{ file_Base64 "https://example.com/image.png" }} // {{ file_Base64 "https://example.com/image.png" }}
"file_Base64": func(source string) (string, error) { "file_Base64": func(source string) (string, error) {
if fileCache == nil { if fileCache == nil {
return "", errors.New("file cache is not initialized") return "", types.ErrFileCacheNotInitialized
} }
cached, err := fileCache.GetOrLoad(source) cached, err := fileCache.GetOrLoad(source)
if err != nil { if err != nil {
@@ -582,7 +582,7 @@ func NewDefaultBodyTemplateFuncMap(
// {{ body_FormData "name" "John" "avatar" "@/path/to/photo.jpg" "doc" "@https://example.com/file.pdf" }} // {{ body_FormData "name" "John" "avatar" "@/path/to/photo.jpg" "doc" "@https://example.com/file.pdf" }}
funcMap["body_FormData"] = func(pairs ...string) (string, error) { funcMap["body_FormData"] = func(pairs ...string) (string, error) {
if len(pairs)%2 != 0 { if len(pairs)%2 != 0 {
return "", errors.New("body_FormData requires an even number of arguments (key-value pairs)") return "", types.ErrFormDataOddArgs
} }
var multipartData bytes.Buffer var multipartData bytes.Buffer
@@ -602,7 +602,7 @@ func NewDefaultBodyTemplateFuncMap(
case strings.HasPrefix(val, "@"): case strings.HasPrefix(val, "@"):
// File (local path or remote URL) // File (local path or remote URL)
if fileCache == nil { if fileCache == nil {
return "", errors.New("file cache is not initialized") return "", types.ErrFileCacheNotInitialized
} }
source := val[1:] source := val[1:]
cached, err := fileCache.GetOrLoad(source) cached, err := fileCache.GetOrLoad(source)

View File

@@ -1,9 +1,8 @@
package script package script
import ( import (
"fmt"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"go.aykhans.me/sarin/internal/types"
) )
// Chain holds the loaded script sources and can create engine instances. // 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. // NewTransformer creates engine instances from the chain's sources.
// Call this once per worker goroutine. // Call this once per worker goroutine.
// It can return the following errors:
// - types.ScriptChainError
func (c *Chain) NewTransformer() (*Transformer, error) { func (c *Chain) NewTransformer() (*Transformer, error) {
if c.IsEmpty() { if c.IsEmpty() {
return &Transformer{}, nil return &Transformer{}, nil
@@ -51,7 +52,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
engine, err := NewLuaEngine(src.Content) engine, err := NewLuaEngine(src.Content)
if err != nil { if err != nil {
t.Close() // Clean up already created engines 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) t.luaEngines = append(t.luaEngines, engine)
} }
@@ -61,7 +62,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
engine, err := NewJsEngine(src.Content) engine, err := NewJsEngine(src.Content)
if err != nil { if err != nil {
t.Close() // Clean up already created engines 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) t.jsEngines = append(t.jsEngines, engine)
} }
@@ -71,18 +72,20 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
// Transform applies all scripts to the request data. // Transform applies all scripts to the request data.
// Lua scripts run first, then JavaScript scripts. // Lua scripts run first, then JavaScript scripts.
// It can return the following errors:
// - types.ScriptChainError
func (t *Transformer) Transform(req *RequestData) error { func (t *Transformer) Transform(req *RequestData) error {
// Run Lua scripts // Run Lua scripts
for i, engine := range t.luaEngines { for i, engine := range t.luaEngines {
if err := engine.Transform(req); err != nil { 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 // Run JS scripts
for i, engine := range t.jsEngines { for i, engine := range t.jsEngines {
if err := engine.Transform(req); err != nil { if err := engine.Transform(req); err != nil {
return fmt.Errorf("js script[%d]: %w", i, err) return types.NewScriptChainError("js", i, err)
} }
} }

View File

@@ -2,9 +2,9 @@ package script
import ( import (
"errors" "errors"
"fmt"
"github.com/dop251/goja" "github.com/dop251/goja"
"go.aykhans.me/sarin/internal/types"
) )
// JsEngine implements the Engine interface using goja (JavaScript). // JsEngine implements the Engine interface using goja (JavaScript).
@@ -20,27 +20,31 @@ type JsEngine struct {
// Example JavaScript script: // Example JavaScript script:
// //
// function transform(req) { // function transform(req) {
// req.headers["X-Custom"] = "value"; // req.headers["X-Custom"] = ["value"];
// return req; // return req;
// } // }
//
// It can return the following errors:
// - types.ErrScriptTransformMissing
// - types.ScriptExecutionError
func NewJsEngine(scriptContent string) (*JsEngine, error) { func NewJsEngine(scriptContent string) (*JsEngine, error) {
vm := goja.New() vm := goja.New()
// Execute the script to define the transform function // Execute the script to define the transform function
_, err := vm.RunString(scriptContent) _, err := vm.RunString(scriptContent)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute JavaScript script: %w", err) return nil, types.NewScriptExecutionError("JavaScript", err)
} }
// Get the transform function // Get the transform function
transformVal := vm.Get("transform") transformVal := vm.Get("transform")
if transformVal == nil || goja.IsUndefined(transformVal) || goja.IsNull(transformVal) { 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) transform, ok := goja.AssertFunction(transformVal)
if !ok { 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{ return &JsEngine{
@@ -50,6 +54,8 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
} }
// Transform executes the JavaScript transform function with the given request data. // 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 { func (e *JsEngine) Transform(req *RequestData) error {
// Convert RequestData to JavaScript object // Convert RequestData to JavaScript object
reqObj := e.requestDataToObject(req) reqObj := e.requestDataToObject(req)
@@ -57,12 +63,12 @@ func (e *JsEngine) Transform(req *RequestData) error {
// Call transform(req) // Call transform(req)
result, err := e.transform(goja.Undefined(), reqObj) result, err := e.transform(goja.Undefined(), reqObj)
if err != nil { if err != nil {
return fmt.Errorf("JavaScript transform error: %w", err) return types.NewScriptExecutionError("JavaScript", err)
} }
// Update RequestData from the returned object // Update RequestData from the returned object
if err := e.objectToRequestData(result, req); err != nil { 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 return nil
@@ -111,12 +117,12 @@ func (e *JsEngine) requestDataToObject(req *RequestData) goja.Value {
// objectToRequestData updates RequestData from a JavaScript object. // objectToRequestData updates RequestData from a JavaScript object.
func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error { func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) { 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) obj := val.ToObject(e.runtime)
if obj == nil { if obj == nil {
return errors.New("transform function must return an object") return types.ErrScriptTransformReturnObject
} }
// Method // Method
@@ -159,7 +165,7 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
// stringSliceToArray converts a Go []string to a JavaScript array. // stringSliceToArray converts a Go []string to a JavaScript array.
func (e *JsEngine) stringSliceToArray(values []string) *goja.Object { func (e *JsEngine) stringSliceToArray(values []string) *goja.Object {
ifaces := make([]interface{}, len(values)) ifaces := make([]any, len(values))
for i, v := range values { for i, v := range values {
ifaces[i] = v ifaces[i] = v
} }
@@ -181,7 +187,7 @@ func (e *JsEngine) objectToStringSliceMap(obj *goja.Object) map[string][]string
} }
// Check if it's an array // Check if it's an array
if arr, ok := v.Export().([]interface{}); ok { if arr, ok := v.Export().([]any); ok {
var values []string var values []string
for _, item := range arr { for _, item := range arr {
if s, ok := item.(string); ok { if s, ok := item.(string); ok {

View File

@@ -1,10 +1,10 @@
package script package script
import ( import (
"errors"
"fmt" "fmt"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"go.aykhans.me/sarin/internal/types"
) )
// LuaEngine implements the Engine interface using gopher-lua. // LuaEngine implements the Engine interface using gopher-lua.
@@ -20,23 +20,27 @@ type LuaEngine struct {
// Example Lua script: // Example Lua script:
// //
// function transform(req) // function transform(req)
// req.headers["X-Custom"] = "value" // req.headers["X-Custom"] = {"value"}
// return req // return req
// end // end
//
// It can return the following errors:
// - types.ErrScriptTransformMissing
// - types.ScriptExecutionError
func NewLuaEngine(scriptContent string) (*LuaEngine, error) { func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
L := lua.NewState() L := lua.NewState()
// Execute the script to define the transform function // Execute the script to define the transform function
if err := L.DoString(scriptContent); err != nil { if err := L.DoString(scriptContent); err != nil {
L.Close() L.Close()
return nil, fmt.Errorf("failed to execute Lua script: %w", err) return nil, types.NewScriptExecutionError("Lua", err)
} }
// Get the transform function // Get the transform function
transform := L.GetGlobal("transform") transform := L.GetGlobal("transform")
if transform.Type() != lua.LTFunction { if transform.Type() != lua.LTFunction {
L.Close() L.Close()
return nil, errors.New("script must define a global 'transform' function") return nil, types.ErrScriptTransformMissing
} }
return &LuaEngine{ return &LuaEngine{
@@ -46,6 +50,8 @@ func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
} }
// Transform executes the Lua transform function with the given request data. // 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 { func (e *LuaEngine) Transform(req *RequestData) error {
// Convert RequestData to Lua table // Convert RequestData to Lua table
reqTable := e.requestDataToTable(req) reqTable := e.requestDataToTable(req)
@@ -54,7 +60,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
e.state.Push(e.transform) e.state.Push(e.transform)
e.state.Push(reqTable) e.state.Push(reqTable)
if err := e.state.PCall(1, 1, nil); err != nil { 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 // Get the result
@@ -62,7 +68,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
e.state.Pop(1) e.state.Pop(1)
if result.Type() != lua.LTTable { 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 // Update RequestData from the returned table

View File

@@ -2,14 +2,14 @@ package script
import ( import (
"context" "context"
"errors"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"go.aykhans.me/sarin/internal/types"
) )
// RequestData represents the request data passed to scripts for transformation. // 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 @) // - Escaped "@": strings starting with "@@" (literal "@" at start, returns string without first @)
// - File reference: "@/path/to/file" or "@./relative/path" // - File reference: "@/path/to/file" or "@./relative/path"
// - URL reference: "@http://..." or "@https://..." // - 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) { func LoadSource(ctx context.Context, source string, engineType EngineType) (*Source, error) {
if source == "" { if source == "" {
return nil, errors.New("script source cannot be empty") return nil, types.ErrScriptEmpty
} }
var content string var content string
@@ -77,7 +81,7 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
content, err = readFile(ref) content, err = readFile(ref)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load script from %q: %w", ref, err) return nil, types.NewScriptLoadError(ref, err)
} }
default: default:
// Inline script // Inline script
@@ -91,12 +95,15 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
} }
// LoadSources loads multiple script sources. // 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) { func LoadSources(ctx context.Context, sources []string, engineType EngineType) ([]*Source, error) {
loaded := make([]*Source, 0, len(sources)) loaded := make([]*Source, 0, len(sources))
for i, src := range sources { for _, src := range sources {
source, err := LoadSource(ctx, src, engineType) source, err := LoadSource(ctx, src, engineType)
if err != nil { if err != nil {
return nil, fmt.Errorf("script[%d]: %w", i, err) return nil, err
} }
loaded = append(loaded, source) 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. // ValidateScript validates a script source by loading it and checking syntax.
// It loads the script (from file/URL/inline), parses it, and verifies // It loads the script (from file/URL/inline), parses it, and verifies
// that a 'transform' function is defined. // 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 { func ValidateScript(ctx context.Context, source string, engineType EngineType) error {
// Load the script source // Load the script source
src, err := LoadSource(ctx, source, engineType) src, err := LoadSource(ctx, source, engineType)
@@ -121,7 +134,7 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
case EngineTypeJavaScript: case EngineTypeJavaScript:
engine, err = NewJsEngine(src.Content) engine, err = NewJsEngine(src.Content)
default: default:
return fmt.Errorf("unknown engine type: %s", engineType) return types.NewScriptUnknownEngineError(string(engineType))
} }
if err != nil { if err != nil {
@@ -134,56 +147,67 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
} }
// ValidateScripts validates multiple script sources. // 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 { 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 { if err := ValidateScript(ctx, src, engineType); err != nil {
return fmt.Errorf("script[%d]: %w", i, err) return err
} }
} }
return nil return nil
} }
// fetchURL downloads content from an HTTP/HTTPS URL. // 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) { func fetchURL(ctx context.Context, url string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create request: %w", err) return "", types.NewHTTPFetchError(url, err)
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch: %w", err) return "", types.NewHTTPFetchError(url, err)
} }
defer resp.Body.Close() //nolint:errcheck defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK { 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) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to read response: %w", err) return "", types.NewHTTPFetchError(url, err)
} }
return string(data), nil return string(data), nil
} }
// readFile reads content from a local file. // readFile reads content from a local file.
// It can return the following errors:
// - types.FileReadError
func readFile(path string) (string, error) { func readFile(path string) (string, error) {
if !filepath.IsAbs(path) { if !filepath.IsAbs(path) {
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get working directory: %w", err) return "", types.NewFileReadError(path, err)
} }
path = filepath.Join(pwd, path) path = filepath.Join(pwd, path)
} }
data, err := os.ReadFile(path) //nolint:gosec data, err := os.ReadFile(path) //nolint:gosec
if err != nil { if err != nil {
return "", fmt.Errorf("failed to read file: %w", err) return "", types.NewFileReadError(path, err)
} }
return string(data), nil return string(data), nil

View File

@@ -6,16 +6,12 @@ import (
"strings" "strings"
) )
var (
// General
ErrNoError = errors.New("no error (internal)")
// CLI
ErrCLINoArgs = errors.New("CLI expects arguments but received none")
)
// ======================================== General ======================================== // ======================================== General ========================================
var (
ErrNoError = errors.New("no error (internal)")
)
type FieldParseError struct { type FieldParseError struct {
Field string Field string
Value string Value string
@@ -131,8 +127,147 @@ func (e UnmarshalError) Unwrap() error {
return e.error return e.error
} }
// ======================================== General I/O ========================================
type FileReadError struct {
Path string
Err error
}
func NewFileReadError(path string, err error) FileReadError {
if err == nil {
err = ErrNoError
}
return FileReadError{path, err}
}
func (e FileReadError) Error() string {
return fmt.Sprintf("failed to read file %s: %v", e.Path, e.Err)
}
func (e FileReadError) Unwrap() error {
return e.Err
}
type HTTPFetchError struct {
URL string
Err error
}
func NewHTTPFetchError(url string, err error) HTTPFetchError {
if err == nil {
err = ErrNoError
}
return HTTPFetchError{url, err}
}
func (e HTTPFetchError) Error() string {
return fmt.Sprintf("failed to fetch %s: %v", e.URL, e.Err)
}
func (e HTTPFetchError) Unwrap() error {
return e.Err
}
type HTTPStatusError struct {
URL string
StatusCode int
Status string
}
func NewHTTPStatusError(url string, statusCode int, status string) HTTPStatusError {
return HTTPStatusError{url, statusCode, status}
}
func (e HTTPStatusError) Error() string {
return fmt.Sprintf("HTTP %d %s (url: %s)", e.StatusCode, e.Status, e.URL)
}
type URLParseError struct {
URL string
Err error
}
func NewURLParseError(url string, err error) URLParseError {
if err == nil {
err = ErrNoError
}
return URLParseError{url, err}
}
func (e URLParseError) Error() string {
return fmt.Sprintf("invalid URL %q: %v", e.URL, e.Err)
}
func (e URLParseError) Unwrap() error {
return e.Err
}
// ======================================== Template ========================================
var (
ErrFileCacheNotInitialized = errors.New("file cache is not initialized")
ErrFormDataOddArgs = errors.New("body_FormData requires an even number of arguments (key-value pairs)")
)
type TemplateParseError struct {
Err error
}
func NewTemplateParseError(err error) TemplateParseError {
if err == nil {
err = ErrNoError
}
return TemplateParseError{err}
}
func (e TemplateParseError) Error() string {
return "template parse error: " + e.Err.Error()
}
func (e TemplateParseError) Unwrap() error {
return e.Err
}
type TemplateRenderError struct {
Err error
}
func NewTemplateRenderError(err error) TemplateRenderError {
if err == nil {
err = ErrNoError
}
return TemplateRenderError{err}
}
func (e TemplateRenderError) Error() string {
return "template rendering: " + e.Err.Error()
}
func (e TemplateRenderError) Unwrap() error {
return e.Err
}
// ======================================== YAML ========================================
type YAMLFormatError struct {
Detail string
}
func NewYAMLFormatError(detail string) YAMLFormatError {
return YAMLFormatError{detail}
}
func (e YAMLFormatError) Error() string {
return e.Detail
}
// ======================================== CLI ======================================== // ======================================== CLI ========================================
var (
ErrCLINoArgs = errors.New("CLI expects arguments but received none")
)
type CLIUnexpectedArgsError struct { type CLIUnexpectedArgsError struct {
Args []string Args []string
} }
@@ -168,6 +303,61 @@ func (e ConfigFileReadError) Unwrap() error {
// ======================================== Proxy ======================================== // ======================================== Proxy ========================================
type ProxyUnsupportedSchemeError struct {
Scheme string
}
func NewProxyUnsupportedSchemeError(scheme string) ProxyUnsupportedSchemeError {
return ProxyUnsupportedSchemeError{scheme}
}
func (e ProxyUnsupportedSchemeError) Error() string {
return "unsupported proxy scheme: " + e.Scheme
}
type ProxyParseError struct {
Err error
}
func NewProxyParseError(err error) ProxyParseError {
if err == nil {
err = ErrNoError
}
return ProxyParseError{err}
}
func (e ProxyParseError) Error() string {
return "failed to parse proxy URL: " + e.Err.Error()
}
func (e ProxyParseError) Unwrap() error {
return e.Err
}
type ProxyConnectError struct {
Status string
}
func NewProxyConnectError(status string) ProxyConnectError {
return ProxyConnectError{status}
}
func (e ProxyConnectError) Error() string {
return "proxy CONNECT failed: " + e.Status
}
type ProxyResolveError struct {
Host string
}
func NewProxyResolveError(host string) ProxyResolveError {
return ProxyResolveError{host}
}
func (e ProxyResolveError) Error() string {
return "no IP addresses found for host: " + e.Host
}
type ProxyDialError struct { type ProxyDialError struct {
Proxy string Proxy string
Err error Err error
@@ -187,3 +377,86 @@ func (e ProxyDialError) Error() string {
func (e ProxyDialError) Unwrap() error { func (e ProxyDialError) Unwrap() error {
return e.Err return e.Err
} }
// ======================================== Script ========================================
var (
ErrScriptEmpty = errors.New("script cannot be empty")
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
ErrScriptTransformReturnObject = errors.New("transform function must return an object")
ErrScriptURLNoHost = errors.New("script URL must have a host")
)
type ScriptLoadError struct {
Source string
Err error
}
func NewScriptLoadError(source string, err error) ScriptLoadError {
if err == nil {
err = ErrNoError
}
return ScriptLoadError{source, err}
}
func (e ScriptLoadError) Error() string {
return fmt.Sprintf("failed to load script from %q: %v", e.Source, e.Err)
}
func (e ScriptLoadError) Unwrap() error {
return e.Err
}
type ScriptExecutionError struct {
EngineType string
Err error
}
func NewScriptExecutionError(engineType string, err error) ScriptExecutionError {
if err == nil {
err = ErrNoError
}
return ScriptExecutionError{engineType, err}
}
func (e ScriptExecutionError) Error() string {
return fmt.Sprintf("%s script error: %v", e.EngineType, e.Err)
}
func (e ScriptExecutionError) Unwrap() error {
return e.Err
}
type ScriptChainError struct {
EngineType string
Index int
Err error
}
func NewScriptChainError(engineType string, index int, err error) ScriptChainError {
if err == nil {
err = ErrNoError
}
return ScriptChainError{engineType, index, err}
}
func (e ScriptChainError) Error() string {
return fmt.Sprintf("%s script[%d]: %v", e.EngineType, e.Index, e.Err)
}
func (e ScriptChainError) Unwrap() error {
return e.Err
}
type ScriptUnknownEngineError struct {
EngineType string
}
func NewScriptUnknownEngineError(engineType string) ScriptUnknownEngineError {
return ScriptUnknownEngineError{engineType}
}
func (e ScriptUnknownEngineError) Error() string {
return "unknown engine type: " + e.EngineType
}

View File

@@ -1,7 +1,6 @@
package types package types
import ( import (
"fmt"
"net/url" "net/url"
) )
@@ -17,6 +16,9 @@ func (proxies *Proxies) Append(proxy ...Proxy) {
*proxies = append(*proxies, proxy...) *proxies = append(*proxies, proxy...)
} }
// Parse parses a raw proxy string and appends it to the list.
// It can return the following errors:
// - ProxyParseError
func (proxies *Proxies) Parse(rawValue string) error { func (proxies *Proxies) Parse(rawValue string) error {
parsedProxy, err := ParseProxy(rawValue) parsedProxy, err := ParseProxy(rawValue)
if err != nil { if err != nil {
@@ -27,10 +29,13 @@ func (proxies *Proxies) Parse(rawValue string) error {
return nil return nil
} }
// ParseProxy parses a raw proxy URL string into a Proxy.
// It can return the following errors:
// - ProxyParseError
func ParseProxy(rawValue string) (*Proxy, error) { func ParseProxy(rawValue string) (*Proxy, error) {
urlParsed, err := url.Parse(rawValue) urlParsed, err := url.Parse(rawValue)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL: %w", err) return nil, NewProxyParseError(err)
} }
proxyParsed := Proxy(*urlParsed) proxyParsed := Proxy(*urlParsed)