mirror of
https://github.com/aykhans/sarin.git
synced 2026-02-28 23:09:13 +00:00
Compare commits
18 Commits
6a713ef241
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c299fda79d | |||
| 1f06b43b06 | |||
| e031c8e7a5 | |||
|
|
de24f9d4a4 | ||
| d197e90103 | |||
| ae054bb3d6 | |||
| 61af28a3d3 | |||
| 665be5d98a | |||
| d346067e8a | |||
| a3e20cd3d3 | |||
| 6d921cf8e3 | |||
|
|
d8b0a1e6a3 | ||
| b21d97192c | |||
| f0606a0f82 | |||
| 3be8ff218c | |||
| 7cb49195f8 | |||
|
|
a154215495 | ||
| c1584eb47b |
8
.github/workflows/lint.yaml
vendored
8
.github/workflows/lint.yaml
vendored
@@ -16,8 +16,12 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.7
|
||||
go-version: 1.26.0
|
||||
- name: go fix
|
||||
run: |
|
||||
go fix ./...
|
||||
git diff --exit-code
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.8.0
|
||||
version: v2.9.0
|
||||
|
||||
14
.github/workflows/release.yaml
vendored
14
.github/workflows/release.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
echo "VERSION=$(git describe --tags --always)" >> $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
|
||||
if: github.event_name == 'release' || inputs.build_binaries
|
||||
@@ -53,12 +53,12 @@ jobs:
|
||||
-X 'go.aykhans.me/sarin/internal/version.GoVersion=$(go version)' \
|
||||
-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 GOEXPERIMENT=greenteagc 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 GOEXPERIMENT=greenteagc 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 GOEXPERIMENT=greenteagc GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-arm64.exe ./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 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-linux-arm64 ./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 GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-darwin-arm64 ./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 GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o ./sarin-windows-arm64.exe ./cmd/cli/main.go
|
||||
|
||||
- name: Upload Release Assets
|
||||
if: github.event_name == 'release' || inputs.build_binaries
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
go: "1.25"
|
||||
go: "1.26"
|
||||
concurrency: 12
|
||||
|
||||
linters:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN --mount=type=bind,source=./go.mod,target=./go.mod \
|
||||
go mod download
|
||||
|
||||
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}' \
|
||||
-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)' \
|
||||
|
||||
19
README.md
19
README.md
@@ -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.
|
||||
|
||||
| ✅ Supported | ❌ Not Supported |
|
||||
| ---------------------------------------------------------- | --------------------------------- |
|
||||
| High-performance with low memory footprint | Detailed response body analysis |
|
||||
| Long-running duration/count based tests | Extensive response statistics |
|
||||
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | Scripting or multi-step scenarios |
|
||||
| Flexible config (CLI, ENV, YAML) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||
| ✅ Supported | ❌ Not Supported |
|
||||
| ---------------------------------------------------------- | ------------------------------- |
|
||||
| High-performance with low memory footprint | Detailed response body analysis |
|
||||
| Long-running duration/count based tests | Extensive response statistics |
|
||||
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||
| Request scripting with Lua and JavaScript | Distributed load testing |
|
||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||
| Flexible config (CLI, ENV, YAML) | Plugins / extensions ecosystem |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -56,12 +57,12 @@ Download the latest binaries from the [releases](https://github.com/aykhans/sari
|
||||
|
||||
### Building from Source
|
||||
|
||||
Requires [Go 1.25+](https://golang.org/dl/).
|
||||
Requires [Go 1.26+](https://golang.org/dl/).
|
||||
|
||||
```sh
|
||||
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' \
|
||||
-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)' \
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3"
|
||||
|
||||
vars:
|
||||
BIN_DIR: ./bin
|
||||
GOLANGCI_LINT_VERSION: v2.8.0
|
||||
GOLANGCI_LINT_VERSION: v2.9.0
|
||||
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
||||
|
||||
tasks:
|
||||
@@ -11,16 +11,22 @@ tasks:
|
||||
desc: Run fmt, tidy, and lint.
|
||||
cmds:
|
||||
- task: fmt
|
||||
- task: fix
|
||||
- task: tidy
|
||||
- task: lint
|
||||
|
||||
fmt:
|
||||
desc: Run linters
|
||||
desc: Run format
|
||||
deps:
|
||||
- install-golangci-lint
|
||||
cmds:
|
||||
- "{{.GOLANGCI}} fmt"
|
||||
|
||||
fix:
|
||||
desc: Run go fix
|
||||
cmds:
|
||||
- go fix ./...
|
||||
|
||||
tidy:
|
||||
desc: Run go mod tidy.
|
||||
cmds:
|
||||
@@ -52,7 +58,7 @@ tasks:
|
||||
cmds:
|
||||
- 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)'
|
||||
-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)'
|
||||
|
||||
@@ -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 |
|
||||
| [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) |
|
||||
| [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.
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
params:
|
||||
key1: value1
|
||||
key2: [value2, value3]
|
||||
key2: [value2, value3] # cycles between value2 and value3
|
||||
|
||||
# OR
|
||||
|
||||
params:
|
||||
- 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:**
|
||||
|
||||
```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:**
|
||||
@@ -255,26 +270,33 @@ SARIN_PARAM="key1=value1"
|
||||
|
||||
## 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
|
||||
headers:
|
||||
key1: value1
|
||||
key2: [value2, value3]
|
||||
key2: [value2, value3] # cycles between value2 and value3
|
||||
|
||||
# OR
|
||||
|
||||
headers:
|
||||
- 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:**
|
||||
|
||||
```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:**
|
||||
@@ -285,26 +307,33 @@ SARIN_HEADER="key1: value1"
|
||||
|
||||
## 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
|
||||
cookies:
|
||||
key1: value1
|
||||
key2: [value2, value3]
|
||||
key2: [value2, value3] # cycles between value2 and value3
|
||||
|
||||
# OR
|
||||
|
||||
cookies:
|
||||
- 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:**
|
||||
|
||||
```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:**
|
||||
@@ -374,3 +403,133 @@ values: |
|
||||
```sh
|
||||
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; }'
|
||||
```
|
||||
|
||||
156
docs/examples.md
156
docs/examples.md
@@ -15,6 +15,7 @@ This guide provides practical examples for common Sarin use cases.
|
||||
- [Docker Usage](#docker-usage)
|
||||
- [Dry Run Mode](#dry-run-mode)
|
||||
- [Show Configuration](#show-configuration)
|
||||
- [Scripting](#scripting)
|
||||
|
||||
---
|
||||
|
||||
@@ -133,20 +134,34 @@ headers:
|
||||
|
||||
</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
|
||||
sarin -U http://example.com -r 1000 -c 10 \
|
||||
-H "X-Region: us-east" \
|
||||
-H "X-Region: us-west" \
|
||||
-H "X-Region: eu-central"
|
||||
-H "X-Region: us-west"
|
||||
```
|
||||
|
||||
<details>
|
||||
<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
|
||||
url: http://example.com
|
||||
requests: 1000
|
||||
@@ -158,8 +173,6 @@ headers:
|
||||
- eu-central
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
```sh
|
||||
@@ -186,7 +199,7 @@ params:
|
||||
|
||||
```sh
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -198,7 +211,7 @@ url: http://example.com/users
|
||||
requests: 1000
|
||||
concurrency: 10
|
||||
params:
|
||||
id: "{{ fakeit_IntRange 1 1000 }}"
|
||||
id: "{{ fakeit_Number 1 1000 }}"
|
||||
fields: "name,email"
|
||||
```
|
||||
|
||||
@@ -823,19 +836,19 @@ quiet: true
|
||||
**Basic Docker usage:**
|
||||
|
||||
```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:**
|
||||
|
||||
```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:**
|
||||
|
||||
```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:**
|
||||
@@ -894,3 +907,124 @@ headers:
|
||||
```
|
||||
|
||||
</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>
|
||||
|
||||
@@ -10,6 +10,8 @@ Sarin supports Go templates in URL paths, methods, bodies, headers, params, cook
|
||||
- [General Functions](#general-functions)
|
||||
- [String Functions](#string-functions)
|
||||
- [Collection Functions](#collection-functions)
|
||||
- [Time Functions](#time-functions)
|
||||
- [Crypto Functions](#crypto-functions)
|
||||
- [Body Functions](#body-functions)
|
||||
- [File Functions](#file-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_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_Join(sep string, values ...string)` | Join strings with separator | `{{ strings_Join "-" "a" "b" "c" }}` → `a-b-c` |
|
||||
|
||||
### Collection Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
| ----------------------------- | --------------------------------------------- | -------------------------------------------- |
|
||||
| `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_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` |
|
||||
| `slice_Uint(values ...uint)` | Create uint slice | `{{ slice_Uint 1 2 3 }}` |
|
||||
| Function | Description | Example |
|
||||
| ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------- |
|
||||
| `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_Join(slice []string, sep string)` | Join string slice with separator | `{{ slice_Join (slice_Str "a" "b" "c") "-" }}` → `a-b-c` |
|
||||
| `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
|
||||
|
||||
@@ -153,11 +173,18 @@ body: '{{ body_FormData "twitter" "@@username" }}'
|
||||
|
||||
| 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` Details:**
|
||||
**`file_Read` and `file_Base64` Details:**
|
||||
|
||||
```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
|
||||
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
|
||||
|
||||
| Function | Description | Example Output |
|
||||
| --------------------------------------------------- | ---------------------------- | --------------------------------------------------- |
|
||||
| `fakeit_City` | City name | `"Marcelside"` |
|
||||
| `fakeit_Country` | Country name | `"United States of America"` |
|
||||
| `fakeit_CountryAbr` | Country abbreviation | `"US"` |
|
||||
| `fakeit_State` | State name | `"Illinois"` |
|
||||
| `fakeit_StateAbr` | State abbreviation | `"IL"` |
|
||||
| `fakeit_Street` | Full street | `"364 East Rapidsborough"` |
|
||||
| `fakeit_StreetName` | Street name | `"View"` |
|
||||
| `fakeit_StreetNumber` | Street number | `"13645"` |
|
||||
| `fakeit_StreetPrefix` | Street prefix | `"East"` |
|
||||
| `fakeit_StreetSuffix` | Street suffix | `"Ave"` |
|
||||
| `fakeit_Unit` | Unit | `"Apt 123"` |
|
||||
| `fakeit_Zip` | ZIP code | `"13645"` |
|
||||
| `fakeit_Latitude` | Random latitude | `-73.534056` |
|
||||
| `fakeit_Longitude` | Random longitude | `-147.068112` |
|
||||
| `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` |
|
||||
| Function | Description | Example Output |
|
||||
| --------------------------------------------------- | ---------------------------- | ---------------------------------------------------- |
|
||||
| `fakeit_City` | City name | `"Marcelside"` |
|
||||
| `fakeit_Country` | Country name | `"United States of America"` |
|
||||
| `fakeit_CountryAbr` | Country abbreviation | `"US"` |
|
||||
| `fakeit_State` | State name | `"Illinois"` |
|
||||
| `fakeit_StateAbr` | State abbreviation | `"IL"` |
|
||||
| `fakeit_Street` | Full street | `"364 East Rapidsborough"` |
|
||||
| `fakeit_StreetName` | Street name | `"View"` |
|
||||
| `fakeit_StreetNumber` | Street number | `"13645"` |
|
||||
| `fakeit_StreetPrefix` | Street prefix | `"East"` |
|
||||
| `fakeit_StreetSuffix` | Street suffix | `"Ave"` |
|
||||
| `fakeit_Unit` | Unit | `"Apt 123"` |
|
||||
| `fakeit_Zip` | ZIP code | `"13645"` |
|
||||
| `fakeit_Latitude` | Random latitude | `-73.534056` |
|
||||
| `fakeit_Longitude` | Random longitude | `-147.068112` |
|
||||
| `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 }}` → `122.471830` |
|
||||
|
||||
### Game
|
||||
|
||||
@@ -343,16 +370,16 @@ These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit)
|
||||
|
||||
### Text
|
||||
|
||||
| Function | Description | Example |
|
||||
| ---------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------- |
|
||||
| `fakeit_Sentence` | Random sentence | `{{ fakeit_Sentence }}` |
|
||||
| `fakeit_Paragraph` | Random paragraph | `{{ fakeit_Paragraph }}` |
|
||||
| `fakeit_LoremIpsumWord` | Lorem ipsum word | `"lorem"` |
|
||||
| `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_Question` | Random question | `"What is your name?"` |
|
||||
| `fakeit_Quote` | Random quote | `"Life is what happens..."` |
|
||||
| `fakeit_Phrase` | Random phrase | `"a piece of cake"` |
|
||||
| Function | Description | Example |
|
||||
| ---------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------- |
|
||||
| `fakeit_Sentence(wordCount ...int)` | Random sentence (optional word count) | `{{ fakeit_Sentence }}` or `{{ fakeit_Sentence 10 }}` |
|
||||
| `fakeit_Paragraph` | Random paragraph | `{{ fakeit_Paragraph }}` |
|
||||
| `fakeit_LoremIpsumWord` | Lorem ipsum word | `"lorem"` |
|
||||
| `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_Question` | Random question | `"What is your name?"` |
|
||||
| `fakeit_Quote` | Random quote | `"Life is what happens..."` |
|
||||
| `fakeit_Phrase` | Random phrase | `"a piece of cake"` |
|
||||
|
||||
### Foods
|
||||
|
||||
|
||||
14
go.mod
14
go.mod
@@ -1,10 +1,10 @@
|
||||
module go.aykhans.me/sarin
|
||||
|
||||
go 1.25.7
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
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/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
go.aykhans.me/utils v1.0.7
|
||||
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 (
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // 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/exp/slice v0.0.0-20260109001716-2fbdffcb221f // 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/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
|
||||
24
go.sum
24
go.sum
@@ -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/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk=
|
||||
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 v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
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/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
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/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/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=
|
||||
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
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/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
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=
|
||||
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/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
versionpkg "go.aykhans.me/sarin/internal/version"
|
||||
"go.aykhans.me/utils/common"
|
||||
)
|
||||
|
||||
const cliUsageText = `Usage:
|
||||
@@ -43,8 +42,8 @@ Flags:
|
||||
-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)
|
||||
-I, -insecure bool Skip SSL/TLS certificate verification (default %v)
|
||||
-lua []string Lua script for request transformation (inline or @file/@url)
|
||||
-js []string JavaScript 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)`
|
||||
|
||||
var _ IParser = ConfigCLIParser{}
|
||||
|
||||
@@ -72,7 +71,6 @@ func (arg *stringSliceArg) Set(value string) error {
|
||||
|
||||
// Parse parses command-line arguments into a Config object.
|
||||
// It can return the following errors:
|
||||
// - types.ErrCLINoArgs
|
||||
// - types.CLIUnexpectedArgsError
|
||||
// - types.FieldParseErrors
|
||||
func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
@@ -179,12 +177,6 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
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.
|
||||
if args := flagSet.Args(); len(args) > 0 {
|
||||
return nil, types.NewCLIUnexpectedArgsError(args)
|
||||
@@ -202,23 +194,23 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
switch flagVar.Name {
|
||||
// General config
|
||||
case "show-config", "s":
|
||||
config.ShowConfig = common.ToPtr(showConfig)
|
||||
config.ShowConfig = new(showConfig)
|
||||
case "config-file", "f":
|
||||
for _, configFile := range configFiles {
|
||||
config.Files = append(config.Files, *types.ParseConfigFile(configFile))
|
||||
}
|
||||
case "concurrency", "c":
|
||||
config.Concurrency = common.ToPtr(concurrency)
|
||||
config.Concurrency = new(concurrency)
|
||||
case "requests", "r":
|
||||
config.Requests = common.ToPtr(requestCount)
|
||||
config.Requests = new(requestCount)
|
||||
case "duration", "d":
|
||||
config.Duration = common.ToPtr(duration)
|
||||
config.Duration = new(duration)
|
||||
case "quiet", "q":
|
||||
config.Quiet = common.ToPtr(quiet)
|
||||
config.Quiet = new(quiet)
|
||||
case "output", "o":
|
||||
config.Output = common.ToPtr(ConfigOutputType(output))
|
||||
config.Output = new(ConfigOutputType(output))
|
||||
case "dry-run", "z":
|
||||
config.DryRun = common.ToPtr(dryRun)
|
||||
config.DryRun = new(dryRun)
|
||||
|
||||
// Request config
|
||||
case "url", "U":
|
||||
@@ -251,9 +243,9 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
case "values", "V":
|
||||
config.Values = append(config.Values, values...)
|
||||
case "timeout", "T":
|
||||
config.Timeout = common.ToPtr(timeout)
|
||||
config.Timeout = new(timeout)
|
||||
case "insecure", "I":
|
||||
config.Insecure = common.ToPtr(insecure)
|
||||
config.Insecure = new(insecure)
|
||||
case "lua":
|
||||
config.Lua = append(config.Lua, luaScripts...)
|
||||
case "js":
|
||||
|
||||
@@ -93,10 +93,6 @@ type Config struct {
|
||||
Js []string `yaml:"js,omitempty"`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
func (config Config) MarshalYAML() (any, error) {
|
||||
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) {
|
||||
config.Files = append(config.Files, newConfig.Files...)
|
||||
if len(newConfig.Methods) > 0 {
|
||||
config.Methods = append(config.Methods, newConfig.Methods...)
|
||||
config.Methods = newConfig.Methods
|
||||
}
|
||||
if newConfig.URL != nil {
|
||||
config.URL = newConfig.URL
|
||||
@@ -321,7 +317,7 @@ func (config *Config) Merge(newConfig *Config) {
|
||||
config.Cookies = append(config.Cookies, newConfig.Cookies...)
|
||||
}
|
||||
if len(newConfig.Bodies) != 0 {
|
||||
config.Bodies = append(config.Bodies, newConfig.Bodies...)
|
||||
config.Bodies = newConfig.Bodies
|
||||
}
|
||||
if len(newConfig.Proxies) != 0 {
|
||||
config.Proxies.Append(newConfig.Proxies...)
|
||||
@@ -360,26 +356,26 @@ func (config *Config) SetDefaults() {
|
||||
config.Timeout = &Defaults.RequestTimeout
|
||||
}
|
||||
if config.Concurrency == nil {
|
||||
config.Concurrency = common.ToPtr(Defaults.Concurrency)
|
||||
config.Concurrency = new(Defaults.Concurrency)
|
||||
}
|
||||
if config.ShowConfig == nil {
|
||||
config.ShowConfig = common.ToPtr(Defaults.ShowConfig)
|
||||
config.ShowConfig = new(Defaults.ShowConfig)
|
||||
}
|
||||
if config.Quiet == nil {
|
||||
config.Quiet = common.ToPtr(Defaults.Quiet)
|
||||
config.Quiet = new(Defaults.Quiet)
|
||||
}
|
||||
if config.Insecure == nil {
|
||||
config.Insecure = common.ToPtr(Defaults.Insecure)
|
||||
config.Insecure = new(Defaults.Insecure)
|
||||
}
|
||||
if config.DryRun == nil {
|
||||
config.DryRun = common.ToPtr(Defaults.DryRun)
|
||||
config.DryRun = new(Defaults.DryRun)
|
||||
}
|
||||
if !config.Headers.Has("User-Agent") {
|
||||
config.Headers = append(config.Headers, types.Header{Key: "User-Agent", Value: []string{Defaults.UserAgent}})
|
||||
}
|
||||
|
||||
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)
|
||||
cliConf, err := cliParser.Parse()
|
||||
_ = 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 {
|
||||
cliParser.PrintHelp()
|
||||
fmt.Fprintln(os.Stderr,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
"go.aykhans.me/utils/common"
|
||||
utilsParse "go.aykhans.me/utils/parser"
|
||||
)
|
||||
|
||||
@@ -67,7 +66,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
||||
}
|
||||
|
||||
if output := parser.getEnv("OUTPUT"); output != "" {
|
||||
config.Output = common.ToPtr(ConfigOutputType(output))
|
||||
config.Output = new(ConfigOutputType(output))
|
||||
}
|
||||
|
||||
if insecure := parser.getEnv("INSECURE"); insecure != "" {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
"go.aykhans.me/utils/common"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -241,7 +240,7 @@ func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
|
||||
config.Quiet = parsedData.Quiet
|
||||
|
||||
if parsedData.Output != nil {
|
||||
config.Output = common.ToPtr(ConfigOutputType(*parsedData.Output))
|
||||
config.Output = new(ConfigOutputType(*parsedData.Output))
|
||||
}
|
||||
|
||||
config.Insecure = parsedData.Insecure
|
||||
|
||||
@@ -3,6 +3,7 @@ package sarin
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.aykhans.me/sarin/internal/script"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
@@ -155,6 +157,10 @@ func (q sarin) Start(ctx context.Context) {
|
||||
var messageChannel chan runtimeMessage
|
||||
var sendMessage messageSender
|
||||
|
||||
if !q.quiet && !term.IsTerminal(os.Stdout.Fd()) {
|
||||
q.quiet = true
|
||||
}
|
||||
|
||||
if q.quiet {
|
||||
sendMessage = func(level runtimeMessageLevel, text string) {}
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,11 @@ package sarin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5" // #nosec G501 -- exposed intentionally as a template utility helper
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/rand/v2"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
@@ -62,10 +66,6 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
||||
},
|
||||
"strings_TrimPrefix": strings.TrimPrefix,
|
||||
"strings_TrimSuffix": strings.TrimSuffix,
|
||||
"strings_Join": func(sep string, values ...string) string {
|
||||
return strings.Join(values, sep)
|
||||
},
|
||||
|
||||
// Dict
|
||||
"dict_Str": func(values ...string) 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_Int": func(values ...int) []int { 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_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.
|
||||
// Usage: {{ file_Base64 "/path/to/file.pdf" }}
|
||||
// {{ file_Base64 "https://example.com/image.png" }}
|
||||
|
||||
@@ -145,22 +145,6 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
|
||||
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.
|
||||
// It can return the following errors:
|
||||
// - types.HTTPFetchError
|
||||
|
||||
@@ -15,7 +15,7 @@ func (cookies Cookies) GetValue(key string) *[]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cookies *Cookies) Append(cookie ...Cookie) {
|
||||
func (cookies *Cookies) Merge(cookie ...Cookie) {
|
||||
for _, c := range cookie {
|
||||
if item := cookies.GetValue(c.Key); item != nil {
|
||||
*item = append(*item, c.Value...)
|
||||
@@ -27,7 +27,7 @@ func (cookies *Cookies) Append(cookie ...Cookie) {
|
||||
|
||||
func (cookies *Cookies) Parse(rawValues ...string) {
|
||||
for _, rawValue := range rawValues {
|
||||
cookies.Append(*ParseCookie(rawValue))
|
||||
*cookies = append(*cookies, *ParseCookie(rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// ======================================== General ========================================
|
||||
|
||||
var (
|
||||
ErrNoError = errors.New("no error (internal)")
|
||||
errNoError = errors.New("no error (internal)")
|
||||
)
|
||||
|
||||
type FieldParseError struct {
|
||||
@@ -20,7 +20,7 @@ type FieldParseError struct {
|
||||
|
||||
func NewFieldParseError(field string, value string, err error) FieldParseError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return FieldParseError{field, value, err}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ type FieldValidationError struct {
|
||||
|
||||
func NewFieldValidationError(field string, value string, err error) FieldValidationError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return FieldValidationError{field, value, err}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ type UnmarshalError struct {
|
||||
|
||||
func NewUnmarshalError(err error) UnmarshalError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return UnmarshalError{err}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ type FileReadError struct {
|
||||
|
||||
func NewFileReadError(path string, err error) FileReadError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return FileReadError{path, err}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ type HTTPFetchError struct {
|
||||
|
||||
func NewHTTPFetchError(url string, err error) HTTPFetchError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return HTTPFetchError{url, err}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ type URLParseError struct {
|
||||
|
||||
func NewURLParseError(url string, err error) URLParseError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return URLParseError{url, err}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ type TemplateParseError struct {
|
||||
|
||||
func NewTemplateParseError(err error) TemplateParseError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return TemplateParseError{err}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ type TemplateRenderError struct {
|
||||
|
||||
func NewTemplateRenderError(err error) TemplateRenderError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return TemplateRenderError{err}
|
||||
}
|
||||
@@ -248,26 +248,8 @@ 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 ========================================
|
||||
|
||||
var (
|
||||
ErrCLINoArgs = errors.New("CLI expects arguments but received none")
|
||||
)
|
||||
|
||||
type CLIUnexpectedArgsError struct {
|
||||
Args []string
|
||||
}
|
||||
@@ -288,7 +270,7 @@ type ConfigFileReadError struct {
|
||||
|
||||
func NewConfigFileReadError(err error) ConfigFileReadError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ConfigFileReadError{err}
|
||||
}
|
||||
@@ -321,7 +303,7 @@ type ProxyParseError struct {
|
||||
|
||||
func NewProxyParseError(err error) ProxyParseError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ProxyParseError{err}
|
||||
}
|
||||
@@ -365,7 +347,7 @@ type ProxyDialError struct {
|
||||
|
||||
func NewProxyDialError(proxy string, err error) ProxyDialError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ProxyDialError{proxy, err}
|
||||
}
|
||||
@@ -395,7 +377,7 @@ type ScriptLoadError struct {
|
||||
|
||||
func NewScriptLoadError(source string, err error) ScriptLoadError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ScriptLoadError{source, err}
|
||||
}
|
||||
@@ -415,7 +397,7 @@ type ScriptExecutionError struct {
|
||||
|
||||
func NewScriptExecutionError(engineType string, err error) ScriptExecutionError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ScriptExecutionError{engineType, err}
|
||||
}
|
||||
@@ -436,7 +418,7 @@ type ScriptChainError struct {
|
||||
|
||||
func NewScriptChainError(engineType string, index int, err error) ScriptChainError {
|
||||
if err == nil {
|
||||
err = ErrNoError
|
||||
err = errNoError
|
||||
}
|
||||
return ScriptChainError{engineType, index, err}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (headers Headers) GetValue(key string) *[]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (headers *Headers) Append(header ...Header) {
|
||||
func (headers *Headers) Merge(header ...Header) {
|
||||
for _, h := range header {
|
||||
if item := headers.GetValue(h.Key); item != nil {
|
||||
*item = append(*item, h.Value...)
|
||||
@@ -36,7 +36,7 @@ func (headers *Headers) Append(header ...Header) {
|
||||
|
||||
func (headers *Headers) Parse(rawValues ...string) {
|
||||
for _, rawValue := range rawValues {
|
||||
headers.Append(*ParseHeader(rawValue))
|
||||
*headers = append(*headers, *ParseHeader(rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ func (params Params) GetValue(key string) *[]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (params *Params) Append(param ...Param) {
|
||||
func (params *Params) Merge(param ...Param) {
|
||||
for _, p := range param {
|
||||
if item := params.GetValue(p.Key); item != nil {
|
||||
*item = append(*item, p.Value...)
|
||||
@@ -27,7 +27,7 @@ func (params *Params) Append(param ...Param) {
|
||||
|
||||
func (params *Params) Parse(rawValues ...string) {
|
||||
for _, rawValue := range rawValues {
|
||||
params.Append(*ParseParam(rawValue))
|
||||
*params = append(*params, *ParseParam(rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user