18 Commits

Author SHA1 Message Date
c299fda79d Merge pull request #175 from aykhans/feat/template-time-crypto-file-read
feat(template): add time/crypto helpers and file_Read function; document new template funcs
2026-02-26 21:53:40 +04:00
1f06b43b06 feat(template): add time/crypto helpers and file_Read function; document new template funcs 2026-02-26 21:50:36 +04:00
e031c8e7a5 Merge pull request #174 from aykhans/dependabot/go_modules/golang.org/x/net-0.51.0
Bump golang.org/x/net from 0.50.0 to 0.51.0
2026-02-26 12:50:22 +04:00
dependabot[bot]
de24f9d4a4 Bump golang.org/x/net from 0.50.0 to 0.51.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.51.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 00:13:56 +00:00
d197e90103 Merge pull request #172 from aykhans/refactor/minor-improvements
Rename Append to Merge, replace strings_Join with slice_Join, and auto-detect non-TTY output
2026-02-15 16:55:40 +04:00
ae054bb3d6 update docs 2026-02-15 16:52:52 +04:00
61af28a3d3 Override Methods and Bodies instead of appending in Config.Merge 2026-02-15 16:27:36 +04:00
665be5d98a update docs 2026-02-15 03:05:03 +04:00
d346067e8a Rename Append to Merge, replace strings_Join with slice_Join, and auto-detect non-TTY output
- Rename Append to Merge on Cookies, Headers, and Params types and simplify Parse to use direct append
- Replace strings_Join(sep, ...values) with slice_Join(slice, sep) for consistency with slice-based template functions
- Auto-enable quiet mode when stdout is not a terminal
- Remove ErrCLINoArgs check to allow running sarin without arguments
- Add -it flag to Docker examples in docs
2026-02-15 02:56:32 +04:00
a3e20cd3d3 Merge pull request #171 from aykhans/dependabot/go_modules/github.com/charmbracelet/bubbles-1.0.0
Bump github.com/charmbracelet/bubbles from 0.21.1 to 1.0.0
2026-02-15 00:23:51 +04:00
6d921cf8e3 Merge pull request #170 from aykhans/dependabot/go_modules/golang.org/x/net-0.50.0
Bump golang.org/x/net from 0.49.0 to 0.50.0
2026-02-15 00:23:34 +04:00
dependabot[bot]
d8b0a1e6a3 Bump golang.org/x/net from 0.49.0 to 0.50.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 20:20:57 +00:00
b21d97192c Merge pull request #169 from aykhans/feat/add-scripting
Add scripting
2026-02-15 00:19:58 +04:00
f0606a0f82 Add Lua and JavaScript scripting documentation 2026-02-14 03:21:52 +04:00
3be8ff218c Replace common.ToPtr with Go 1.26 builtin new and add go fix to CI 2026-02-13 18:56:10 +04:00
7cb49195f8 Bump Go to 1.26.0 and golangci-lint to v2.9.0
Drop GOEXPERIMENT=greenteagc flag as the green tea GC is now the default in Go 1.26.
2026-02-11 22:07:52 +04:00
dependabot[bot]
a154215495 Bump github.com/charmbracelet/bubbles from 0.21.1 to 1.0.0
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.21.1 to 1.0.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.21.1...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 00:14:01 +00:00
c1584eb47b Remove unused functions and unexport internal sentinel error 2026-02-09 00:06:01 +04:00
22 changed files with 526 additions and 202 deletions

View File

@@ -16,8 +16,12 @@ 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.7 go-version: 1.26.0
- name: go fix
run: |
go fix ./...
git diff --exit-code
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.8.0 version: v2.9.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.7" >> $GITHUB_ENV echo "GO_VERSION=1.26.0" >> $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
@@ -53,12 +53,12 @@ jobs:
-X 'go.aykhans.me/sarin/internal/version.GoVersion=$(go version)' \ -X 'go.aykhans.me/sarin/internal/version.GoVersion=$(go version)' \
-s -w" -s -w"
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-linux-amd64 ./cmd/cli/main.go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-linux-amd64 ./cmd/cli/main.go
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-linux-arm64 ./cmd/cli/main.go CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-linux-arm64 ./cmd/cli/main.go
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-darwin-amd64 ./cmd/cli/main.go CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-darwin-amd64 ./cmd/cli/main.go
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-darwin-arm64 ./cmd/cli/main.go CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-darwin-arm64 ./cmd/cli/main.go
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-amd64.exe ./cmd/cli/main.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-amd64.exe ./cmd/cli/main.go
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-arm64.exe ./cmd/cli/main.go CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-arm64.exe ./cmd/cli/main.go
- name: Upload Release Assets - name: Upload Release Assets
if: github.event_name == 'release' || inputs.build_binaries if: github.event_name == 'release' || inputs.build_binaries

View File

@@ -1,7 +1,7 @@
version: "2" version: "2"
run: run:
go: "1.25" go: "1.26"
concurrency: 12 concurrency: 12
linters: linters:

View File

@@ -1,4 +1,4 @@
ARG GO_VERSION=1.25.7 ARG GO_VERSION=1.26.0
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
@@ -12,7 +12,7 @@ RUN --mount=type=bind,source=./go.mod,target=./go.mod \
go mod download go mod download
RUN --mount=type=bind,source=./,target=./ \ RUN --mount=type=bind,source=./,target=./ \
CGO_ENABLED=0 GOEXPERIMENT=greenteagc go build \ CGO_ENABLED=0 go build \
-ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=${VERSION}' \ -ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=${VERSION}' \
-X 'go.aykhans.me/sarin/internal/version.GitCommit=${GIT_COMMIT}' \ -X 'go.aykhans.me/sarin/internal/version.GitCommit=${GIT_COMMIT}' \
-X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)' \ -X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)' \

View File

@@ -22,13 +22,14 @@
Sarin is designed for efficient HTTP load testing with minimal resource consumption. It prioritizes simplicity—features like templating add zero overhead when unused. Sarin is designed for efficient HTTP load testing with minimal resource consumption. It prioritizes simplicity—features like templating add zero overhead when unused.
| ✅ Supported | ❌ Not Supported | | ✅ Supported | ❌ Not Supported |
| ---------------------------------------------------------- | --------------------------------- | | ---------------------------------------------------------- | ------------------------------- |
| High-performance with low memory footprint | Detailed response body analysis | | High-performance with low memory footprint | Detailed response body analysis |
| Long-running duration/count based tests | Extensive response statistics | | Long-running duration/count based tests | Extensive response statistics |
| Dynamic requests via 320+ template functions | Web UI or complex TUI | | Dynamic requests via 320+ template functions | Web UI or complex TUI |
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | Scripting or multi-step scenarios | | Request scripting with Lua and JavaScript | Distributed load testing |
| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC | | Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
| Flexible config (CLI, ENV, YAML) | Plugins / extensions ecosystem |
## Installation ## Installation
@@ -56,12 +57,12 @@ Download the latest binaries from the [releases](https://github.com/aykhans/sari
### Building from Source ### Building from Source
Requires [Go 1.25+](https://golang.org/dl/). Requires [Go 1.26+](https://golang.org/dl/).
```sh ```sh
git clone https://github.com/aykhans/sarin.git && cd sarin git clone https://github.com/aykhans/sarin.git && cd sarin
CGO_ENABLED=0 GOEXPERIMENT=greenteagc go build \ CGO_ENABLED=0 go build \
-ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=dev' \ -ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=dev' \
-X 'go.aykhans.me/sarin/internal/version.GitCommit=$(git rev-parse HEAD)' \ -X 'go.aykhans.me/sarin/internal/version.GitCommit=$(git rev-parse HEAD)' \
-X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)' \ -X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)' \

View File

@@ -3,7 +3,7 @@ version: "3"
vars: vars:
BIN_DIR: ./bin BIN_DIR: ./bin
GOLANGCI_LINT_VERSION: v2.8.0 GOLANGCI_LINT_VERSION: v2.9.0
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}" GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
tasks: tasks:
@@ -11,16 +11,22 @@ tasks:
desc: Run fmt, tidy, and lint. desc: Run fmt, tidy, and lint.
cmds: cmds:
- task: fmt - task: fmt
- task: fix
- task: tidy - task: tidy
- task: lint - task: lint
fmt: fmt:
desc: Run linters desc: Run format
deps: deps:
- install-golangci-lint - install-golangci-lint
cmds: cmds:
- "{{.GOLANGCI}} fmt" - "{{.GOLANGCI}} fmt"
fix:
desc: Run go fix
cmds:
- go fix ./...
tidy: tidy:
desc: Run go mod tidy. desc: Run go mod tidy.
cmds: cmds:
@@ -52,7 +58,7 @@ tasks:
cmds: cmds:
- rm -f {{.OUTPUT}} - rm -f {{.OUTPUT}}
- >- - >-
CGO_ENABLED=0 GOEXPERIMENT=greenteagc go build CGO_ENABLED=0 go build
-ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=$(git describe --tags --always)' -ldflags "-X 'go.aykhans.me/sarin/internal/version.Version=$(git describe --tags --always)'
-X 'go.aykhans.me/sarin/internal/version.GitCommit=$(git rev-parse HEAD)' -X 'go.aykhans.me/sarin/internal/version.GitCommit=$(git rev-parse HEAD)'
-X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'go.aykhans.me/sarin/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)'

View File

@@ -36,6 +36,8 @@ Use `-s` or `--show-config` to see the final merged configuration before sending
| [Cookies](#cookies) | `cookies`<br>(object) | `-cookie` / `-C`<br>(string / []string) | `SARIN_COOKIE`<br>(string) | - | HTTP cookies | | [Cookies](#cookies) | `cookies`<br>(object) | `-cookie` / `-C`<br>(string / []string) | `SARIN_COOKIE`<br>(string) | - | HTTP cookies |
| [Proxy](#proxy) | `proxy`<br>(string / []string) | `-proxy` / `-X`<br>(string / []string) | `SARIN_PROXY`<br>(string) | - | Proxy URL(s) | | [Proxy](#proxy) | `proxy`<br>(string / []string) | `-proxy` / `-X`<br>(string / []string) | `SARIN_PROXY`<br>(string) | - | Proxy URL(s) |
| [Values](#values) | `values`<br>(string / []string) | `-values` / `-V`<br>(string / []string) | `SARIN_VALUES`<br>(string) | - | Template values (key=value) | | [Values](#values) | `values`<br>(string / []string) | `-values` / `-V`<br>(string / []string) | `SARIN_VALUES`<br>(string) | - | Template values (key=value) |
| [Lua](#lua) | `lua`<br>(string / []string) | `-lua`<br>(string / []string) | `SARIN_LUA`<br>(string) | - | Lua script(s) |
| [Js](#js) | `js`<br>(string / []string) | `-js`<br>(string / []string) | `SARIN_JS`<br>(string) | - | JavaScript script(s) |
--- ---
@@ -103,6 +105,12 @@ SARIN_CONFIG_FILE=/config1.yaml sarin -f /config2.yaml -f https://example.com/co
If all four files define `url`, the value from `config3.yaml` wins. If all four files define `url`, the value from `config3.yaml` wins.
**Merge behavior by field:**
- **Scalar fields** (`url`, `requests`, `duration`, `timeout`, `concurrency`, etc.) — higher priority overrides lower priority
- **Method and Body** — higher priority overrides lower priority (no merging)
- **Headers, Params, Cookies, Proxies, Values, Lua, and Js** — accumulated across all config files
## URL ## URL
Target URL. Must be HTTP or HTTPS. The URL path supports [templating](templating.md), allowing dynamic path generation per request. Target URL. Must be HTTP or HTTPS. The URL path supports [templating](templating.md), allowing dynamic path generation per request.
@@ -225,26 +233,33 @@ SARIN_BODY='{"product": "car"}'
## Params ## Params
URL query parameters. If multiple values are provided for a key, Sarin cycles through them in order, starting from a random index for each request. Supports [templating](templating.md). URL query parameters. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:** **YAML example:**
```yaml ```yaml
params: params:
key1: value1 key1: value1
key2: [value2, value3] key2: [value2, value3] # cycles between value2 and value3
# OR # OR
params: params:
- key1: value1 - key1: value1
- key2: [value2, value3] - key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
params:
- key2: value2
- key2: value3 # both sent in every request
``` ```
**CLI example:** **CLI example:**
```sh ```sh
-param "key1=value1" -param "key2=value2" -param "key2=value3" -param "key1=value1" -param "key2=value2" -param "key2=value3" # sends both value2 and value3
``` ```
**ENV example:** **ENV example:**
@@ -255,26 +270,33 @@ SARIN_PARAM="key1=value1"
## Headers ## Headers
HTTP headers. If multiple values are provided for a key, Sarin cycles through them in order, starting from a random index for each request. Supports [templating](templating.md). HTTP headers. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:** **YAML example:**
```yaml ```yaml
headers: headers:
key1: value1 key1: value1
key2: [value2, value3] key2: [value2, value3] # cycles between value2 and value3
# OR # OR
headers: headers:
- key1: value1 - key1: value1
- key2: [value2, value3] - key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
headers:
- key2: value2
- key2: value3 # both sent in every request
``` ```
**CLI example:** **CLI example:**
```sh ```sh
-header "key1: value1" -header "key2: value2" -header "key2: value3" -header "key1: value1" -header "key2: value2" -header "key2: value3" # sends both value2 and value3
``` ```
**ENV example:** **ENV example:**
@@ -285,26 +307,33 @@ SARIN_HEADER="key1: value1"
## Cookies ## Cookies
HTTP cookies. If multiple values are provided for a key, Sarin cycles through them in order, starting from a random index for each request. Supports [templating](templating.md). HTTP cookies. Supports [templating](templating.md).
When the same key appears as **separate entries** (in CLI or config file), all values are sent in every request. When multiple values are specified as an **array on a single key** (config file only), Sarin cycles through them.
**YAML example:** **YAML example:**
```yaml ```yaml
cookies: cookies:
key1: value1 key1: value1
key2: [value2, value3] key2: [value2, value3] # cycles between value2 and value3
# OR # OR
cookies: cookies:
- key1: value1 - key1: value1
- key2: [value2, value3] - key2: [value2, value3] # cycles between value2 and value3
# To send both values in every request, use separate entries:
cookies:
- key2: value2
- key2: value3 # both sent in every request
``` ```
**CLI example:** **CLI example:**
```sh ```sh
-cookie "key1=value1" -cookie "key2=value2" -cookie "key2=value3" -cookie "key1=value1" -cookie "key2=value2" -cookie "key2=value3" # sends both value2 and value3
``` ```
**ENV example:** **ENV example:**
@@ -374,3 +403,133 @@ values: |
```sh ```sh
SARIN_VALUES="key1=value1" SARIN_VALUES="key1=value1"
``` ```
## Lua
Lua script(s) for request transformation. Each script must define a global `transform` function that receives a request object and returns the modified request object. Scripts run after template rendering, before the request is sent.
If multiple Lua scripts are provided, they are chained in order—the output of one becomes the input to the next. When both Lua and JavaScript scripts are specified, all Lua scripts run first, then all JavaScript scripts.
**Script sources:**
Scripts can be provided as:
- **Inline script:** Direct script code
- **File reference:** `@/path/to/script.lua` or `@./relative/path.lua`
- **URL reference:** `@http://...` or `@https://...`
- **Escaped `@`:** `@@...` for inline scripts that start with a literal `@`
**The `transform` function:**
```lua
function transform(req)
-- req.method (string) - HTTP method (e.g. "GET", "POST")
-- req.path (string) - URL path (e.g. "/api/users")
-- req.body (string) - Request body
-- req.headers (table of string/arrays) - HTTP headers (e.g. {["X-Key"] = "value"})
-- req.params (table of string/arrays) - Query parameters (e.g. {["id"] = "123"})
-- req.cookies (table of string/arrays) - Cookies (e.g. {["session"] = "abc"})
req.headers["X-Custom"] = "my-value"
return req
end
```
> **Note:** Header, parameter, and cookie values can be a single string or a table (array) for multiple values per key (e.g. `{"val1", "val2"}`).
**YAML example:**
```yaml
lua: |
function transform(req)
req.headers["X-Custom"] = "my-value"
return req
end
# OR
lua:
- "@/path/to/script1.lua"
- "@/path/to/script2.lua"
```
**CLI example:**
```sh
-lua 'function transform(req) req.headers["X-Custom"] = "my-value" return req end'
# OR
-lua @/path/to/script1.lua -lua @/path/to/script2.lua
```
**ENV example:**
```sh
SARIN_LUA='function transform(req) req.headers["X-Custom"] = "my-value" return req end'
```
## Js
JavaScript script(s) for request transformation. Each script must define a global `transform` function that receives a request object and returns the modified request object. Scripts run after template rendering, before the request is sent.
If multiple JavaScript scripts are provided, they are chained in order—the output of one becomes the input to the next. When both Lua and JavaScript scripts are specified, all Lua scripts run first, then all JavaScript scripts.
**Script sources:**
Scripts can be provided as:
- **Inline script:** Direct script code
- **File reference:** `@/path/to/script.js` or `@./relative/path.js`
- **URL reference:** `@http://...` or `@https://...`
- **Escaped `@`:** `@@...` for inline scripts that start with a literal `@`
**The `transform` function:**
```javascript
function transform(req) {
// req.method (string) - HTTP method (e.g. "GET", "POST")
// req.path (string) - URL path (e.g. "/api/users")
// req.body (string) - Request body
// req.headers (object of string/arrays) - HTTP headers (e.g. {"X-Key": "value"})
// req.params (object of string/arrays) - Query parameters (e.g. {"id": "123"})
// req.cookies (object of string/arrays) - Cookies (e.g. {"session": "abc"})
req.headers["X-Custom"] = "my-value";
return req;
}
```
> **Note:** Header, parameter, and cookie values can be a single string or an array for multiple values per key (e.g. `["val1", "val2"]`).
**YAML example:**
```yaml
js: |
function transform(req) {
req.headers["X-Custom"] = "my-value";
return req;
}
# OR
js:
- "@/path/to/script1.js"
- "@/path/to/script2.js"
```
**CLI example:**
```sh
-js 'function transform(req) { req.headers["X-Custom"] = "my-value"; return req; }'
# OR
-js @/path/to/script1.js -js @/path/to/script2.js
```
**ENV example:**
```sh
SARIN_JS='function transform(req) { req.headers["X-Custom"] = "my-value"; return req; }'
```

View File

@@ -15,6 +15,7 @@ This guide provides practical examples for common Sarin use cases.
- [Docker Usage](#docker-usage) - [Docker Usage](#docker-usage)
- [Dry Run Mode](#dry-run-mode) - [Dry Run Mode](#dry-run-mode)
- [Show Configuration](#show-configuration) - [Show Configuration](#show-configuration)
- [Scripting](#scripting)
--- ---
@@ -133,20 +134,34 @@ headers:
</details> </details>
**Random headers from multiple values:** **Multiple values for the same header (all sent in every request):**
> **Note:** When multiple values are provided for the same header, Sarin starts at a random index and cycles through all values in order. Once the cycle completes, it picks a new random starting point. This ensures all values are used while maintaining some randomness. > **Note:** When the same key appears as separate entries (in CLI or config file), all values are sent in every request.
```sh ```sh
sarin -U http://example.com -r 1000 -c 10 \ sarin -U http://example.com -r 1000 -c 10 \
-H "X-Region: us-east" \ -H "X-Region: us-east" \
-H "X-Region: us-west" \ -H "X-Region: us-west"
-H "X-Region: eu-central"
``` ```
<details> <details>
<summary>YAML equivalent</summary> <summary>YAML equivalent</summary>
```yaml
url: http://example.com
requests: 1000
concurrency: 10
headers:
- X-Region: us-east
- X-Region: us-west
```
</details>
**Cycling headers from multiple values (config file only):**
> **Note:** When multiple values are specified as an array on a single key, Sarin starts at a random index and cycles through all values in order. Once the cycle completes, it picks a new random starting point.
```yaml ```yaml
url: http://example.com url: http://example.com
requests: 1000 requests: 1000
@@ -158,8 +173,6 @@ headers:
- eu-central - eu-central
``` ```
</details>
**Query parameters:** **Query parameters:**
```sh ```sh
@@ -186,7 +199,7 @@ params:
```sh ```sh
sarin -U http://example.com/users -r 1000 -c 10 \ sarin -U http://example.com/users -r 1000 -c 10 \
-P "id={{ fakeit_IntRange 1 1000 }}" \ -P "id={{ fakeit_Number 1 1000 }}" \
-P "fields=name,email" -P "fields=name,email"
``` ```
@@ -198,7 +211,7 @@ url: http://example.com/users
requests: 1000 requests: 1000
concurrency: 10 concurrency: 10
params: params:
id: "{{ fakeit_IntRange 1 1000 }}" id: "{{ fakeit_Number 1 1000 }}"
fields: "name,email" fields: "name,email"
``` ```
@@ -823,19 +836,19 @@ quiet: true
**Basic Docker usage:** **Basic Docker usage:**
```sh ```sh
docker run --rm aykhans/sarin -U http://example.com -r 1000 -c 10 docker run -it --rm aykhans/sarin -U http://example.com -r 1000 -c 10
``` ```
**With local config file:** **With local config file:**
```sh ```sh
docker run --rm -v $(pwd)/config.yaml:/config.yaml aykhans/sarin -f /config.yaml docker run -it --rm -v $(pwd)/config.yaml:/config.yaml aykhans/sarin -f /config.yaml
``` ```
**With remote config file:** **With remote config file:**
```sh ```sh
docker run --rm aykhans/sarin -f https://example.com/config.yaml docker run -it --rm aykhans/sarin -f https://example.com/config.yaml
``` ```
**Interactive mode with TTY:** **Interactive mode with TTY:**
@@ -894,3 +907,124 @@ headers:
``` ```
</details> </details>
## Scripting
Transform requests using Lua or JavaScript scripts. Scripts run after template rendering, before the request is sent.
**Add a custom header with Lua:**
```sh
sarin -U http://example.com/api -r 1000 -c 10 \
-lua 'function transform(req) req.headers["X-Custom"] = "my-value" return req end'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api
requests: 1000
concurrency: 10
lua: |
function transform(req)
req.headers["X-Custom"] = "my-value"
return req
end
```
</details>
**Modify request body with JavaScript:**
```sh
sarin -U http://example.com/api/data -r 1000 -c 10 \
-M POST \
-H "Content-Type: application/json" \
-B '{"name": "test"}' \
-js 'function transform(req) { var body = JSON.parse(req.body); body.timestamp = Date.now(); req.body = JSON.stringify(body); return req; }'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/data
requests: 1000
concurrency: 10
method: POST
headers:
Content-Type: application/json
body: '{"name": "test"}'
js: |
function transform(req) {
var body = JSON.parse(req.body);
body.timestamp = Date.now();
req.body = JSON.stringify(body);
return req;
}
```
</details>
**Load script from a file:**
```sh
sarin -U http://example.com/api -r 1000 -c 10 \
-lua @./scripts/transform.lua
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api
requests: 1000
concurrency: 10
lua: "@./scripts/transform.lua"
```
</details>
**Load script from a URL:**
```sh
sarin -U http://example.com/api -r 1000 -c 10 \
-js @https://example.com/scripts/transform.js
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api
requests: 1000
concurrency: 10
js: "@https://example.com/scripts/transform.js"
```
</details>
**Chain multiple scripts (Lua runs first, then JavaScript):**
```sh
sarin -U http://example.com/api -r 1000 -c 10 \
-lua @./scripts/auth.lua \
-lua @./scripts/headers.lua \
-js @./scripts/body.js
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api
requests: 1000
concurrency: 10
lua:
- "@./scripts/auth.lua"
- "@./scripts/headers.lua"
js: "@./scripts/body.js"
```
</details>

View File

@@ -10,6 +10,8 @@ Sarin supports Go templates in URL paths, methods, bodies, headers, params, cook
- [General Functions](#general-functions) - [General Functions](#general-functions)
- [String Functions](#string-functions) - [String Functions](#string-functions)
- [Collection Functions](#collection-functions) - [Collection Functions](#collection-functions)
- [Time Functions](#time-functions)
- [Crypto Functions](#crypto-functions)
- [Body Functions](#body-functions) - [Body Functions](#body-functions)
- [File Functions](#file-functions) - [File Functions](#file-functions)
- [Fake Data Functions](#fake-data-functions) - [Fake Data Functions](#fake-data-functions)
@@ -98,16 +100,34 @@ sarin -U http://example.com/users \
| `strings_Truncate(s string, n int)` | Truncate to `n` characters with ellipsis | `{{ strings_Truncate "hello world" 5 }}``hello...` | | `strings_Truncate(s string, n int)` | Truncate to `n` characters with ellipsis | `{{ strings_Truncate "hello world" 5 }}``hello...` |
| `strings_TrimPrefix(s string, prefix string)` | Remove prefix from string | `{{ strings_TrimPrefix "hello" "he" }}``llo` | | `strings_TrimPrefix(s string, prefix string)` | Remove prefix from string | `{{ strings_TrimPrefix "hello" "he" }}``llo` |
| `strings_TrimSuffix(s string, suffix string)` | Remove suffix from string | `{{ strings_TrimSuffix "hello" "lo" }}``hel` | | `strings_TrimSuffix(s string, suffix string)` | Remove suffix from string | `{{ strings_TrimSuffix "hello" "lo" }}``hel` |
| `strings_Join(sep string, values ...string)` | Join strings with separator | `{{ strings_Join "-" "a" "b" "c" }}``a-b-c` |
### Collection Functions ### Collection Functions
| Function | Description | Example | | Function | Description | Example |
| ----------------------------- | --------------------------------------------- | -------------------------------------------- | | ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------- |
| `dict_Str(pairs ...string)` | Create string dictionary from key-value pairs | `{{ dict_Str "key1" "val1" "key2" "val2" }}` | | `dict_Str(pairs ...string)` | Create string dictionary from key-value pairs | `{{ dict_Str "key1" "val1" "key2" "val2" }}` |
| `slice_Str(values ...string)` | Create string slice | `{{ slice_Str "a" "b" "c" }}` | | `slice_Str(values ...string)` | Create string slice | `{{ slice_Str "a" "b" "c" }}` |
| `slice_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` | | `slice_Join(slice []string, sep string)` | Join string slice with separator | `{{ slice_Join (slice_Str "a" "b" "c") "-" }}``a-b-c` |
| `slice_Uint(values ...uint)` | Create uint slice | `{{ slice_Uint 1 2 3 }}` | | `slice_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` |
| `slice_Uint(values ...uint)` | Create uint slice | `{{ slice_Uint 1 2 3 }}` |
### Time Functions
| Function | Description | Example |
| ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------- |
| `time_NowUnix` | Current Unix timestamp (seconds) | `{{ time_NowUnix }}``1735689600` |
| `time_NowUnixMilli` | Current Unix timestamp (milliseconds) | `{{ time_NowUnixMilli }}``1735689600123` |
| `time_NowRFC3339` | Current time in RFC3339 format | `{{ time_NowRFC3339 }}``"2026-02-26T21:00:00Z"` |
| `time_Format(layout, t)` | Format a `time.Time` value with a Go layout | `{{ time_Format "2006-01-02" (strings_ToDate "2024-05-10") }}``"2024-05-10"` |
### Crypto Functions
| Function | Description | Example |
| ------------------------------------ | ------------------------------------------ | -------------------------------------------- |
| `crypto_SHA256(s string)` | SHA-256 hash (hex-encoded) | `{{ crypto_SHA256 "hello" }}` |
| `crypto_MD5(s string)` | MD5 hash (hex-encoded) | `{{ crypto_MD5 "hello" }}` |
| `crypto_HMACSHA256(key, msg string)` | HMAC-SHA256 signature (hex-encoded) | `{{ crypto_HMACSHA256 "secret" "payload" }}` |
| `crypto_Base64URL(s string)` | Base64 URL-safe encoding (without padding) | `{{ crypto_Base64URL "hello world" }}` |
### Body Functions ### Body Functions
@@ -153,11 +173,18 @@ body: '{{ body_FormData "twitter" "@@username" }}'
| Function | Description | Example | | Function | Description | Example |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------- | | ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `file_Read(source string)` | Read a file (local path or URL) and return raw content as string. Files are cached after first read. | `{{ file_Read "/path/to/file.txt" }}` |
| `file_Base64(source string)` | Read a file (local path or URL) and return its Base64 encoded content. Files are cached after first read. | `{{ file_Base64 "/path/to/file.pdf" }}` | | `file_Base64(source string)` | Read a file (local path or URL) and return its Base64 encoded content. Files are cached after first read. | `{{ file_Base64 "/path/to/file.pdf" }}` |
**`file_Base64` Details:** **`file_Read` and `file_Base64` Details:**
```yaml ```yaml
# Local file as plain text
body: '{{ file_Read "/path/to/template.json" }}'
# Remote text file
body: '{{ file_Read "https://example.com/payload.txt" }}'
# Local file as Base64 in JSON body # Local file as Base64 in JSON body
body: '{"file": "{{ file_Base64 "/path/to/document.pdf" }}", "filename": "document.pdf"}' body: '{"file": "{{ file_Base64 "/path/to/document.pdf" }}", "filename": "document.pdf"}'
@@ -239,24 +266,24 @@ These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit)
### Address ### Address
| Function | Description | Example Output | | Function | Description | Example Output |
| --------------------------------------------------- | ---------------------------- | --------------------------------------------------- | | --------------------------------------------------- | ---------------------------- | ---------------------------------------------------- |
| `fakeit_City` | City name | `"Marcelside"` | | `fakeit_City` | City name | `"Marcelside"` |
| `fakeit_Country` | Country name | `"United States of America"` | | `fakeit_Country` | Country name | `"United States of America"` |
| `fakeit_CountryAbr` | Country abbreviation | `"US"` | | `fakeit_CountryAbr` | Country abbreviation | `"US"` |
| `fakeit_State` | State name | `"Illinois"` | | `fakeit_State` | State name | `"Illinois"` |
| `fakeit_StateAbr` | State abbreviation | `"IL"` | | `fakeit_StateAbr` | State abbreviation | `"IL"` |
| `fakeit_Street` | Full street | `"364 East Rapidsborough"` | | `fakeit_Street` | Full street | `"364 East Rapidsborough"` |
| `fakeit_StreetName` | Street name | `"View"` | | `fakeit_StreetName` | Street name | `"View"` |
| `fakeit_StreetNumber` | Street number | `"13645"` | | `fakeit_StreetNumber` | Street number | `"13645"` |
| `fakeit_StreetPrefix` | Street prefix | `"East"` | | `fakeit_StreetPrefix` | Street prefix | `"East"` |
| `fakeit_StreetSuffix` | Street suffix | `"Ave"` | | `fakeit_StreetSuffix` | Street suffix | `"Ave"` |
| `fakeit_Unit` | Unit | `"Apt 123"` | | `fakeit_Unit` | Unit | `"Apt 123"` |
| `fakeit_Zip` | ZIP code | `"13645"` | | `fakeit_Zip` | ZIP code | `"13645"` |
| `fakeit_Latitude` | Random latitude | `-73.534056` | | `fakeit_Latitude` | Random latitude | `-73.534056` |
| `fakeit_Longitude` | Random longitude | `-147.068112` | | `fakeit_Longitude` | Random longitude | `-147.068112` |
| `fakeit_LatitudeInRange(min float64, max float64)` | Latitude in specified range | `{{ fakeit_LatitudeInRange 0 90 }}``22.921026` | | `fakeit_LatitudeInRange(min float64, max float64)` | Latitude in specified range | `{{ fakeit_LatitudeInRange 0 90 }}``22.921026` |
| `fakeit_LongitudeInRange(min float64, max float64)` | Longitude in specified range | `{{ fakeit_LongitudeInRange 0 180 }}``-8.170450` | | `fakeit_LongitudeInRange(min float64, max float64)` | Longitude in specified range | `{{ fakeit_LongitudeInRange 0 180 }}``122.471830` |
### Game ### Game
@@ -343,16 +370,16 @@ These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit)
### Text ### Text
| Function | Description | Example | | Function | Description | Example |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------- | | ---------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------- |
| `fakeit_Sentence` | Random sentence | `{{ fakeit_Sentence }}` | | `fakeit_Sentence(wordCount ...int)` | Random sentence (optional word count) | `{{ fakeit_Sentence }}` or `{{ fakeit_Sentence 10 }}` |
| `fakeit_Paragraph` | Random paragraph | `{{ fakeit_Paragraph }}` | | `fakeit_Paragraph` | Random paragraph | `{{ fakeit_Paragraph }}` |
| `fakeit_LoremIpsumWord` | Lorem ipsum word | `"lorem"` | | `fakeit_LoremIpsumWord` | Lorem ipsum word | `"lorem"` |
| `fakeit_LoremIpsumSentence(wordCount int)` | Lorem ipsum sentence with specified word count | `{{ fakeit_LoremIpsumSentence 5 }}` | | `fakeit_LoremIpsumSentence(wordCount int)` | Lorem ipsum sentence with specified word count | `{{ fakeit_LoremIpsumSentence 5 }}` |
| `fakeit_LoremIpsumParagraph(paragraphs int, sentences int, words int, separator string)` | Lorem ipsum paragraphs with specified structure | `{{ fakeit_LoremIpsumParagraph 1 3 5 "\n" }}` | | `fakeit_LoremIpsumParagraph(paragraphs int, sentences int, words int, separator string)` | Lorem ipsum paragraphs with specified structure | `{{ fakeit_LoremIpsumParagraph 1 3 5 "\n" }}` |
| `fakeit_Question` | Random question | `"What is your name?"` | | `fakeit_Question` | Random question | `"What is your name?"` |
| `fakeit_Quote` | Random quote | `"Life is what happens..."` | | `fakeit_Quote` | Random quote | `"Life is what happens..."` |
| `fakeit_Phrase` | Random phrase | `"a piece of cake"` | | `fakeit_Phrase` | Random phrase | `"a piece of cake"` |
### Foods ### Foods

14
go.mod
View File

@@ -1,10 +1,10 @@
module go.aykhans.me/sarin module go.aykhans.me/sarin
go 1.25.7 go 1.26.0
require ( require (
github.com/brianvoe/gofakeit/v7 v7.14.0 github.com/brianvoe/gofakeit/v7 v7.14.0
github.com/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
@@ -15,7 +15,7 @@ require (
github.com/yuin/gopher-lua v1.1.1 github.com/yuin/gopher-lua v1.1.1
go.aykhans.me/utils v1.0.7 go.aykhans.me/utils v1.0.7
go.yaml.in/yaml/v4 v4.0.0-rc.3 go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/net v0.49.0 golang.org/x/net v0.51.0
) )
require ( require (
@@ -25,7 +25,7 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.5 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260109001716-2fbdffcb221f // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20260109001716-2fbdffcb221f // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect
@@ -53,7 +53,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.16 // indirect github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.39.0 // indirect golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
) )

24
go.sum
View File

@@ -16,8 +16,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk= github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk=
github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
@@ -28,8 +28,8 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
@@ -111,16 +111,16 @@ go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -10,7 +10,6 @@ import (
"go.aykhans.me/sarin/internal/types" "go.aykhans.me/sarin/internal/types"
versionpkg "go.aykhans.me/sarin/internal/version" versionpkg "go.aykhans.me/sarin/internal/version"
"go.aykhans.me/utils/common"
) )
const cliUsageText = `Usage: const cliUsageText = `Usage:
@@ -43,8 +42,8 @@ Flags:
-V, -values []string List of values for templating (e.g. "key1=value1") -V, -values []string List of values for templating (e.g. "key1=value1")
-T, -timeout time Timeout for the request (e.g. 400ms, 3s, 1m10s) (default %v) -T, -timeout time Timeout for the request (e.g. 400ms, 3s, 1m10s) (default %v)
-I, -insecure bool Skip SSL/TLS certificate verification (default %v) -I, -insecure bool Skip SSL/TLS certificate verification (default %v)
-lua []string Lua script for request transformation (inline or @file/@url) -lua []string Lua script for request transformation (inline or @file/@url)
-js []string JavaScript script for request transformation (inline or @file/@url)` -js []string JavaScript script for request transformation (inline or @file/@url)`
var _ IParser = ConfigCLIParser{} var _ IParser = ConfigCLIParser{}
@@ -72,7 +71,6 @@ func (arg *stringSliceArg) Set(value string) error {
// Parse parses command-line arguments into a Config object. // Parse parses command-line arguments into a Config object.
// It can return the following errors: // It can return the following errors:
// - types.ErrCLINoArgs
// - types.CLIUnexpectedArgsError // - types.CLIUnexpectedArgsError
// - types.FieldParseErrors // - types.FieldParseErrors
func (parser ConfigCLIParser) Parse() (*Config, error) { func (parser ConfigCLIParser) Parse() (*Config, error) {
@@ -179,12 +177,6 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
panic(err) panic(err)
} }
// Check if no flags were set and no non-flag arguments were provided.
// This covers cases where `sarin` is run without any meaningful arguments.
if flagSet.NFlag() == 0 && len(flagSet.Args()) == 0 {
return nil, types.ErrCLINoArgs
}
// Check for any unexpected non-flag arguments remaining after parsing. // Check for any unexpected non-flag arguments remaining after parsing.
if args := flagSet.Args(); len(args) > 0 { if args := flagSet.Args(); len(args) > 0 {
return nil, types.NewCLIUnexpectedArgsError(args) return nil, types.NewCLIUnexpectedArgsError(args)
@@ -202,23 +194,23 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
switch flagVar.Name { switch flagVar.Name {
// General config // General config
case "show-config", "s": case "show-config", "s":
config.ShowConfig = common.ToPtr(showConfig) config.ShowConfig = new(showConfig)
case "config-file", "f": case "config-file", "f":
for _, configFile := range configFiles { for _, configFile := range configFiles {
config.Files = append(config.Files, *types.ParseConfigFile(configFile)) config.Files = append(config.Files, *types.ParseConfigFile(configFile))
} }
case "concurrency", "c": case "concurrency", "c":
config.Concurrency = common.ToPtr(concurrency) config.Concurrency = new(concurrency)
case "requests", "r": case "requests", "r":
config.Requests = common.ToPtr(requestCount) config.Requests = new(requestCount)
case "duration", "d": case "duration", "d":
config.Duration = common.ToPtr(duration) config.Duration = new(duration)
case "quiet", "q": case "quiet", "q":
config.Quiet = common.ToPtr(quiet) config.Quiet = new(quiet)
case "output", "o": case "output", "o":
config.Output = common.ToPtr(ConfigOutputType(output)) config.Output = new(ConfigOutputType(output))
case "dry-run", "z": case "dry-run", "z":
config.DryRun = common.ToPtr(dryRun) config.DryRun = new(dryRun)
// Request config // Request config
case "url", "U": case "url", "U":
@@ -251,9 +243,9 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
case "values", "V": case "values", "V":
config.Values = append(config.Values, values...) config.Values = append(config.Values, values...)
case "timeout", "T": case "timeout", "T":
config.Timeout = common.ToPtr(timeout) config.Timeout = new(timeout)
case "insecure", "I": case "insecure", "I":
config.Insecure = common.ToPtr(insecure) config.Insecure = new(insecure)
case "lua": case "lua":
config.Lua = append(config.Lua, luaScripts...) config.Lua = append(config.Lua, luaScripts...)
case "js": case "js":

View File

@@ -93,10 +93,6 @@ type Config struct {
Js []string `yaml:"js,omitempty"` Js []string `yaml:"js,omitempty"`
} }
func NewConfig() *Config {
return &Config{}
}
func (config Config) MarshalYAML() (any, error) { func (config Config) MarshalYAML() (any, error) {
const randomValueComment = "Cycles through all values, with a new random start each round" const randomValueComment = "Cycles through all values, with a new random start each round"
@@ -279,7 +275,7 @@ func (config Config) Print() bool {
func (config *Config) Merge(newConfig *Config) { func (config *Config) Merge(newConfig *Config) {
config.Files = append(config.Files, newConfig.Files...) config.Files = append(config.Files, newConfig.Files...)
if len(newConfig.Methods) > 0 { if len(newConfig.Methods) > 0 {
config.Methods = append(config.Methods, newConfig.Methods...) config.Methods = newConfig.Methods
} }
if newConfig.URL != nil { if newConfig.URL != nil {
config.URL = newConfig.URL config.URL = newConfig.URL
@@ -321,7 +317,7 @@ func (config *Config) Merge(newConfig *Config) {
config.Cookies = append(config.Cookies, newConfig.Cookies...) config.Cookies = append(config.Cookies, newConfig.Cookies...)
} }
if len(newConfig.Bodies) != 0 { if len(newConfig.Bodies) != 0 {
config.Bodies = append(config.Bodies, newConfig.Bodies...) config.Bodies = newConfig.Bodies
} }
if len(newConfig.Proxies) != 0 { if len(newConfig.Proxies) != 0 {
config.Proxies.Append(newConfig.Proxies...) config.Proxies.Append(newConfig.Proxies...)
@@ -360,26 +356,26 @@ func (config *Config) SetDefaults() {
config.Timeout = &Defaults.RequestTimeout config.Timeout = &Defaults.RequestTimeout
} }
if config.Concurrency == nil { if config.Concurrency == nil {
config.Concurrency = common.ToPtr(Defaults.Concurrency) config.Concurrency = new(Defaults.Concurrency)
} }
if config.ShowConfig == nil { if config.ShowConfig == nil {
config.ShowConfig = common.ToPtr(Defaults.ShowConfig) config.ShowConfig = new(Defaults.ShowConfig)
} }
if config.Quiet == nil { if config.Quiet == nil {
config.Quiet = common.ToPtr(Defaults.Quiet) config.Quiet = new(Defaults.Quiet)
} }
if config.Insecure == nil { if config.Insecure == nil {
config.Insecure = common.ToPtr(Defaults.Insecure) config.Insecure = new(Defaults.Insecure)
} }
if config.DryRun == nil { if config.DryRun == nil {
config.DryRun = common.ToPtr(Defaults.DryRun) config.DryRun = new(Defaults.DryRun)
} }
if !config.Headers.Has("User-Agent") { if !config.Headers.Has("User-Agent") {
config.Headers = append(config.Headers, types.Header{Key: "User-Agent", Value: []string{Defaults.UserAgent}}) config.Headers = append(config.Headers, types.Header{Key: "User-Agent", Value: []string{Defaults.UserAgent}})
} }
if config.Output == nil { if config.Output == nil {
config.Output = common.ToPtr(Defaults.Output) config.Output = new(Defaults.Output)
} }
} }
@@ -540,12 +536,6 @@ func ReadAllConfigs() *Config {
cliParser := NewConfigCLIParser(os.Args) cliParser := NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse() cliConf, err := cliParser.Parse()
_ = utilsErr.MustHandle(err, _ = utilsErr.MustHandle(err,
utilsErr.OnSentinel(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp()
fmt.Fprintln(os.Stderr, StyleYellow.Render("\nNo arguments provided."))
os.Exit(1)
return nil
}),
utilsErr.OnType(func(err types.CLIUnexpectedArgsError) error { utilsErr.OnType(func(err types.CLIUnexpectedArgsError) error {
cliParser.PrintHelp() cliParser.PrintHelp()
fmt.Fprintln(os.Stderr, fmt.Fprintln(os.Stderr,

View File

@@ -7,7 +7,6 @@ import (
"time" "time"
"go.aykhans.me/sarin/internal/types" "go.aykhans.me/sarin/internal/types"
"go.aykhans.me/utils/common"
utilsParse "go.aykhans.me/utils/parser" utilsParse "go.aykhans.me/utils/parser"
) )
@@ -67,7 +66,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
} }
if output := parser.getEnv("OUTPUT"); output != "" { if output := parser.getEnv("OUTPUT"); output != "" {
config.Output = common.ToPtr(ConfigOutputType(output)) config.Output = new(ConfigOutputType(output))
} }
if insecure := parser.getEnv("INSECURE"); insecure != "" { if insecure := parser.getEnv("INSECURE"); insecure != "" {

View File

@@ -12,7 +12,6 @@ import (
"time" "time"
"go.aykhans.me/sarin/internal/types" "go.aykhans.me/sarin/internal/types"
"go.aykhans.me/utils/common"
"go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4"
) )
@@ -241,7 +240,7 @@ func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
config.Quiet = parsedData.Quiet config.Quiet = parsedData.Quiet
if parsedData.Output != nil { if parsedData.Output != nil {
config.Output = common.ToPtr(ConfigOutputType(*parsedData.Output)) config.Output = new(ConfigOutputType(*parsedData.Output))
} }
config.Insecure = parsedData.Insecure config.Insecure = parsedData.Insecure

View File

@@ -3,6 +3,7 @@ package sarin
import ( import (
"context" "context"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -13,6 +14,7 @@ import (
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/term"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"go.aykhans.me/sarin/internal/script" "go.aykhans.me/sarin/internal/script"
"go.aykhans.me/sarin/internal/types" "go.aykhans.me/sarin/internal/types"
@@ -155,6 +157,10 @@ func (q sarin) Start(ctx context.Context) {
var messageChannel chan runtimeMessage var messageChannel chan runtimeMessage
var sendMessage messageSender var sendMessage messageSender
if !q.quiet && !term.IsTerminal(os.Stdout.Fd()) {
q.quiet = true
}
if q.quiet { if q.quiet {
sendMessage = func(level runtimeMessageLevel, text string) {} sendMessage = func(level runtimeMessageLevel, text string) {}
} else { } else {

View File

@@ -2,7 +2,11 @@ package sarin
import ( import (
"bytes" "bytes"
"crypto/hmac"
"crypto/md5" // #nosec G501 -- exposed intentionally as a template utility helper
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"math/rand/v2" "math/rand/v2"
"mime/multipart" "mime/multipart"
"strings" "strings"
@@ -62,10 +66,6 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
}, },
"strings_TrimPrefix": strings.TrimPrefix, "strings_TrimPrefix": strings.TrimPrefix,
"strings_TrimSuffix": strings.TrimSuffix, "strings_TrimSuffix": strings.TrimSuffix,
"strings_Join": func(sep string, values ...string) string {
return strings.Join(values, sep)
},
// Dict // Dict
"dict_Str": func(values ...string) map[string]string { "dict_Str": func(values ...string) map[string]string {
dict := make(map[string]string) dict := make(map[string]string)
@@ -83,8 +83,49 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
"slice_Str": func(values ...string) []string { return values }, "slice_Str": func(values ...string) []string { return values },
"slice_Int": func(values ...int) []int { return values }, "slice_Int": func(values ...int) []int { return values },
"slice_Uint": func(values ...uint) []uint { return values }, "slice_Uint": func(values ...uint) []uint { return values },
"slice_Join": strings.Join,
// Time
"time_NowUnix": func() int64 { return time.Now().Unix() },
"time_NowUnixMilli": func() int64 { return time.Now().UnixMilli() },
"time_NowRFC3339": func() string { return time.Now().Format(time.RFC3339) },
"time_Format": func(layout string, t time.Time) string {
return t.Format(layout)
},
// Crypto
"crypto_SHA256": func(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])
},
"crypto_MD5": func(s string) string {
sum := md5.Sum([]byte(s)) // #nosec G401 -- MD5 is intentionally provided as a non-security template helper
return hex.EncodeToString(sum[:])
},
"crypto_HMACSHA256": func(key string, msg string) string {
mac := hmac.New(sha256.New, []byte(key))
_, _ = mac.Write([]byte(msg))
return hex.EncodeToString(mac.Sum(nil))
},
"crypto_Base64URL": func(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
},
// File // File
// file_Read reads a file (local or remote URL) and returns its content as a string.
// Usage: {{ file_Read "/path/to/file.txt" }}
// {{ file_Read "https://example.com/data.txt" }}
"file_Read": func(source string) (string, error) {
if fileCache == nil {
return "", types.ErrFileCacheNotInitialized
}
cached, err := fileCache.GetOrLoad(source)
if err != nil {
return "", err
}
return string(cached.Content), nil
},
// file_Base64 reads a file (local or remote URL) and returns its Base64 encoded content. // file_Base64 reads a file (local or remote URL) and returns its Base64 encoded content.
// Usage: {{ file_Base64 "/path/to/file.pdf" }} // Usage: {{ file_Base64 "/path/to/file.pdf" }}
// {{ file_Base64 "https://example.com/image.png" }} // {{ file_Base64 "https://example.com/image.png" }}

View File

@@ -145,22 +145,6 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
return nil return nil
} }
// 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 _, src := range sources {
if err := ValidateScript(ctx, src, engineType); err != nil {
return err
}
}
return nil
}
// fetchURL downloads content from an HTTP/HTTPS URL. // fetchURL downloads content from an HTTP/HTTPS URL.
// It can return the following errors: // It can return the following errors:
// - types.HTTPFetchError // - types.HTTPFetchError

View File

@@ -15,7 +15,7 @@ func (cookies Cookies) GetValue(key string) *[]string {
return nil return nil
} }
func (cookies *Cookies) Append(cookie ...Cookie) { func (cookies *Cookies) Merge(cookie ...Cookie) {
for _, c := range cookie { for _, c := range cookie {
if item := cookies.GetValue(c.Key); item != nil { if item := cookies.GetValue(c.Key); item != nil {
*item = append(*item, c.Value...) *item = append(*item, c.Value...)
@@ -27,7 +27,7 @@ func (cookies *Cookies) Append(cookie ...Cookie) {
func (cookies *Cookies) Parse(rawValues ...string) { func (cookies *Cookies) Parse(rawValues ...string) {
for _, rawValue := range rawValues { for _, rawValue := range rawValues {
cookies.Append(*ParseCookie(rawValue)) *cookies = append(*cookies, *ParseCookie(rawValue))
} }
} }

View File

@@ -9,7 +9,7 @@ import (
// ======================================== General ======================================== // ======================================== General ========================================
var ( var (
ErrNoError = errors.New("no error (internal)") errNoError = errors.New("no error (internal)")
) )
type FieldParseError struct { type FieldParseError struct {
@@ -20,7 +20,7 @@ type FieldParseError struct {
func NewFieldParseError(field string, value string, err error) FieldParseError { func NewFieldParseError(field string, value string, err error) FieldParseError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return FieldParseError{field, value, err} return FieldParseError{field, value, err}
} }
@@ -68,7 +68,7 @@ type FieldValidationError struct {
func NewFieldValidationError(field string, value string, err error) FieldValidationError { func NewFieldValidationError(field string, value string, err error) FieldValidationError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return FieldValidationError{field, value, err} return FieldValidationError{field, value, err}
} }
@@ -114,7 +114,7 @@ type UnmarshalError struct {
func NewUnmarshalError(err error) UnmarshalError { func NewUnmarshalError(err error) UnmarshalError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return UnmarshalError{err} return UnmarshalError{err}
} }
@@ -136,7 +136,7 @@ type FileReadError struct {
func NewFileReadError(path string, err error) FileReadError { func NewFileReadError(path string, err error) FileReadError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return FileReadError{path, err} return FileReadError{path, err}
} }
@@ -156,7 +156,7 @@ type HTTPFetchError struct {
func NewHTTPFetchError(url string, err error) HTTPFetchError { func NewHTTPFetchError(url string, err error) HTTPFetchError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return HTTPFetchError{url, err} return HTTPFetchError{url, err}
} }
@@ -190,7 +190,7 @@ type URLParseError struct {
func NewURLParseError(url string, err error) URLParseError { func NewURLParseError(url string, err error) URLParseError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return URLParseError{url, err} return URLParseError{url, err}
} }
@@ -216,7 +216,7 @@ type TemplateParseError struct {
func NewTemplateParseError(err error) TemplateParseError { func NewTemplateParseError(err error) TemplateParseError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return TemplateParseError{err} return TemplateParseError{err}
} }
@@ -235,7 +235,7 @@ type TemplateRenderError struct {
func NewTemplateRenderError(err error) TemplateRenderError { func NewTemplateRenderError(err error) TemplateRenderError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return TemplateRenderError{err} return TemplateRenderError{err}
} }
@@ -248,26 +248,8 @@ func (e TemplateRenderError) Unwrap() error {
return e.Err 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
} }
@@ -288,7 +270,7 @@ type ConfigFileReadError struct {
func NewConfigFileReadError(err error) ConfigFileReadError { func NewConfigFileReadError(err error) ConfigFileReadError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ConfigFileReadError{err} return ConfigFileReadError{err}
} }
@@ -321,7 +303,7 @@ type ProxyParseError struct {
func NewProxyParseError(err error) ProxyParseError { func NewProxyParseError(err error) ProxyParseError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ProxyParseError{err} return ProxyParseError{err}
} }
@@ -365,7 +347,7 @@ type ProxyDialError struct {
func NewProxyDialError(proxy string, err error) ProxyDialError { func NewProxyDialError(proxy string, err error) ProxyDialError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ProxyDialError{proxy, err} return ProxyDialError{proxy, err}
} }
@@ -395,7 +377,7 @@ type ScriptLoadError struct {
func NewScriptLoadError(source string, err error) ScriptLoadError { func NewScriptLoadError(source string, err error) ScriptLoadError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ScriptLoadError{source, err} return ScriptLoadError{source, err}
} }
@@ -415,7 +397,7 @@ type ScriptExecutionError struct {
func NewScriptExecutionError(engineType string, err error) ScriptExecutionError { func NewScriptExecutionError(engineType string, err error) ScriptExecutionError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ScriptExecutionError{engineType, err} return ScriptExecutionError{engineType, err}
} }
@@ -436,7 +418,7 @@ type ScriptChainError struct {
func NewScriptChainError(engineType string, index int, err error) ScriptChainError { func NewScriptChainError(engineType string, index int, err error) ScriptChainError {
if err == nil { if err == nil {
err = ErrNoError err = errNoError
} }
return ScriptChainError{engineType, index, err} return ScriptChainError{engineType, index, err}
} }

View File

@@ -24,7 +24,7 @@ func (headers Headers) GetValue(key string) *[]string {
return nil return nil
} }
func (headers *Headers) Append(header ...Header) { func (headers *Headers) Merge(header ...Header) {
for _, h := range header { for _, h := range header {
if item := headers.GetValue(h.Key); item != nil { if item := headers.GetValue(h.Key); item != nil {
*item = append(*item, h.Value...) *item = append(*item, h.Value...)
@@ -36,7 +36,7 @@ func (headers *Headers) Append(header ...Header) {
func (headers *Headers) Parse(rawValues ...string) { func (headers *Headers) Parse(rawValues ...string) {
for _, rawValue := range rawValues { for _, rawValue := range rawValues {
headers.Append(*ParseHeader(rawValue)) *headers = append(*headers, *ParseHeader(rawValue))
} }
} }

View File

@@ -15,7 +15,7 @@ func (params Params) GetValue(key string) *[]string {
return nil return nil
} }
func (params *Params) Append(param ...Param) { func (params *Params) Merge(param ...Param) {
for _, p := range param { for _, p := range param {
if item := params.GetValue(p.Key); item != nil { if item := params.GetValue(p.Key); item != nil {
*item = append(*item, p.Value...) *item = append(*item, p.Value...)
@@ -27,7 +27,7 @@ func (params *Params) Append(param ...Param) {
func (params *Params) Parse(rawValues ...string) { func (params *Params) Parse(rawValues ...string) {
for _, rawValue := range rawValues { for _, rawValue := range rawValues {
params.Append(*ParseParam(rawValue)) *params = append(*params, *ParseParam(rawValue))
} }
} }