diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 35861d01..dd90ac9d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
-- Version [e.g. 22] (`dasel --version`)
+- Version [e.g. 22] (`dasel version`)
**Additional context**
Add any other context about the problem here.
diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml
index a955362d..5f6de346 100644
--- a/.github/workflows/build-dev.yaml
+++ b/.github/workflows/build-dev.yaml
@@ -65,11 +65,11 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
- go-version: '^1.21.0' # The Go version to download (if necessary) and use.
+ go-version: '^1.23.0' # The Go version to download (if necessary) and use.
- name: Set env
run: echo RELEASE_VERSION=development >> $GITHUB_ENV
- name: Build
- run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
+ run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
- name: Test version
if: matrix.test_version == true
- run: ./target/release/${{ matrix.artifact_name }} --version
+ run: ./target/release/${{ matrix.artifact_name }} version
diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml
index 0accb8e0..124cc3fd 100644
--- a/.github/workflows/build-test.yaml
+++ b/.github/workflows/build-test.yaml
@@ -77,15 +77,15 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
- go-version: '^1.21.0' # The Go version to download (if necessary) and use.
+ go-version: '^1.23.0' # The Go version to download (if necessary) and use.
- name: Set env
run: echo RELEASE_VERSION=development >> $GITHUB_ENV
- name: Build
- run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
+ run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
- name: Test version
if: matrix.test_version == true
- run: ./target/release/${{ matrix.artifact_name }} --version
+ run: ./target/release/${{ matrix.artifact_name }} version
- name: Test execution
if: matrix.test_execution == true
run: |
- echo '{"hello": "World"}' | ./target/release/${{ matrix.artifact_name }} -r json 'hello'
+ echo '{"hello": "World"}' | ./target/release/${{ matrix.artifact_name }} -i json 'hello'
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 78832c17..b71531bc 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -73,14 +73,14 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
- go-version: '^1.21.0'
+ go-version: '^1.23.0'
- name: Set env
run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV
- name: Build
- run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
+ run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel
- name: Test version
if: matrix.test_version == true
- run: ./target/release/${{ matrix.artifact_name }} --version
+ run: ./target/release/${{ matrix.artifact_name }} version
- name: Gzip binaries
run: gzip -c ./target/release/${{ matrix.artifact_name }} > ./target/release/${{ matrix.artifact_name }}.gz
- name: Upload binaries to release
diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml
index 1d48e364..7c65d182 100644
--- a/.github/workflows/container.yaml
+++ b/.github/workflows/container.yaml
@@ -42,7 +42,7 @@ jobs:
tags: dasel:test
- name: Test
run: |
- echo '{"hello": "World"}' | docker run -i --rm dasel:test -r json 'hello'
+ echo '{"hello": "World"}' | docker run -i --rm dasel:test -i json 'hello'
- name: Set version tag variables
if: ${{ steps.version.outputs.is_valid == 'true' }}
run: |
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7bca04bf..f28efb46 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -7,7 +7,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
- go-version: '^1.21.0'
+ go-version: '^1.23.0'
- name: Checkout code
uses: actions/checkout@v3
- uses: actions/cache@v3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea05913d..55d951fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -154,7 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- Changed go module to `github.com/tomwright/dasel/v2` to ensure it works correctly with go modules.
+- Changed go module to `github.com/tomwright/dasel/v3` to ensure it works correctly with go modules.
## [v2.1.0] - 2023-01-11
diff --git a/README.md b/README.md
index e970e997..6e2e8796 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# dasel
[](https://daseldocs.tomwright.me)
-[](https://goreportcard.com/report/github.com/TomWright/dasel/v2)
-[](https://pkg.go.dev/github.com/tomwright/dasel/v2)
+[](https://goreportcard.com/report/github.com/tomwright/dasel/v3)
+[](https://pkg.go.dev/github.com/tomwright/dasel/v3)


[](https://codecov.io/gh/TomWright/dasel)
@@ -75,7 +75,7 @@ brew install dasel
You can also install a [development version](https://daseldocs.tomwright.me/installation#development-version) with:
```bash
-go install github.com/tomwright/dasel/v2/cmd/dasel@master
+go install github.com/tomwright/dasel/v3/cmd/dasel@master
```
For more information see the [installation documentation](https://daseldocs.tomwright.me/installation).
@@ -181,7 +181,7 @@ Please [open a discussion](https://github.com/TomWright/dasel/discussions) if:
- Uses a [standard query/selector syntax](https://daseldocs.tomwright.me/functions/selector-overview) across all data formats.
- Zero runtime dependencies.
- [Available on Linux, Mac and Windows](https://daseldocs.tomwright.me/installation).
-- Available to [import and use in your own projects](https://pkg.go.dev/github.com/tomwright/dasel/v2).
+- Available to [import and use in your own projects](https://pkg.go.dev/github.com/tomwright/dasel/v3).
- [Run via Docker](https://daseldocs.tomwright.me/installation#docker).
- [Faster than jq/yq](#benchmarks).
- [Pre-commit hooks](#pre-commit).
diff --git a/api.go b/api.go
new file mode 100644
index 00000000..0a3dec3d
--- /dev/null
+++ b/api.go
@@ -0,0 +1,55 @@
+// Package dasel contains everything you'll need to use dasel from a go application.
+package dasel
+
+import (
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// Query queries the data using the selector and returns the results.
+func Query(data any, selector string, opts ...execution.ExecuteOptionFn) ([]*model.Value, int, error) {
+ options := execution.NewOptions(opts...)
+ val := model.NewValue(data)
+ out, err := execution.ExecuteSelector(selector, val, options)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ if out.IsBranch() {
+ res := make([]*model.Value, 0)
+ if err := out.RangeSlice(func(i int, v *model.Value) error {
+ res = append(res, v)
+ return nil
+ }); err != nil {
+ return nil, 0, err
+ }
+ return res, len(res), nil
+ }
+
+ return []*model.Value{out}, 1, nil
+}
+
+func Select(data any, selector string, opts ...execution.ExecuteOptionFn) (any, int, error) {
+ res, count, err := Query(data, selector, opts...)
+ if err != nil {
+ return nil, 0, err
+ }
+ out := make([]any, 0)
+ for _, v := range res {
+ out = append(out, v.Interface())
+ }
+ return out, count, err
+}
+
+func Modify(data any, selector string, newValue any, opts ...execution.ExecuteOptionFn) (int, error) {
+ res, count, err := Query(data, selector, opts...)
+ if err != nil {
+ return 0, err
+ }
+ for _, v := range res {
+ if err := v.Set(model.NewValue(newValue)); err != nil {
+ return 0, err
+ }
+ }
+ return count, nil
+}
diff --git a/api_test.go b/api_test.go
new file mode 100644
index 00000000..6d99e86f
--- /dev/null
+++ b/api_test.go
@@ -0,0 +1,48 @@
+package dasel_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3"
+)
+
+type modifyTestCase struct {
+ selector string
+ in any
+ value any
+ exp any
+ count int
+}
+
+func (tc modifyTestCase) run(t *testing.T) {
+ count, err := dasel.Modify(&tc.in, tc.selector, tc.value)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if count != tc.count {
+ t.Errorf("unexpected count: %d", count)
+ }
+ if !cmp.Equal(tc.exp, tc.in) {
+ t.Errorf("unexpected result: %s", cmp.Diff(tc.exp, tc.in))
+ }
+}
+
+func TestModify(t *testing.T) {
+ t.Run("index", func(t *testing.T) {
+ t.Run("int over int", modifyTestCase{
+ selector: "$this[1]",
+ in: []int{1, 2, 3},
+ value: 4,
+ exp: []int{1, 4, 3},
+ count: 1,
+ }.run)
+ t.Run("string over int", modifyTestCase{
+ selector: "$this[1]",
+ in: []any{1, 2, 3},
+ value: "4",
+ exp: []any{1, "4", 3},
+ count: 1,
+ }.run)
+ })
+}
diff --git a/benchmark/README.md b/benchmark/README.md
deleted file mode 100644
index a4a86786..00000000
--- a/benchmark/README.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Benchmarks
-
-These benchmarks are auto generated using `./benchmark/run.sh`.
-
-```
-brew install hyperfine
-pip install matplotlib
-./benchmark/run.sh
-```
-
-I have put together what I believe to be equivalent commands in dasel/jq/yq.
-
-If you have any feedback or wish to add new benchmarks please submit a PR.
-## Benchmarks
-
-### Root Object
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 -f benchmark/data.json` | 6.6 ± 0.2 | 6.1 | 7.2 | 1.00 |
-| `dasel -f benchmark/data.json` | 8.7 ± 0.5 | 8.0 | 10.3 | 1.33 ± 0.09 |
-| `jq '.' benchmark/data.json` | 28.1 ± 0.7 | 27.0 | 31.5 | 4.28 ± 0.19 |
-| `yq --yaml-output '.' benchmark/data.yaml` | 127.9 ± 3.1 | 124.5 | 151.6 | 19.50 ± 0.84 |
-
-### Top level property
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 -f benchmark/data.json 'id'` | 6.6 ± 0.2 | 6.1 | 7.4 | 1.00 |
-| `dasel -f benchmark/data.json '.id'` | 8.3 ± 0.3 | 7.8 | 9.7 | 1.27 ± 0.06 |
-| `jq '.id' benchmark/data.json` | 28.2 ± 0.9 | 27.1 | 31.5 | 4.31 ± 0.21 |
-| `yq --yaml-output '.id' benchmark/data.yaml` | 128.4 ± 10.1 | 124.4 | 211.7 | 19.59 ± 1.71 |
-
-### Nested property
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 -f benchmark/data.json 'user.name.first'` | 6.5 ± 0.2 | 6.1 | 7.3 | 1.00 |
-| `dasel -f benchmark/data.json '.user.name.first'` | 8.3 ± 0.3 | 7.9 | 9.9 | 1.28 ± 0.07 |
-| `jq '.user.name.first' benchmark/data.json` | 28.2 ± 0.9 | 27.0 | 32.9 | 4.34 ± 0.22 |
-| `yq --yaml-output '.user.name.first' benchmark/data.yaml` | 126.7 ± 2.1 | 124.5 | 138.2 | 19.52 ± 0.81 |
-
-### Array index
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 -f benchmark/data.json 'favouriteNumbers.[1]'` | 6.5 ± 0.2 | 6.0 | 7.5 | 1.00 |
-| `dasel -f benchmark/data.json '.favouriteNumbers.[1]'` | 8.6 ± 0.7 | 7.9 | 11.3 | 1.33 ± 0.12 |
-| `jq '.favouriteNumbers[1]' benchmark/data.json` | 28.4 ± 1.6 | 27.3 | 38.1 | 4.36 ± 0.29 |
-| `yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml` | 128.3 ± 9.2 | 124.2 | 213.8 | 19.69 ± 1.59 |
-
-### Append to array of strings
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]'` | 6.6 ± 0.3 | 6.1 | 8.3 | 1.00 |
-| `dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue` | 8.4 ± 0.3 | 7.8 | 9.2 | 1.28 ± 0.07 |
-| `jq '.favouriteColours += ["blue"]' benchmark/data.json` | 28.3 ± 0.9 | 27.4 | 32.7 | 4.31 ± 0.25 |
-| `yq --yaml-output '.favouriteColours += ["blue"]' benchmark/data.yaml` | 127.6 ± 2.4 | 124.1 | 140.3 | 19.45 ± 1.01 |
-
-### Update a string value
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]'` | 6.6 ± 0.3 | 6.1 | 7.4 | 1.00 |
-| `dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue` | 9.5 ± 1.7 | 8.0 | 12.9 | 1.45 ± 0.27 |
-| `jq '.favouriteColours[0] = "blue"' benchmark/data.json` | 28.5 ± 1.3 | 27.3 | 33.1 | 4.33 ± 0.26 |
-| `yq --yaml-output '.favouriteColours[0] = "blue"' benchmark/data.yaml` | 127.3 ± 2.7 | 125.0 | 149.4 | 19.36 ± 0.86 |
-
-### Overwrite an object
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 put -f benchmark/data.json -o - -t json -v '{"first":"Frank","last":"Jones"}' 'user.name'` | 6.3 ± 0.3 | 6.0 | 7.2 | 1.00 |
-| `dasel put document -f benchmark/data.json -o - -d json '.user.name' '{"first":"Frank","last":"Jones"}'` | 8.3 ± 0.3 | 7.8 | 9.6 | 1.31 ± 0.07 |
-| `jq '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.json` | 28.2 ± 1.0 | 27.2 | 31.7 | 4.45 ± 0.23 |
-| `yq --yaml-output '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.yaml` | 127.5 ± 2.5 | 124.6 | 143.8 | 20.10 ± 0.89 |
-
-### List keys of an array
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 -f benchmark/data.json 'all().key()'` | 6.4 ± 0.3 | 6.0 | 7.4 | 1.00 |
-| `dasel -f benchmark/data.json -m '.-'` | 8.3 ± 0.3 | 7.8 | 9.6 | 1.30 ± 0.07 |
-| `jq 'keys[]' benchmark/data.json` | 28.1 ± 1.0 | 27.1 | 32.1 | 4.41 ± 0.24 |
-| `yq --yaml-output 'keys[]' benchmark/data.yaml` | 126.6 ± 2.1 | 123.7 | 138.3 | 19.82 ± 0.88 |
-
-### Delete property
-
-
-
-| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
-|:---|---:|---:|---:|---:|
-| `daselv2 delete -f benchmark/data.json -o - 'id'` | 6.5 ± 0.3 | 6.1 | 8.2 | 1.00 |
-| `dasel delete -f benchmark/data.json -o - '.id'` | 8.4 ± 0.3 | 7.9 | 10.1 | 1.30 ± 0.08 |
-| `jq 'del(.id)' benchmark/data.json` | 28.3 ± 0.9 | 27.4 | 32.0 | 4.38 ± 0.24 |
-| `yq --yaml-output 'del(.id)' benchmark/data.yaml` | 127.5 ± 2.7 | 124.7 | 147.3 | 19.74 ± 0.99 |
diff --git a/benchmark/data.json b/benchmark/data.json
deleted file mode 100644
index 4db0e9a3..00000000
--- a/benchmark/data.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "id": "1234",
- "user": {
- "name": {
- "first": "Tom",
- "last": "Wright"
- }
- },
- "favouriteNumbers": [
- 1, 2, 3, 4
- ],
- "favouriteColours": [
- "red", "green"
- ],
- "phones": [
- {
- "make": "OnePlus",
- "model": "8 Pro"
- },
- {
- "make": "Apple",
- "model": "iPhone 12"
- }
- ]
-}
\ No newline at end of file
diff --git a/benchmark/data.yaml b/benchmark/data.yaml
deleted file mode 100644
index 8475f981..00000000
--- a/benchmark/data.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-id: "1234"
-user:
- name:
- first: Tom
- last: Wright
-favouriteNumbers:
- - 1
- - 2
- - 3
- - 4
-favouriteColours:
- - red
- - green
-phones:
- - make: OnePlus
- model: 8 Pro
- - make: Apple
- model: iPhone 12
diff --git a/benchmark/data/append_array_of_strings.json b/benchmark/data/append_array_of_strings.json
deleted file mode 100644
index 0d9de847..00000000
--- a/benchmark/data/append_array_of_strings.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]'",
- "mean": 0.006559234080000001,
- "stddev": 0.00031854298910522985,
- "median": 0.006491430500000001,
- "user": 0.0034752449999999983,
- "system": 0.0019132700000000006,
- "min": 0.0061455780000000005,
- "max": 0.008277945000000002,
- "times": [
- 0.006303749000000001,
- 0.006631118000000002,
- 0.006537565,
- 0.007438413000000001,
- 0.0064620170000000005,
- 0.006565493,
- 0.006521805,
- 0.006858463,
- 0.006817912000000001,
- 0.006348036000000001,
- 0.006693780000000002,
- 0.006865791000000001,
- 0.006582719000000001,
- 0.006369064000000001,
- 0.006204007000000001,
- 0.006465219000000001,
- 0.006740448000000001,
- 0.006536465000000002,
- 0.006334705000000001,
- 0.00631221,
- 0.006295486000000001,
- 0.006552023000000001,
- 0.006945283000000002,
- 0.007000094000000002,
- 0.006553250000000002,
- 0.0063897450000000005,
- 0.006361923,
- 0.006184466000000001,
- 0.006665724000000001,
- 0.006420157000000001,
- 0.006370204000000001,
- 0.006288761,
- 0.006343052000000002,
- 0.006363932000000001,
- 0.00647316,
- 0.006402695000000002,
- 0.006668931000000001,
- 0.006541594000000001,
- 0.006582801000000001,
- 0.006318445000000001,
- 0.006391798000000001,
- 0.006816351000000002,
- 0.006677724000000001,
- 0.006354416000000002,
- 0.006566185,
- 0.006488604,
- 0.006282272,
- 0.006343139000000001,
- 0.006340616,
- 0.0061589270000000015,
- 0.006322890000000001,
- 0.006777022000000001,
- 0.006579106000000001,
- 0.006367268000000001,
- 0.0061455780000000005,
- 0.0063252740000000005,
- 0.007078150000000002,
- 0.006927046000000001,
- 0.006444273,
- 0.007286268,
- 0.008277945000000002,
- 0.006734930000000002,
- 0.006724939000000001,
- 0.0063336170000000015,
- 0.0064386220000000015,
- 0.0066451320000000015,
- 0.006584144000000002,
- 0.006644218,
- 0.0061654460000000015,
- 0.006394843000000001,
- 0.006808028000000001,
- 0.006577802000000001,
- 0.006859139,
- 0.0067177190000000005,
- 0.0066748760000000015,
- 0.006459542,
- 0.006456209000000001,
- 0.006369647000000001,
- 0.006324126000000001,
- 0.006717296000000001,
- 0.006654964000000001,
- 0.006629765000000001,
- 0.006462782,
- 0.0061580160000000005,
- 0.006389663,
- 0.0064942570000000015,
- 0.0076943210000000005,
- 0.006775359000000002,
- 0.006167174000000001,
- 0.0062170340000000015,
- 0.006594792,
- 0.006984558000000002,
- 0.006617657000000001,
- 0.006359693000000001,
- 0.006477142000000002,
- 0.006428988000000002,
- 0.006472503000000001,
- 0.006237141000000002,
- 0.006502066000000001,
- 0.006713701000000001
- ]
- },
- {
- "command": "dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue",
- "mean": 0.008422656970000003,
- "stddev": 0.00025259091696336404,
- "median": 0.008380131000000002,
- "user": 0.004592984999999998,
- "system": 0.0025722500000000003,
- "min": 0.007787787000000001,
- "max": 0.009244352,
- "times": [
- 0.008415565000000002,
- 0.008523451000000001,
- 0.008440252,
- 0.009034088000000001,
- 0.007981774,
- 0.008095846,
- 0.008162243000000001,
- 0.008717780000000001,
- 0.008437570000000002,
- 0.008281179000000001,
- 0.008536304000000002,
- 0.008375172000000002,
- 0.008247918000000002,
- 0.008279651,
- 0.008390803,
- 0.008225355000000002,
- 0.008426573000000001,
- 0.008228413,
- 0.008110071000000002,
- 0.008865932000000002,
- 0.008358800000000001,
- 0.008252533000000001,
- 0.008431301,
- 0.008291585,
- 0.008620712,
- 0.008442852,
- 0.008188211,
- 0.008339394000000002,
- 0.008153110000000002,
- 0.008107502,
- 0.008528822000000002,
- 0.008870019000000002,
- 0.008918309000000001,
- 0.008473604000000001,
- 0.008328539000000001,
- 0.008514341000000002,
- 0.008199196,
- 0.008438659000000001,
- 0.008860850000000002,
- 0.008555207,
- 0.008248031000000001,
- 0.008231746000000002,
- 0.008178108000000002,
- 0.008615023000000001,
- 0.008343317000000001,
- 0.008848316000000002,
- 0.008246622,
- 0.008353140000000002,
- 0.008884523000000002,
- 0.008460423000000002,
- 0.008209031,
- 0.008008005,
- 0.008596640000000001,
- 0.008597531000000002,
- 0.008138745000000001,
- 0.008335233000000001,
- 0.008312650000000001,
- 0.008536947000000001,
- 0.008326726000000001,
- 0.008189442000000002,
- 0.008295920000000002,
- 0.008300092000000002,
- 0.008616051000000001,
- 0.008408462000000002,
- 0.008564943000000002,
- 0.008644960000000002,
- 0.008815857000000002,
- 0.008879512,
- 0.009244352,
- 0.008562036,
- 0.008493366,
- 0.008165944000000001,
- 0.008532769,
- 0.009191958000000002,
- 0.008291675000000002,
- 0.008156465000000002,
- 0.008450780000000001,
- 0.008731780000000001,
- 0.008357380000000001,
- 0.008227090000000001,
- 0.008327979000000001,
- 0.008419013000000001,
- 0.008301521000000001,
- 0.007787787000000001,
- 0.008470077000000001,
- 0.008395379000000001,
- 0.008554001,
- 0.008121759000000001,
- 0.008370102,
- 0.008148166000000002,
- 0.008274022,
- 0.008502364000000002,
- 0.008295774,
- 0.008303814000000001,
- 0.008456303000000002,
- 0.008316067000000002,
- 0.008602751,
- 0.008684052000000001,
- 0.008385090000000001,
- 0.008308599000000002
- ]
- },
- {
- "command": "jq '.favouriteColours += [\"blue\"]' benchmark/data.json",
- "mean": 0.02827737350000002,
- "stddev": 0.0009034554968123969,
- "median": 0.027977958500000004,
- "user": 0.023749294999999986,
- "system": 0.0010989999999999995,
- "min": 0.027433315000000003,
- "max": 0.032711741,
- "times": [
- 0.028249142,
- 0.027607176000000004,
- 0.028237523000000004,
- 0.027965739000000007,
- 0.027833120000000006,
- 0.028149408000000004,
- 0.027752193000000005,
- 0.027708494000000004,
- 0.028167615000000003,
- 0.027505071000000002,
- 0.027818529000000005,
- 0.027737869000000002,
- 0.027818596000000004,
- 0.028173534000000004,
- 0.027433315000000003,
- 0.027962974000000005,
- 0.027878396000000003,
- 0.027924166000000004,
- 0.027681920000000006,
- 0.028897150000000007,
- 0.028638061000000003,
- 0.029799878000000005,
- 0.028602848000000004,
- 0.027682301000000003,
- 0.027993433,
- 0.030802984000000002,
- 0.028793343000000002,
- 0.027658053000000005,
- 0.027993507000000004,
- 0.028521237,
- 0.027550071000000002,
- 0.027552987000000004,
- 0.027854634000000007,
- 0.027478314000000004,
- 0.028739950000000004,
- 0.028959976000000002,
- 0.032711741,
- 0.028446239,
- 0.031081846000000007,
- 0.028377441000000007,
- 0.027803511000000006,
- 0.028191663000000002,
- 0.028819486000000002,
- 0.027961312000000006,
- 0.028230457000000004,
- 0.027755106000000005,
- 0.027871419000000005,
- 0.028027399000000005,
- 0.027746458000000005,
- 0.028239528000000003,
- 0.027508068000000004,
- 0.027703518000000007,
- 0.028501507000000006,
- 0.027968828000000005,
- 0.027599649000000004,
- 0.029671172000000006,
- 0.028462668000000007,
- 0.028453872,
- 0.030067800000000002,
- 0.028698855000000006,
- 0.027985984000000002,
- 0.027701038,
- 0.027873509,
- 0.027845307000000003,
- 0.030754275,
- 0.027844408000000005,
- 0.027958159000000007,
- 0.027847244000000004,
- 0.028165775000000007,
- 0.027454797000000003,
- 0.027532953000000002,
- 0.027947877000000006,
- 0.027456149000000003,
- 0.027607745000000003,
- 0.029191528000000005,
- 0.030514849000000007,
- 0.028020067000000006,
- 0.028056035000000003,
- 0.028045762000000005,
- 0.027502740000000005,
- 0.027836392000000005,
- 0.027846828000000007,
- 0.028450904000000003,
- 0.028528377000000004,
- 0.027995119000000002,
- 0.028167002000000007,
- 0.027766284000000006,
- 0.029786385000000002,
- 0.027920931000000006,
- 0.028838241000000004,
- 0.027497927000000005,
- 0.027969933000000006,
- 0.027687134000000006,
- 0.028433188,
- 0.030652526000000003,
- 0.028315720000000006,
- 0.028198241000000002,
- 0.027792101000000003,
- 0.027434784000000004,
- 0.028290082000000005
- ]
- },
- {
- "command": "yq --yaml-output '.favouriteColours += [\"blue\"]' benchmark/data.yaml",
- "mean": 0.12757348592000006,
- "stddev": 0.002423386351919514,
- "median": 0.126939505,
- "user": 0.100982175,
- "system": 0.02253113999999999,
- "min": 0.124055579,
- "max": 0.140345268,
- "times": [
- 0.128675823,
- 0.126400477,
- 0.125956854,
- 0.13034874500000002,
- 0.13147044200000002,
- 0.126811315,
- 0.127014623,
- 0.127606391,
- 0.126181221,
- 0.125620472,
- 0.12679447800000002,
- 0.129316076,
- 0.127785916,
- 0.12570957400000002,
- 0.12676715600000002,
- 0.128046135,
- 0.126416467,
- 0.127508558,
- 0.130695916,
- 0.126317928,
- 0.12795430300000002,
- 0.126423871,
- 0.13034294200000002,
- 0.127369654,
- 0.127041136,
- 0.12736871,
- 0.125452362,
- 0.126104472,
- 0.126491188,
- 0.126336209,
- 0.127610943,
- 0.127407252,
- 0.130519425,
- 0.12742024500000002,
- 0.12682843000000002,
- 0.129290077,
- 0.128543136,
- 0.126169411,
- 0.127122389,
- 0.129844372,
- 0.12644313000000001,
- 0.126928857,
- 0.125591383,
- 0.140345268,
- 0.139181639,
- 0.128848355,
- 0.13040216400000001,
- 0.12674186,
- 0.126526087,
- 0.125780825,
- 0.124903615,
- 0.128609726,
- 0.12974159400000002,
- 0.126703288,
- 0.12824459900000001,
- 0.12574405800000002,
- 0.126099089,
- 0.12865711900000001,
- 0.129010554,
- 0.126941853,
- 0.128240893,
- 0.127679874,
- 0.13064425600000001,
- 0.131224538,
- 0.128336573,
- 0.12813685,
- 0.125827146,
- 0.12621572,
- 0.12560569200000002,
- 0.125860979,
- 0.124055579,
- 0.126881923,
- 0.125962619,
- 0.12518109900000002,
- 0.127471768,
- 0.127690343,
- 0.128166174,
- 0.127003408,
- 0.12652575300000002,
- 0.125387038,
- 0.126035472,
- 0.125921581,
- 0.126444672,
- 0.125781222,
- 0.127364204,
- 0.128575241,
- 0.125477276,
- 0.13448268700000002,
- 0.12798704,
- 0.125854668,
- 0.126937157,
- 0.126396237,
- 0.12654605600000002,
- 0.128356378,
- 0.126673739,
- 0.12586508700000001,
- 0.12480339500000001,
- 0.126783315,
- 0.12731166300000002,
- 0.12712312
- ]
- }
- ]
-}
diff --git a/benchmark/data/array_index.json b/benchmark/data/array_index.json
deleted file mode 100644
index 9fe8aa05..00000000
--- a/benchmark/data/array_index.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 -f benchmark/data.json 'favouriteNumbers.[1]'",
- "mean": 0.006514253420000005,
- "stddev": 0.00023920540841924985,
- "median": 0.006499365910000002,
- "user": 0.0034387200000000006,
- "system": 0.001897159999999999,
- "min": 0.006029065410000001,
- "max": 0.007518131410000002,
- "times": [
- 0.006359525410000002,
- 0.006347639410000001,
- 0.006576967410000002,
- 0.006356866410000002,
- 0.006479986410000001,
- 0.006564113410000003,
- 0.0065071984100000015,
- 0.006296185410000002,
- 0.006579091410000001,
- 0.006270604410000002,
- 0.006377850410000002,
- 0.006337435410000002,
- 0.0064835464100000025,
- 0.006029065410000001,
- 0.006624118410000003,
- 0.006805352410000002,
- 0.006504854410000002,
- 0.006361274410000001,
- 0.006281048410000002,
- 0.006218516410000001,
- 0.006569730410000002,
- 0.0067085914100000026,
- 0.006597795410000003,
- 0.006550108410000002,
- 0.006199433410000002,
- 0.006646654410000001,
- 0.0066885634100000025,
- 0.006426557410000002,
- 0.006253597410000002,
- 0.006507234410000002,
- 0.006513284410000002,
- 0.006509154410000002,
- 0.006532394410000001,
- 0.0062824794100000015,
- 0.006370951410000002,
- 0.007518131410000002,
- 0.0066445524100000024,
- 0.006324154410000001,
- 0.006117128410000002,
- 0.006303271410000002,
- 0.006296227410000002,
- 0.006374254410000002,
- 0.006644465410000001,
- 0.006604774410000002,
- 0.006767576410000002,
- 0.006675733410000002,
- 0.0066030214100000015,
- 0.006599147410000002,
- 0.006396714410000002,
- 0.0063837354100000025,
- 0.006366449410000002,
- 0.007385814410000002,
- 0.006801945410000001,
- 0.006620137410000003,
- 0.006766972410000002,
- 0.006599058410000002,
- 0.006998681410000002,
- 0.006589350410000002,
- 0.006254862410000003,
- 0.0064223514100000025,
- 0.006246433410000002,
- 0.006586239410000002,
- 0.006303758410000002,
- 0.006372647410000001,
- 0.006681676410000002,
- 0.006438418410000002,
- 0.006268517410000002,
- 0.006369256410000001,
- 0.006462034410000001,
- 0.006868734410000002,
- 0.006789160410000002,
- 0.006677967410000003,
- 0.006568746410000001,
- 0.0063789354100000015,
- 0.006527026410000003,
- 0.007141169410000002,
- 0.006495515410000001,
- 0.006374035410000001,
- 0.006697130410000002,
- 0.006644469410000003,
- 0.006503216410000002,
- 0.006392894410000002,
- 0.006225889410000001,
- 0.0064580144100000025,
- 0.0068659544100000015,
- 0.006844370410000002,
- 0.006483242410000001,
- 0.006274596410000001,
- 0.006224130410000002,
- 0.006540259410000002,
- 0.006295319410000002,
- 0.006269856410000002,
- 0.0063334874100000015,
- 0.006879290410000002,
- 0.006494981410000001,
- 0.006418471410000001,
- 0.006356486410000002,
- 0.0067143114100000015,
- 0.006515453410000002,
- 0.006666962410000001
- ]
- },
- {
- "command": "dasel -f benchmark/data.json '.favouriteNumbers.[1]'",
- "mean": 0.008647657960000004,
- "stddev": 0.0007277783433625674,
- "median": 0.008415794910000003,
- "user": 0.004660040000000002,
- "system": 0.0026595400000000006,
- "min": 0.007886576410000002,
- "max": 0.011274111410000003,
- "times": [
- 0.008091182410000002,
- 0.008500325410000002,
- 0.008437382410000001,
- 0.008153446410000003,
- 0.008247395410000002,
- 0.008421636410000002,
- 0.011274111410000003,
- 0.010322209410000002,
- 0.008463181410000003,
- 0.009376744410000002,
- 0.008555214410000001,
- 0.008573467410000002,
- 0.008128346410000003,
- 0.008813606410000003,
- 0.008438236410000003,
- 0.009196735410000002,
- 0.008564181410000002,
- 0.010511814410000002,
- 0.009468749410000001,
- 0.008802863410000002,
- 0.008605001410000002,
- 0.010925524410000002,
- 0.009946141410000001,
- 0.008616927410000003,
- 0.008637830410000002,
- 0.010160582410000002,
- 0.008824594410000003,
- 0.008436981410000002,
- 0.009372740410000002,
- 0.009647739410000002,
- 0.008806561410000002,
- 0.008272658410000001,
- 0.008816100410000001,
- 0.008556087410000001,
- 0.008144582410000002,
- 0.008199081410000001,
- 0.008271778410000001,
- 0.008409896410000003,
- 0.008233549410000002,
- 0.008232787410000003,
- 0.008105463410000001,
- 0.008582689410000002,
- 0.008192368410000001,
- 0.008075073410000001,
- 0.007968772410000002,
- 0.008254628410000003,
- 0.008527812410000002,
- 0.008479314410000003,
- 0.008354950410000003,
- 0.008147517410000003,
- 0.008525995410000002,
- 0.008955531410000002,
- 0.008065773410000003,
- 0.008226882410000002,
- 0.008409953410000002,
- 0.008214646410000002,
- 0.010703925410000002,
- 0.011114687410000003,
- 0.010800033410000002,
- 0.009392145410000002,
- 0.008435751410000003,
- 0.009202693410000002,
- 0.008168179410000002,
- 0.008639129410000002,
- 0.008029818410000002,
- 0.008601068410000003,
- 0.008394223410000002,
- 0.008408747410000002,
- 0.008187698410000001,
- 0.008291510410000001,
- 0.008568021410000001,
- 0.008768702410000002,
- 0.008409633410000002,
- 0.007994059410000002,
- 0.008288848410000002,
- 0.008213092410000003,
- 0.008336138410000003,
- 0.008298858410000002,
- 0.008338562410000002,
- 0.008073126410000003,
- 0.008204922410000003,
- 0.007886576410000002,
- 0.008812249410000002,
- 0.008339298410000001,
- 0.008733824410000002,
- 0.007943106410000003,
- 0.008180954410000002,
- 0.008153251410000001,
- 0.008367307410000002,
- 0.008332021410000001,
- 0.008155285410000003,
- 0.008937120410000002,
- 0.008431322410000002,
- 0.008183839410000003,
- 0.008171576410000003,
- 0.008491446410000002,
- 0.008337803410000002,
- 0.008296235410000002,
- 0.008074806410000001,
- 0.008528844410000002
- ]
- },
- {
- "command": "jq '.favouriteNumbers[1]' benchmark/data.json",
- "mean": 0.028413096960000005,
- "stddev": 0.001565334465344961,
- "median": 0.02790975291,
- "user": 0.023769529999999994,
- "system": 0.0011027,
- "min": 0.027314145410000004,
- "max": 0.03813759741,
- "times": [
- 0.02866351441,
- 0.02804803141,
- 0.02905255641,
- 0.028478343410000002,
- 0.02778846241,
- 0.027579653410000002,
- 0.02777273541,
- 0.028304104410000003,
- 0.02771720641,
- 0.02783232241,
- 0.02751481041,
- 0.02800383041,
- 0.027813935410000004,
- 0.02815265041,
- 0.027663818410000006,
- 0.02774647241,
- 0.027657070410000005,
- 0.027488081410000004,
- 0.027503223410000006,
- 0.02762275041,
- 0.027677073410000003,
- 0.02789480541,
- 0.02742048641,
- 0.02786538141,
- 0.02784820841,
- 0.02799416441,
- 0.02937163541,
- 0.02746712441,
- 0.028044678410000003,
- 0.02771839541,
- 0.028703898410000005,
- 0.02791840341,
- 0.027554731410000005,
- 0.030890312410000002,
- 0.029575748409999998,
- 0.02808533941,
- 0.02839717841,
- 0.027671851410000003,
- 0.02791822241,
- 0.028758057410000003,
- 0.02826042341,
- 0.02854033541,
- 0.027612497410000006,
- 0.02777538241,
- 0.032834956410000005,
- 0.02784191441,
- 0.027901283409999998,
- 0.028700409410000002,
- 0.02805565441,
- 0.02897347341,
- 0.03218960441,
- 0.027828689410000003,
- 0.02745777841,
- 0.02749297841,
- 0.027491953410000006,
- 0.027845749410000002,
- 0.027456611410000005,
- 0.027662951410000004,
- 0.028730480410000002,
- 0.02782251441,
- 0.028070582410000003,
- 0.027789150410000005,
- 0.027636199410000006,
- 0.027460479410000002,
- 0.02789893141,
- 0.029678910410000002,
- 0.027764806410000002,
- 0.02778805441,
- 0.027667394410000004,
- 0.028058332410000004,
- 0.027620474410000005,
- 0.027969598410000004,
- 0.028256382410000003,
- 0.02777342141,
- 0.02780203941,
- 0.028193748410000004,
- 0.02823348941,
- 0.02870256141,
- 0.03603843141,
- 0.03813759741,
- 0.02844438941,
- 0.03095494941,
- 0.027886718410000004,
- 0.02781991241,
- 0.030275500410000003,
- 0.028131206409999998,
- 0.02882531041,
- 0.027650028410000006,
- 0.028074380410000002,
- 0.028077266410000003,
- 0.02993324941,
- 0.027314145410000004,
- 0.028671839410000004,
- 0.028090556410000002,
- 0.02780517941,
- 0.028053429410000004,
- 0.029660858410000004,
- 0.027580942410000003,
- 0.02875648341,
- 0.02910785441
- ]
- },
- {
- "command": "yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml",
- "mean": 0.12829232476999997,
- "stddev": 0.009213408897836083,
- "median": 0.12641279091000002,
- "user": 0.10105771,
- "system": 0.02273175,
- "min": 0.12417706341000001,
- "max": 0.21379469041000002,
- "times": [
- 0.12554962341,
- 0.12477438441000002,
- 0.12587045241000003,
- 0.12486042041,
- 0.12674162841,
- 0.12793471041,
- 0.12637979241000002,
- 0.12532095641000002,
- 0.12417706341000001,
- 0.12476282241,
- 0.12967359741,
- 0.12590004441000002,
- 0.12704103041,
- 0.12584643541,
- 0.12737940941,
- 0.12785718641000002,
- 0.12561894041000002,
- 0.12914734541,
- 0.12707724841,
- 0.12584949741,
- 0.12768988541,
- 0.12790638941000002,
- 0.12886176941000002,
- 0.12559227941,
- 0.13309706441000002,
- 0.13219139341000002,
- 0.12629921441000003,
- 0.21379469041000002,
- 0.14367038841000002,
- 0.13730815441000002,
- 0.12636776141,
- 0.12595065941000003,
- 0.12815899541,
- 0.12532795641000002,
- 0.14030630841000002,
- 0.12699569041000003,
- 0.12568449441,
- 0.12593256241,
- 0.12808099341,
- 0.12975459141,
- 0.12540442341000002,
- 0.12478916041000002,
- 0.12717988841000002,
- 0.12717494841000002,
- 0.12750201241,
- 0.12793334341,
- 0.12674744941000002,
- 0.13053000241,
- 0.12924352141,
- 0.12764550341000003,
- 0.12725821641,
- 0.12614585541,
- 0.12508599341,
- 0.12599111241000002,
- 0.12504626641000002,
- 0.12777565041000002,
- 0.12954196041000002,
- 0.13542973941000003,
- 0.12591780941000003,
- 0.12617934741,
- 0.12547421441,
- 0.12693017841,
- 0.13412827841000002,
- 0.12755205341,
- 0.13084645941,
- 0.13301300941000002,
- 0.12548762441000003,
- 0.13012358141000002,
- 0.12678677941000002,
- 0.12610393741,
- 0.12544432441,
- 0.12504027641,
- 0.12644109741,
- 0.12456221241000001,
- 0.12691115041,
- 0.12645919641,
- 0.12456890341000001,
- 0.12492088641000001,
- 0.12607901341000002,
- 0.12595077741000002,
- 0.12487406141000001,
- 0.12877730841,
- 0.12494141741,
- 0.12586954341,
- 0.12640240541,
- 0.12451928741000001,
- 0.12590133241,
- 0.13326628341000002,
- 0.12642317641,
- 0.12915061241,
- 0.12627308241000001,
- 0.12645617441,
- 0.12630826541,
- 0.12598877341,
- 0.12562453741000001,
- 0.12810504541,
- 0.12544396141000003,
- 0.12650310441,
- 0.12588862641,
- 0.12443548841000002
- ]
- }
- ]
-}
diff --git a/benchmark/data/delete_property.json b/benchmark/data/delete_property.json
deleted file mode 100644
index 76df8236..00000000
--- a/benchmark/data/delete_property.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 delete -f benchmark/data.json -o - 'id'",
- "mean": 0.006462584990000004,
- "stddev": 0.0002934537907552468,
- "median": 0.006400216880000004,
- "user": 0.0034548449999999994,
- "system": 0.00191293,
- "min": 0.006071011380000003,
- "max": 0.008181844380000004,
- "times": [
- 0.006523620380000003,
- 0.006801800380000004,
- 0.006656418380000003,
- 0.006421608380000004,
- 0.006071011380000003,
- 0.006396515380000004,
- 0.006438390380000004,
- 0.006587852380000004,
- 0.006433019380000004,
- 0.006387138380000004,
- 0.006194296380000004,
- 0.006373346380000004,
- 0.006380730380000003,
- 0.006551954380000004,
- 0.0063849413800000036,
- 0.006292079380000003,
- 0.006709308380000004,
- 0.006726304380000004,
- 0.006560664380000003,
- 0.006151727380000003,
- 0.006210096380000004,
- 0.006484853380000004,
- 0.006225769380000003,
- 0.006519048380000003,
- 0.006295020380000004,
- 0.006076460380000004,
- 0.006386713380000004,
- 0.006260286380000004,
- 0.006264486380000003,
- 0.006516939380000003,
- 0.006223403380000004,
- 0.007283798380000003,
- 0.006271291380000004,
- 0.008181844380000004,
- 0.006308980380000004,
- 0.006491413380000003,
- 0.006714409380000003,
- 0.006287841380000004,
- 0.006159291380000003,
- 0.006347553380000004,
- 0.006107234380000003,
- 0.006788070380000004,
- 0.0063410983800000036,
- 0.006269166380000003,
- 0.006452880380000003,
- 0.006384252380000004,
- 0.006734236380000003,
- 0.0067479653800000036,
- 0.006189380380000003,
- 0.006183437380000004,
- 0.006469931380000004,
- 0.006469876380000003,
- 0.006227551380000003,
- 0.006518452380000003,
- 0.006227649380000004,
- 0.0064737283800000035,
- 0.007018632380000004,
- 0.006941159380000004,
- 0.006595595380000003,
- 0.006522178380000004,
- 0.007185969380000004,
- 0.006412871380000004,
- 0.0065832913800000035,
- 0.006759526380000004,
- 0.0062513163800000035,
- 0.007108463380000004,
- 0.006668878380000003,
- 0.006537349380000003,
- 0.006417351380000004,
- 0.006152635380000004,
- 0.006510916380000004,
- 0.006944444380000003,
- 0.006535870380000003,
- 0.006310626380000003,
- 0.006079835380000004,
- 0.006350962380000004,
- 0.006375414380000003,
- 0.006381707380000004,
- 0.006377108380000003,
- 0.006253683380000003,
- 0.006254289380000003,
- 0.006280740380000004,
- 0.0064039183800000034,
- 0.006243702380000004,
- 0.0064915613800000035,
- 0.006499035380000004,
- 0.006279454380000004,
- 0.006316164380000003,
- 0.006262746380000003,
- 0.006230493380000004,
- 0.006297001380000004,
- 0.006542056380000003,
- 0.006491284380000004,
- 0.0064794233800000035,
- 0.006534393380000003,
- 0.006631998380000004,
- 0.006696304380000004,
- 0.006312349380000004,
- 0.006228607380000004,
- 0.006370048380000003
- ]
- },
- {
- "command": "dasel delete -f benchmark/data.json -o - '.id'",
- "mean": 0.008384274170000004,
- "stddev": 0.00032488191902144457,
- "median": 0.008326066880000003,
- "user": 0.004562625,
- "system": 0.0025348400000000004,
- "min": 0.007896483380000003,
- "max": 0.010092166380000003,
- "times": [
- 0.008632449380000004,
- 0.008126327380000003,
- 0.008210599380000004,
- 0.008103489380000004,
- 0.008318152380000004,
- 0.007896483380000003,
- 0.008136694380000004,
- 0.008516831380000004,
- 0.008556746380000004,
- 0.008144872380000003,
- 0.008105856380000004,
- 0.008532452380000003,
- 0.008292100380000004,
- 0.008302018380000003,
- 0.008385852380000003,
- 0.008261829380000004,
- 0.008350843380000004,
- 0.008127957380000004,
- 0.008294285380000004,
- 0.008577841380000003,
- 0.008092074380000003,
- 0.008164201380000003,
- 0.008097422380000004,
- 0.008057035380000004,
- 0.008912470380000004,
- 0.008766830380000003,
- 0.008160295380000003,
- 0.008217624380000003,
- 0.009021105380000003,
- 0.008296080380000003,
- 0.008343097380000004,
- 0.008136188380000003,
- 0.008308167380000004,
- 0.008168921380000003,
- 0.008066609380000004,
- 0.008142299380000004,
- 0.008620015380000004,
- 0.008641791380000003,
- 0.008221956380000004,
- 0.008093134380000004,
- 0.008232084380000004,
- 0.010092166380000003,
- 0.008137383380000003,
- 0.008484122380000004,
- 0.008460836380000003,
- 0.008317360380000003,
- 0.008097974380000003,
- 0.008052076380000004,
- 0.008353207380000003,
- 0.008392616380000004,
- 0.008914519380000003,
- 0.008136258380000004,
- 0.008416963380000003,
- 0.008509991380000003,
- 0.008481845380000004,
- 0.008208618380000003,
- 0.008239198380000003,
- 0.008104179380000003,
- 0.008020998380000003,
- 0.008081001380000003,
- 0.008333981380000004,
- 0.008540593380000003,
- 0.008454602380000004,
- 0.008514009380000003,
- 0.008254774380000003,
- 0.008359444380000004,
- 0.008192411380000004,
- 0.008629116380000003,
- 0.008238469380000004,
- 0.008626174380000004,
- 0.008460451380000004,
- 0.008168089380000004,
- 0.008117273380000003,
- 0.008868779380000004,
- 0.008765519380000003,
- 0.008447181380000003,
- 0.008033323380000004,
- 0.008643269380000004,
- 0.008348699380000003,
- 0.009242104380000003,
- 0.008188816380000003,
- 0.008533904380000004,
- 0.008347605380000003,
- 0.008421365380000004,
- 0.008570044380000004,
- 0.008259236380000003,
- 0.008428365380000004,
- 0.009599609380000004,
- 0.008068953380000004,
- 0.008167179380000004,
- 0.008338064380000003,
- 0.008477878380000003,
- 0.008103978380000003,
- 0.008309656380000004,
- 0.008436991380000003,
- 0.008420416380000004,
- 0.008563287380000003,
- 0.008451805380000003,
- 0.008952137380000004,
- 0.008415449380000004
- ]
- },
- {
- "command": "jq 'del(.id)' benchmark/data.json",
- "mean": 0.028321697830000003,
- "stddev": 0.0008928528791512938,
- "median": 0.028021402380000004,
- "user": 0.023717495000000005,
- "system": 0.0011359500000000002,
- "min": 0.027389924380000007,
- "max": 0.031961913380000005,
- "times": [
- 0.02790503238,
- 0.027402959380000008,
- 0.02759823738,
- 0.027800803380000003,
- 0.027781630380000005,
- 0.027641249380000003,
- 0.027840866380000003,
- 0.027634188380000008,
- 0.028114909380000004,
- 0.02868352038,
- 0.027724118380000003,
- 0.02783807638,
- 0.027701622380000006,
- 0.028241956380000002,
- 0.027590296380000004,
- 0.028188352380000004,
- 0.028291702380000006,
- 0.028620646380000003,
- 0.029156677380000003,
- 0.028984475380000006,
- 0.029153603380000004,
- 0.029079290380000004,
- 0.02857449738,
- 0.028354916380000005,
- 0.029073862380000003,
- 0.030463022380000003,
- 0.028571739380000002,
- 0.029763361380000006,
- 0.028266568380000003,
- 0.02947337338,
- 0.03126591838,
- 0.027447031380000007,
- 0.02770254138,
- 0.028909887380000006,
- 0.028268380380000006,
- 0.02784867238,
- 0.027861454380000004,
- 0.027502637380000007,
- 0.031047283380000006,
- 0.027898154380000005,
- 0.028423720380000003,
- 0.028487762380000003,
- 0.027954284380000007,
- 0.02793334838,
- 0.02820465038,
- 0.02776063938,
- 0.030876964380000003,
- 0.031961913380000005,
- 0.028083573380000004,
- 0.02836499438,
- 0.027792560380000005,
- 0.02886763338,
- 0.027746354380000005,
- 0.02769368238,
- 0.028670414380000002,
- 0.027547167380000008,
- 0.028480088380000004,
- 0.02824932738,
- 0.028197317380000003,
- 0.027910828380000002,
- 0.027856331380000002,
- 0.027738983380000005,
- 0.028038293380000003,
- 0.027921961380000006,
- 0.02776414738,
- 0.028152741380000006,
- 0.028163816380000005,
- 0.027932751380000002,
- 0.02784527338,
- 0.027535820380000006,
- 0.02865774938,
- 0.027996039380000004,
- 0.027936055380000005,
- 0.02805494738,
- 0.027739949380000004,
- 0.028004511380000006,
- 0.027850633380000003,
- 0.031107026380000007,
- 0.027909694380000002,
- 0.029818966380000006,
- 0.027797977380000005,
- 0.028685633380000006,
- 0.027888780380000006,
- 0.029422888380000005,
- 0.028476081380000004,
- 0.02886139538,
- 0.028052449380000004,
- 0.027389924380000007,
- 0.02885513738,
- 0.027518568380000004,
- 0.027580699380000004,
- 0.027718841380000005,
- 0.027460099380000005,
- 0.027540914380000003,
- 0.02804553538,
- 0.028949621380000005,
- 0.028102977380000005,
- 0.02771834938,
- 0.027953803380000003,
- 0.027649672380000004
- ]
- },
- {
- "command": "yq --yaml-output 'del(.id)' benchmark/data.yaml",
- "mean": 0.12754083835000002,
- "stddev": 0.002708745253399845,
- "median": 0.12667261338000002,
- "user": 0.10090879500000001,
- "system": 0.022498000000000004,
- "min": 0.12466231938,
- "max": 0.14727682938,
- "times": [
- 0.12852639038000002,
- 0.13086764538,
- 0.13111911838,
- 0.13053014038000002,
- 0.12928528738,
- 0.13032076238,
- 0.13020268838000001,
- 0.13070791138,
- 0.12958945638000002,
- 0.13139219638000002,
- 0.12663775738000002,
- 0.12605091038000002,
- 0.12571975638000002,
- 0.12647100038,
- 0.12605892138000002,
- 0.12908346738,
- 0.12765379538000002,
- 0.13048580338000001,
- 0.12833174938,
- 0.12587022838,
- 0.12595621038000002,
- 0.12487088838000002,
- 0.12584067338000002,
- 0.12589552638,
- 0.12584753238000002,
- 0.12726676938,
- 0.12711129038000002,
- 0.12831101138,
- 0.12541951138000001,
- 0.12526978638,
- 0.12936814638000002,
- 0.12482319038,
- 0.12610445338,
- 0.12973859038000002,
- 0.12546581938,
- 0.12697492738000002,
- 0.12602576838000001,
- 0.12658640938000001,
- 0.12548401838,
- 0.12604172338,
- 0.12950636738000001,
- 0.12829646638,
- 0.12606076438,
- 0.12582380838,
- 0.12724688038,
- 0.12848656438,
- 0.12647799738,
- 0.13026748938000002,
- 0.12673980238000002,
- 0.12557189838000002,
- 0.12651612838,
- 0.12952494138,
- 0.12755315438,
- 0.12627444038000002,
- 0.12587682438,
- 0.12703641838000002,
- 0.12637814938,
- 0.12521580038000002,
- 0.12588377038,
- 0.12681269738,
- 0.12466231938,
- 0.12653075138,
- 0.12916903338000002,
- 0.12681703538,
- 0.12583430238,
- 0.13038447438,
- 0.14727682938,
- 0.12884357038000002,
- 0.12636121338,
- 0.12878266438000002,
- 0.12581123838,
- 0.12679651838,
- 0.12600387138000002,
- 0.12651411538000001,
- 0.12668466738,
- 0.13090388938,
- 0.12600993538000002,
- 0.12753866938,
- 0.12669067338,
- 0.13121109438,
- 0.12646465038000002,
- 0.12570582738,
- 0.13248716838000002,
- 0.12666055938,
- 0.12550285938,
- 0.12618612338000001,
- 0.12771003738,
- 0.12786413938000002,
- 0.12692584538,
- 0.12611658238,
- 0.12647015438,
- 0.12514854238,
- 0.12594847338,
- 0.12714151738,
- 0.12635010438000002,
- 0.12861604838000001,
- 0.12623786838,
- 0.12724344138000002,
- 0.12534301038,
- 0.13027641838
- ]
- }
- ]
-}
diff --git a/benchmark/data/list_array_keys.json b/benchmark/data/list_array_keys.json
deleted file mode 100644
index d5a6c6b0..00000000
--- a/benchmark/data/list_array_keys.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 -f benchmark/data.json 'all().key()'",
- "mean": 0.006386365404999999,
- "stddev": 0.00026077175804909504,
- "median": 0.006328408524999999,
- "user": 0.0034238599999999986,
- "system": 0.0018489000000000003,
- "min": 0.005956364024999998,
- "max": 0.0073663480249999995,
- "times": [
- 0.006823103024999998,
- 0.0064611990249999985,
- 0.006203120024999999,
- 0.006298995024999999,
- 0.006136317024999998,
- 0.006265511024999999,
- 0.006325998024999999,
- 0.006201800024999998,
- 0.006473850024999999,
- 0.006980715025,
- 0.006871870025,
- 0.006265309025,
- 0.006096389024999998,
- 0.0062431850249999995,
- 0.006527500025,
- 0.0063891230249999995,
- 0.006773702024999999,
- 0.0059579650249999986,
- 0.006081048025,
- 0.006370505025,
- 0.006391586024999999,
- 0.006510955024999998,
- 0.006139216024999999,
- 0.0064211310249999995,
- 0.006359475024999999,
- 0.006423912025,
- 0.006470235024999999,
- 0.006169161024999999,
- 0.0062588430249999995,
- 0.006670626024999998,
- 0.006243085024999998,
- 0.006269853024999998,
- 0.006219110024999999,
- 0.006872447025,
- 0.006281264024999999,
- 0.006568001024999998,
- 0.006368560024999998,
- 0.006228630024999998,
- 0.006340461025,
- 0.006514619024999999,
- 0.006886594024999999,
- 0.006789752024999999,
- 0.006273581025,
- 0.006353929024999999,
- 0.006761846025,
- 0.006524992025,
- 0.007333751024999999,
- 0.006076344024999999,
- 0.006295292024999999,
- 0.006118911024999999,
- 0.006885342024999999,
- 0.006238494024999998,
- 0.006240202025,
- 0.006256806025,
- 0.006330654024999999,
- 0.0065517670249999995,
- 0.006398470024999999,
- 0.006333304024999999,
- 0.006369339024999999,
- 0.006324547024999999,
- 0.006440147025,
- 0.006227710024999999,
- 0.005956364024999998,
- 0.006653155024999999,
- 0.006551595025,
- 0.0061279170249999996,
- 0.006482197025,
- 0.006370144024999999,
- 0.006060583024999999,
- 0.006080086025,
- 0.0067538890249999985,
- 0.006691448024999999,
- 0.006195852024999999,
- 0.0063250080249999995,
- 0.006491252024999999,
- 0.006618424024999999,
- 0.0062754600249999995,
- 0.006097442024999999,
- 0.006568754024999999,
- 0.006400322024999999,
- 0.006261797024999999,
- 0.006326163024999999,
- 0.0061050400249999985,
- 0.006077227024999999,
- 0.0073663480249999995,
- 0.006468680024999999,
- 0.006322582025,
- 0.006370484024999999,
- 0.006258679024999999,
- 0.006159159024999999,
- 0.0062668470249999985,
- 0.006446402025,
- 0.006340532024999999,
- 0.0061810080249999995,
- 0.006237171024999999,
- 0.006541650024999999,
- 0.006257626024999998,
- 0.006026297024999999,
- 0.0062337810249999985,
- 0.006209025024999999
- ]
- },
- {
- "command": "dasel -f benchmark/data.json -m '.-'",
- "mean": 0.008323275174999999,
- "stddev": 0.00028002756793531094,
- "median": 0.008276479024999998,
- "user": 0.00452566,
- "system": 0.00255623,
- "min": 0.007805135024999999,
- "max": 0.009648467025,
- "times": [
- 0.008065891025,
- 0.008671235024999998,
- 0.008198722025,
- 0.008494075024999999,
- 0.008079633024999998,
- 0.007861465025,
- 0.008049851024999999,
- 0.008756716025,
- 0.008310123024999998,
- 0.008153820024999998,
- 0.008190754025,
- 0.007980154024999998,
- 0.008726181024999998,
- 0.008454527024999998,
- 0.008488313025,
- 0.008157967025,
- 0.007910841025,
- 0.008276221024999999,
- 0.008496079024999999,
- 0.008384316024999998,
- 0.008385384024999998,
- 0.008041796024999999,
- 0.008243508024999999,
- 0.008078672025,
- 0.008629602024999999,
- 0.008481034025,
- 0.008670758025,
- 0.008558393025,
- 0.008284490025,
- 0.007908443025,
- 0.008073299024999998,
- 0.008405523025,
- 0.008455882025,
- 0.009648467025,
- 0.008517730025,
- 0.008529154025,
- 0.008240335025,
- 0.008569850024999999,
- 0.008523820025,
- 0.008712843025,
- 0.008886034025,
- 0.008155769024999999,
- 0.008021048024999998,
- 0.008613059024999998,
- 0.008367404025,
- 0.008262533024999998,
- 0.008008223025,
- 0.008115337024999999,
- 0.008722667024999999,
- 0.008367751024999998,
- 0.007981787025,
- 0.008272636024999998,
- 0.008273013024999998,
- 0.008211535024999998,
- 0.007860345025,
- 0.008329975024999998,
- 0.008507433025,
- 0.008627451025,
- 0.008120705024999997,
- 0.008023008025,
- 0.008352531024999998,
- 0.009055623025,
- 0.008261318024999999,
- 0.008040982025,
- 0.008222700024999998,
- 0.008530625024999999,
- 0.008230760024999999,
- 0.008276737024999998,
- 0.008292712025,
- 0.008402920025,
- 0.008108297024999999,
- 0.007805135024999999,
- 0.008118455024999999,
- 0.008473499024999998,
- 0.008274075024999997,
- 0.008128111024999999,
- 0.008212798024999999,
- 0.008205228025,
- 0.008277310024999998,
- 0.008403641024999998,
- 0.008219625025,
- 0.008486386025,
- 0.008839279025,
- 0.008407514025,
- 0.008318761025,
- 0.008447044025,
- 0.008214079024999998,
- 0.008495601025,
- 0.008050656024999998,
- 0.008150989025,
- 0.008097720024999998,
- 0.008593043025,
- 0.008128884024999998,
- 0.008025272024999999,
- 0.008130464024999999,
- 0.008202559025,
- 0.008048412025,
- 0.008377535025,
- 0.008719098025,
- 0.008307557024999999
- ]
- },
- {
- "command": "jq 'keys[]' benchmark/data.json",
- "mean": 0.028143955954999984,
- "stddev": 0.0009735027878388671,
- "median": 0.027865165025000003,
- "user": 0.023619860000000003,
- "system": 0.0010893000000000005,
- "min": 0.027056787025,
- "max": 0.032070168025,
- "times": [
- 0.027934758025,
- 0.027698561025,
- 0.031689497025,
- 0.027392124025000002,
- 0.027998845025000003,
- 0.028202600025000003,
- 0.027756287025,
- 0.027576571025,
- 0.027539574025000003,
- 0.027666792025,
- 0.029228857025,
- 0.027923499025,
- 0.027272014025000003,
- 0.028314678024999998,
- 0.027956029025000002,
- 0.027698238025000002,
- 0.027743653025000002,
- 0.027538497025,
- 0.027944904025,
- 0.028101713025000002,
- 0.027872767025000002,
- 0.028127928025,
- 0.027356851025,
- 0.027316379025,
- 0.028032076024999998,
- 0.027687635025000004,
- 0.028802335025,
- 0.027694205025,
- 0.027771865025000002,
- 0.028042469025000002,
- 0.027418824025,
- 0.027744745025000003,
- 0.030740821025000004,
- 0.028118357025,
- 0.027526325025000003,
- 0.027694109025000004,
- 0.027408485025000002,
- 0.028917232025000003,
- 0.028029996025000004,
- 0.028490818025000003,
- 0.027292986025,
- 0.028173846025000004,
- 0.027801290025,
- 0.027591944025000002,
- 0.027784581024999998,
- 0.031238030025000002,
- 0.029953265025000002,
- 0.028131364025,
- 0.027769269025,
- 0.027648732025,
- 0.027903154025,
- 0.027598100025000002,
- 0.027507720025,
- 0.028115994025,
- 0.028126665025000004,
- 0.028893995025,
- 0.028908099025,
- 0.027903923025,
- 0.027857563025000004,
- 0.027493749025,
- 0.030408693025000003,
- 0.027661406025,
- 0.027397789025,
- 0.028019378025,
- 0.027774265025,
- 0.027783169025,
- 0.027247329025000003,
- 0.032070168025,
- 0.027987995025000004,
- 0.028234832025000003,
- 0.027951489024999998,
- 0.027788086025,
- 0.027686869025,
- 0.028503507025,
- 0.027848491024999998,
- 0.027823441025,
- 0.028372427025,
- 0.027979096025000004,
- 0.028377635025,
- 0.027674458025,
- 0.027481699025,
- 0.027637930025,
- 0.027490243025,
- 0.027816246024999998,
- 0.027828545025,
- 0.028536270025000005,
- 0.028319113025000002,
- 0.032059504025000005,
- 0.028085674025,
- 0.027608832025000002,
- 0.030077762025000003,
- 0.027468421025000002,
- 0.028911626024999998,
- 0.027953957025000004,
- 0.028085417025000003,
- 0.028674344025,
- 0.028135312025000005,
- 0.027554706025,
- 0.027356500025,
- 0.027056787025
- ]
- },
- {
- "command": "yq --yaml-output 'keys[]' benchmark/data.yaml",
- "mean": 0.126571703835,
- "stddev": 0.0021394719133965433,
- "median": 0.125940086025,
- "user": 0.10013251000000002,
- "system": 0.022029060000000003,
- "min": 0.123734583025,
- "max": 0.13830425602500002,
- "times": [
- 0.125756854025,
- 0.128261602025,
- 0.13582618302500002,
- 0.13830425602500002,
- 0.125245255025,
- 0.125060733025,
- 0.128660257025,
- 0.129463940025,
- 0.12602612902500002,
- 0.12732227202500002,
- 0.125472846025,
- 0.125226568025,
- 0.124530027025,
- 0.124969231025,
- 0.12590821702500002,
- 0.128343272025,
- 0.127953705025,
- 0.12611021502500003,
- 0.12532648602500002,
- 0.12830964802500003,
- 0.12583718902500002,
- 0.126487929025,
- 0.128857570025,
- 0.126424799025,
- 0.125313309025,
- 0.12748003602500002,
- 0.12535427902500001,
- 0.128593588025,
- 0.12649578802500003,
- 0.125927754025,
- 0.125435495025,
- 0.128667324025,
- 0.125223930025,
- 0.126303508025,
- 0.12655347902500003,
- 0.12504773802500002,
- 0.12554994402500003,
- 0.12805640202500002,
- 0.126659992025,
- 0.12493408602499999,
- 0.12463694802500001,
- 0.128609102025,
- 0.126847961025,
- 0.125437499025,
- 0.12554400502500002,
- 0.12493894502500001,
- 0.125184227025,
- 0.12597276202500002,
- 0.12550308402500002,
- 0.12624721402500003,
- 0.12681439902500002,
- 0.123963375025,
- 0.124511463025,
- 0.12595756102500003,
- 0.12818226402500002,
- 0.12769913302500002,
- 0.12683222202500002,
- 0.125638937025,
- 0.12580227802500002,
- 0.12688296302500002,
- 0.12567133202500003,
- 0.12638211502500002,
- 0.12843807602500001,
- 0.12650712602500003,
- 0.12541215602500003,
- 0.12594728902500002,
- 0.125041758025,
- 0.12691484802500003,
- 0.12774400602500002,
- 0.125095811025,
- 0.125020227025,
- 0.124312893025,
- 0.12719350002500002,
- 0.12524880802500002,
- 0.124531921025,
- 0.129067605025,
- 0.12719105802500003,
- 0.131577940025,
- 0.124861034025,
- 0.12557717502500002,
- 0.12896975402500002,
- 0.12487534102499999,
- 0.123734583025,
- 0.12556551402500002,
- 0.12789639302500003,
- 0.129524043025,
- 0.127940503025,
- 0.12444353402500001,
- 0.12552950702500001,
- 0.129136937025,
- 0.125623067025,
- 0.12852164402500002,
- 0.124284457025,
- 0.12471930202499999,
- 0.12654868402500002,
- 0.12582711002500002,
- 0.12481392502500001,
- 0.12593288302500003,
- 0.127950950025,
- 0.125107395025
- ]
- }
- ]
-}
diff --git a/benchmark/data/nested_property.json b/benchmark/data/nested_property.json
deleted file mode 100644
index f9b72d92..00000000
--- a/benchmark/data/nested_property.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 -f benchmark/data.json 'user.name.first'",
- "mean": 0.006487652180000001,
- "stddev": 0.00024423167525266897,
- "median": 0.006445644290000002,
- "user": 0.003428965,
- "system": 0.0019433850000000006,
- "min": 0.006095448290000001,
- "max": 0.0073182502900000015,
- "times": [
- 0.006831172290000001,
- 0.006747688290000001,
- 0.006431944290000002,
- 0.006095448290000001,
- 0.0068636282900000015,
- 0.006326370290000003,
- 0.006240141290000003,
- 0.006234264290000001,
- 0.0064460912900000025,
- 0.006748853290000002,
- 0.006574874290000001,
- 0.006445601290000002,
- 0.0062569812900000014,
- 0.006912480290000002,
- 0.006396435290000002,
- 0.006361998290000002,
- 0.006593969290000001,
- 0.006442816290000002,
- 0.006294693290000002,
- 0.006737553290000002,
- 0.006523573290000003,
- 0.006657498290000003,
- 0.006547728290000002,
- 0.006445687290000002,
- 0.006576812290000003,
- 0.006292208290000002,
- 0.006404524290000001,
- 0.006124009290000001,
- 0.006472860290000002,
- 0.006285672290000002,
- 0.006710900290000002,
- 0.006383344290000002,
- 0.006260324290000003,
- 0.0061664112900000016,
- 0.0064424132900000024,
- 0.006538858290000001,
- 0.006285781290000002,
- 0.0063825252900000014,
- 0.006209047290000002,
- 0.0069047392900000015,
- 0.006824828290000001,
- 0.006356151290000002,
- 0.006280992290000002,
- 0.0065193832900000016,
- 0.0065049282900000015,
- 0.006443101290000001,
- 0.006304665290000002,
- 0.006197614290000002,
- 0.006482574290000001,
- 0.006546075290000002,
- 0.006921652290000002,
- 0.0064951202900000015,
- 0.006550217290000002,
- 0.006747768290000003,
- 0.007300800290000003,
- 0.006683423290000002,
- 0.006549836290000001,
- 0.006398740290000001,
- 0.006501656290000001,
- 0.006212342290000002,
- 0.006276076290000002,
- 0.0061985592900000025,
- 0.006132589290000002,
- 0.006110080290000002,
- 0.006380609290000002,
- 0.006570782290000002,
- 0.006505264290000002,
- 0.006190007290000002,
- 0.006595402290000002,
- 0.0063085012900000025,
- 0.006312516290000002,
- 0.006099382290000002,
- 0.006273319290000003,
- 0.006622400290000002,
- 0.006402412290000001,
- 0.006377549290000001,
- 0.006488725290000002,
- 0.006336644290000002,
- 0.006903563290000001,
- 0.0065346702900000025,
- 0.006507768290000002,
- 0.006706909290000002,
- 0.0065434882900000015,
- 0.0068588362900000015,
- 0.0073182502900000015,
- 0.006871688290000002,
- 0.006584755290000003,
- 0.006251557290000001,
- 0.006458628290000002,
- 0.006792576290000001,
- 0.0064006102900000025,
- 0.006258148290000001,
- 0.0064059592900000024,
- 0.006847130290000001,
- 0.006784295290000002,
- 0.006404388290000002,
- 0.006322567290000003,
- 0.006361548290000001,
- 0.006244743290000002,
- 0.006802520290000003
- ]
- },
- {
- "command": "dasel -f benchmark/data.json '.user.name.first'",
- "mean": 0.008322260480000005,
- "stddev": 0.00034664275213069337,
- "median": 0.008256586790000003,
- "user": 0.0045410749999999995,
- "system": 0.0025487450000000007,
- "min": 0.007858434290000002,
- "max": 0.009907730290000001,
- "times": [
- 0.008216180290000001,
- 0.008447594290000002,
- 0.008486039290000002,
- 0.008090117290000002,
- 0.008021255290000003,
- 0.008191652290000002,
- 0.008335455290000002,
- 0.008229900290000002,
- 0.008366511290000002,
- 0.008381936290000003,
- 0.008263803290000002,
- 0.008103882290000001,
- 0.008273973290000002,
- 0.008775878290000002,
- 0.008382195290000002,
- 0.008433673290000002,
- 0.008032323290000002,
- 0.008254274290000002,
- 0.008194083290000003,
- 0.008314864290000001,
- 0.008138005290000002,
- 0.008066073290000001,
- 0.008737797290000002,
- 0.008658454290000003,
- 0.008157911290000002,
- 0.008668914290000001,
- 0.008282954290000002,
- 0.008087636290000002,
- 0.008069797290000002,
- 0.008026231290000001,
- 0.008492900290000003,
- 0.008364670290000003,
- 0.008425305290000002,
- 0.008051114290000001,
- 0.008353079290000003,
- 0.008137967290000003,
- 0.007957766290000001,
- 0.007858434290000002,
- 0.008633432290000001,
- 0.008387260290000002,
- 0.008203868290000003,
- 0.008083204290000002,
- 0.008058489290000002,
- 0.008374324290000002,
- 0.008509135290000002,
- 0.008003664290000002,
- 0.007890454290000001,
- 0.008354729290000001,
- 0.008230575290000002,
- 0.008513773290000002,
- 0.008194116290000003,
- 0.008182666290000001,
- 0.007988283290000002,
- 0.009339137290000002,
- 0.009907730290000001,
- 0.009511054290000003,
- 0.008261410290000002,
- 0.008424228290000002,
- 0.008415410290000001,
- 0.008552510290000002,
- 0.008186474290000002,
- 0.008291928290000002,
- 0.008358543290000003,
- 0.008182654290000002,
- 0.008486117290000003,
- 0.008310434290000002,
- 0.008152130290000002,
- 0.009259653290000003,
- 0.008158226290000003,
- 0.008030379290000002,
- 0.007888863290000003,
- 0.007978166290000002,
- 0.008561305290000001,
- 0.008652051290000002,
- 0.008566420290000001,
- 0.007902313290000003,
- 0.008605550290000001,
- 0.008329847290000003,
- 0.008017567290000001,
- 0.008258899290000002,
- 0.008486068290000003,
- 0.008411088290000002,
- 0.008252188290000002,
- 0.008019006290000002,
- 0.009370380290000003,
- 0.008481246290000003,
- 0.008136593290000002,
- 0.008089808290000002,
- 0.008063672290000002,
- 0.008190584290000002,
- 0.008325156290000002,
- 0.008216602290000001,
- 0.008105132290000002,
- 0.007952897290000002,
- 0.008087382290000002,
- 0.007942431290000001,
- 0.008128853290000002,
- 0.008295839290000002,
- 0.008954371290000002,
- 0.008189159290000003
- ]
- },
- {
- "command": "jq '.user.name.first' benchmark/data.json",
- "mean": 0.028171564310000008,
- "stddev": 0.0009405597556538961,
- "median": 0.027870227290000004,
- "user": 0.023672114999999997,
- "system": 0.0010968650000000003,
- "min": 0.027035319290000002,
- "max": 0.03294296129,
- "times": [
- 0.028135227290000002,
- 0.027841496290000003,
- 0.027634541290000002,
- 0.028139402290000004,
- 0.029014549290000004,
- 0.029694246290000004,
- 0.027494167290000003,
- 0.027035319290000002,
- 0.028340424290000006,
- 0.027805572290000004,
- 0.02790869429,
- 0.027765366290000003,
- 0.027200904290000005,
- 0.028719174290000003,
- 0.02766834329,
- 0.027562324290000002,
- 0.027631049290000004,
- 0.02862848129,
- 0.028577670290000003,
- 0.029810451290000005,
- 0.027981760290000005,
- 0.02785606729,
- 0.02783750729,
- 0.028133694290000004,
- 0.028495573290000006,
- 0.027827889290000005,
- 0.02796772729,
- 0.027921327290000006,
- 0.03144434729,
- 0.027657399290000002,
- 0.027790770290000003,
- 0.027381074290000005,
- 0.028586187290000002,
- 0.027706438290000003,
- 0.027842664290000006,
- 0.028953905290000005,
- 0.028802964290000004,
- 0.02804459529,
- 0.027851360290000002,
- 0.030459600290000007,
- 0.027607017290000004,
- 0.028068594290000005,
- 0.027892314290000005,
- 0.028163632290000004,
- 0.027624205290000002,
- 0.027616289290000002,
- 0.027523722290000004,
- 0.027875247290000002,
- 0.028187214290000005,
- 0.027697603290000004,
- 0.03004398229,
- 0.028111386290000005,
- 0.027902625290000004,
- 0.02782274329,
- 0.027908481290000002,
- 0.02822099829,
- 0.027865207290000006,
- 0.027674405290000002,
- 0.027543510290000003,
- 0.028023704290000007,
- 0.028422084290000003,
- 0.028591124290000006,
- 0.02789644729,
- 0.02809500929,
- 0.027802746290000006,
- 0.027835874290000007,
- 0.02813325029,
- 0.028921283290000002,
- 0.028330407290000005,
- 0.02814936129,
- 0.027675654290000004,
- 0.027654205290000004,
- 0.027860719290000002,
- 0.03294296129,
- 0.02781811529,
- 0.027434146290000003,
- 0.027599300290000003,
- 0.027427154290000002,
- 0.027447405290000004,
- 0.02900386329,
- 0.027739355290000003,
- 0.027714250290000006,
- 0.02760863729,
- 0.02793279429,
- 0.028196587290000005,
- 0.02767645129,
- 0.028033950290000005,
- 0.027682183290000002,
- 0.031910497290000006,
- 0.027786269290000006,
- 0.02794948329,
- 0.027568970290000005,
- 0.027483134290000003,
- 0.02771581429,
- 0.027842668290000004,
- 0.02841784329,
- 0.03082046729,
- 0.02789747729,
- 0.027788662290000002,
- 0.027826680290000003
- ]
- },
- {
- "command": "yq --yaml-output '.user.name.first' benchmark/data.yaml",
- "mean": 0.12665368810000005,
- "stddev": 0.0021426641685805604,
- "median": 0.12598725429000002,
- "user": 0.100011725,
- "system": 0.022276244999999997,
- "min": 0.12445907229,
- "max": 0.13821544329,
- "times": [
- 0.12536145529,
- 0.12544597029000001,
- 0.12480518729000001,
- 0.12563508729,
- 0.12545197529,
- 0.12732867729,
- 0.12500454829,
- 0.12613088929,
- 0.12552378829,
- 0.12462444029,
- 0.12538138629,
- 0.13174731829,
- 0.12729835829,
- 0.12591004529,
- 0.12547844429,
- 0.12481067929,
- 0.12542326529,
- 0.12745116929,
- 0.12895119829,
- 0.12666287729,
- 0.12563772129,
- 0.12583449529000001,
- 0.12946466229,
- 0.12533437429,
- 0.12686138029,
- 0.12709797529,
- 0.12827146829,
- 0.12785409529,
- 0.12688063329,
- 0.12530266729,
- 0.12472259028999999,
- 0.12516347629,
- 0.12629098229,
- 0.12590087029,
- 0.12713439629,
- 0.12503758229,
- 0.12445907229,
- 0.12771501629,
- 0.12641518629,
- 0.12457429629,
- 0.12864072429,
- 0.12457474329000001,
- 0.12671371329,
- 0.12464369729,
- 0.12510077129,
- 0.12494224629,
- 0.12565757929000002,
- 0.12621914329,
- 0.12981714529,
- 0.12531311929,
- 0.12672209029,
- 0.12655298829,
- 0.12571011429,
- 0.12570983129,
- 0.12532608029,
- 0.12583801529,
- 0.12466616928999999,
- 0.12895168929,
- 0.13051969829,
- 0.12770234229,
- 0.12708315429,
- 0.12768032729,
- 0.12612925929,
- 0.12758765529,
- 0.13092517529,
- 0.12879533229,
- 0.12821580329,
- 0.12497348229,
- 0.12515832629,
- 0.12481059728999999,
- 0.12612197929,
- 0.12967891629,
- 0.12661266229,
- 0.13029565129,
- 0.12650265929,
- 0.12910420729,
- 0.13088482929,
- 0.12452952529,
- 0.12587831729,
- 0.12466936129,
- 0.12698203129,
- 0.12514577529,
- 0.12563987629,
- 0.12710649329,
- 0.12688229029,
- 0.12800063429,
- 0.12585519229,
- 0.12688132429,
- 0.12454633529,
- 0.12546563929000001,
- 0.12530741029,
- 0.12501238529,
- 0.12504354929,
- 0.12462488729000001,
- 0.12606446329,
- 0.12464945729000002,
- 0.12808851229,
- 0.13319983729,
- 0.13821544329,
- 0.12731844629
- ]
- }
- ]
-}
diff --git a/benchmark/data/overwrite_object.json b/benchmark/data/overwrite_object.json
deleted file mode 100644
index 34955558..00000000
--- a/benchmark/data/overwrite_object.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 put -f benchmark/data.json -o - -t json -v '{\"first\":\"Frank\",\"last\":\"Jones\"}' 'user.name'",
- "mean": 0.006341373460000002,
- "stddev": 0.00025171601114701303,
- "median": 0.006286778339999999,
- "user": 0.0034100350000000014,
- "system": 0.0018213249999999997,
- "min": 0.005981926339999999,
- "max": 0.007235230339999999,
- "times": [
- 0.006233814339999999,
- 0.006371497339999999,
- 0.00656039334,
- 0.00625557834,
- 0.00616375334,
- 0.006115574339999999,
- 0.0063470583399999995,
- 0.0070183893399999985,
- 0.00639418034,
- 0.0063792083399999994,
- 0.006037023339999999,
- 0.006110430339999999,
- 0.006430860339999999,
- 0.006133216339999999,
- 0.006313927339999999,
- 0.006303803339999999,
- 0.006427428339999999,
- 0.0064329383399999995,
- 0.006263417339999999,
- 0.00625479034,
- 0.005997828339999999,
- 0.006180127339999999,
- 0.00643621034,
- 0.006277918339999999,
- 0.006193547339999999,
- 0.006083440339999999,
- 0.00639727434,
- 0.006354280339999999,
- 0.00697133334,
- 0.00639906134,
- 0.00607994934,
- 0.006134735339999999,
- 0.00629563834,
- 0.00621280934,
- 0.00619897734,
- 0.00612636934,
- 0.006450544339999999,
- 0.0069621083399999985,
- 0.006539925339999999,
- 0.00630446734,
- 0.005981926339999999,
- 0.006406678339999999,
- 0.0066075293399999995,
- 0.006880499339999999,
- 0.00616346934,
- 0.006171480339999999,
- 0.006258659339999998,
- 0.00608151434,
- 0.006234356339999999,
- 0.00635193534,
- 0.006218463339999999,
- 0.006272819339999999,
- 0.006316442339999999,
- 0.00658866434,
- 0.006216271339999999,
- 0.00606850334,
- 0.00647316534,
- 0.00624359834,
- 0.0066381353399999985,
- 0.00603157134,
- 0.006296354339999999,
- 0.006425741339999999,
- 0.006197608339999999,
- 0.006221534339999999,
- 0.006118788339999999,
- 0.006270434339999999,
- 0.0066267123399999985,
- 0.006375894339999999,
- 0.007235230339999999,
- 0.006158178339999999,
- 0.00616178834,
- 0.006694705339999999,
- 0.006355839339999999,
- 0.006536538339999998,
- 0.0060859433399999985,
- 0.006160449339999999,
- 0.006437025339999999,
- 0.006253797339999999,
- 0.00630721934,
- 0.00665190534,
- 0.0059899193399999984,
- 0.006478852339999999,
- 0.006113119339999999,
- 0.006436469339999999,
- 0.005990861339999999,
- 0.0061798103399999995,
- 0.006978912339999999,
- 0.0071938373399999984,
- 0.00625667934,
- 0.006313264339999999,
- 0.006245034339999999,
- 0.00645978834,
- 0.006516215339999999,
- 0.006189350339999998,
- 0.006160162339999999,
- 0.006315909339999998,
- 0.006576675339999999,
- 0.00641240834,
- 0.00619732334,
- 0.00621155434
- ]
- },
- {
- "command": "dasel put document -f benchmark/data.json -o - -d json '.user.name' '{\"first\":\"Frank\",\"last\":\"Jones\"}'",
- "mean": 0.008305120169999996,
- "stddev": 0.0003055619245520301,
- "median": 0.00824025334,
- "user": 0.004561955,
- "system": 0.0024566849999999993,
- "min": 0.007826440339999999,
- "max": 0.00956123634,
- "times": [
- 0.00815616334,
- 0.00842398634,
- 0.008201165339999998,
- 0.00842376934,
- 0.00851224634,
- 0.00817089434,
- 0.00797620734,
- 0.00837247434,
- 0.00814944234,
- 0.008061429339999999,
- 0.008073310339999999,
- 0.00834033234,
- 0.00855247134,
- 0.00861009634,
- 0.00827284534,
- 0.00816883534,
- 0.008226205339999999,
- 0.00823579534,
- 0.008420996339999999,
- 0.00815685934,
- 0.00848492834,
- 0.008241168339999999,
- 0.007959077339999999,
- 0.00812036934,
- 0.00838146134,
- 0.00834674534,
- 0.00854426834,
- 0.008997975339999999,
- 0.00888003834,
- 0.00830601634,
- 0.00815826634,
- 0.00823933834,
- 0.008825796339999999,
- 0.008247047339999999,
- 0.00817231934,
- 0.007826440339999999,
- 0.008487560339999999,
- 0.008506216339999999,
- 0.008029968339999999,
- 0.008215666339999999,
- 0.008447306339999999,
- 0.008218668339999999,
- 0.008063742339999999,
- 0.007912756339999999,
- 0.008356535339999999,
- 0.00821390034,
- 0.00837144234,
- 0.00794838734,
- 0.00798794234,
- 0.00843467734,
- 0.00844300634,
- 0.007995561339999999,
- 0.007987371339999999,
- 0.00825739734,
- 0.00851154834,
- 0.008477175339999999,
- 0.009025570339999999,
- 0.00868199834,
- 0.008182787339999999,
- 0.00789868534,
- 0.008099541339999999,
- 0.00956123634,
- 0.00813962134,
- 0.00814225134,
- 0.00803994334,
- 0.00830310034,
- 0.00863678934,
- 0.00810758734,
- 0.007928605339999999,
- 0.00805092034,
- 0.00812647834,
- 0.00807571134,
- 0.00796316534,
- 0.00834672634,
- 0.00838763134,
- 0.00824460634,
- 0.008008575339999999,
- 0.00804327534,
- 0.008130260339999999,
- 0.00856786834,
- 0.007986876339999999,
- 0.00826565334,
- 0.00852794434,
- 0.00876601634,
- 0.00858587934,
- 0.009037517339999999,
- 0.009427813339999999,
- 0.008305326339999999,
- 0.008079328339999999,
- 0.008006308339999999,
- 0.008327199339999999,
- 0.00848087034,
- 0.008293095339999999,
- 0.00804069834,
- 0.008201009339999999,
- 0.00816989334,
- 0.00834348834,
- 0.00811271634,
- 0.00813625134,
- 0.008621583339999999
- ]
- },
- {
- "command": "jq '.user.name = {\"first\":\"Frank\",\"last\":\"Jones\"}' benchmark/data.json",
- "mean": 0.028219372960000003,
- "stddev": 0.0009514379323784507,
- "median": 0.027847021340000003,
- "user": 0.023802905,
- "system": 0.0010287949999999999,
- "min": 0.027172288340000003,
- "max": 0.03170986134,
- "times": [
- 0.027836747340000002,
- 0.029546909340000004,
- 0.030286126340000004,
- 0.028043875340000003,
- 0.029906048340000005,
- 0.028626713340000005,
- 0.031294193340000005,
- 0.02897921834,
- 0.02741279334,
- 0.02761746734,
- 0.027775631340000002,
- 0.027353716340000004,
- 0.027396287340000002,
- 0.02801330734,
- 0.027298554340000003,
- 0.027683096340000005,
- 0.02773906334,
- 0.027172288340000003,
- 0.02874248434,
- 0.027897162340000003,
- 0.028742514340000004,
- 0.027563145340000005,
- 0.02779021334,
- 0.02754357734,
- 0.029230977340000006,
- 0.02752237634,
- 0.027928665340000006,
- 0.027670670340000002,
- 0.027573084340000005,
- 0.02723672434,
- 0.02755409834,
- 0.02805305634,
- 0.027633621340000004,
- 0.027534345340000002,
- 0.02770430234,
- 0.027501010340000003,
- 0.02812557334,
- 0.027991434340000003,
- 0.02819818534,
- 0.027619045340000002,
- 0.027507159340000004,
- 0.027520404340000002,
- 0.02787176334,
- 0.028829086340000004,
- 0.02938263234,
- 0.02725168534,
- 0.02765125534,
- 0.02757334234,
- 0.02744664134,
- 0.027457173340000002,
- 0.027729292340000003,
- 0.02734225934,
- 0.030621315340000003,
- 0.028711967340000005,
- 0.02812930434,
- 0.02766086734,
- 0.02902451734,
- 0.02788596434,
- 0.027809459340000002,
- 0.027857295340000005,
- 0.02765912634,
- 0.027516572340000003,
- 0.02773792134,
- 0.02868376434,
- 0.028573131340000002,
- 0.03001327434,
- 0.03170986134,
- 0.02945307734,
- 0.028157527340000002,
- 0.02918236734,
- 0.027910540340000005,
- 0.02783274434,
- 0.028783700340000003,
- 0.027521628340000003,
- 0.028661903340000003,
- 0.02786713934,
- 0.02890315634,
- 0.030401162340000003,
- 0.027814426340000004,
- 0.03107529834,
- 0.02764136934,
- 0.02737561834,
- 0.02752955134,
- 0.02786980134,
- 0.02770941734,
- 0.028435474340000003,
- 0.027321903340000002,
- 0.02773080134,
- 0.027691142340000002,
- 0.027764648340000003,
- 0.028251598340000003,
- 0.028448460340000002,
- 0.027352670340000003,
- 0.02733155434,
- 0.029367740340000004,
- 0.028063202340000003,
- 0.028345224340000003,
- 0.029124822340000002,
- 0.029439972340000002,
- 0.02908231034
- ]
- },
- {
- "command": "yq --yaml-output '.user.name = {\"first\":\"Frank\",\"last\":\"Jones\"}' benchmark/data.yaml",
- "mean": 0.12746747059000002,
- "stddev": 0.0025282221610687414,
- "median": 0.12681947284,
- "user": 0.10092174500000001,
- "system": 0.022513125000000002,
- "min": 0.12456903434000001,
- "max": 0.14375962434,
- "times": [
- 0.12654605234000002,
- 0.12575955934000002,
- 0.12708964234,
- 0.12647232434000003,
- 0.12470873034,
- 0.12843976134,
- 0.12656671734000002,
- 0.12634188334000002,
- 0.12881208834000002,
- 0.12740605434000002,
- 0.12891338934000002,
- 0.12694720834,
- 0.12576680034,
- 0.12737960634,
- 0.12615119534000002,
- 0.12987429334,
- 0.12530916434000003,
- 0.12599174034000002,
- 0.12836345734000001,
- 0.12587978834000002,
- 0.13604959434000002,
- 0.14375962434,
- 0.12991076934,
- 0.13063876334000002,
- 0.12568143134,
- 0.12641532034000003,
- 0.12511058934000002,
- 0.12653845534000002,
- 0.12651077634000002,
- 0.12668432834,
- 0.12760148634000001,
- 0.12689729634000002,
- 0.12553759234,
- 0.12653033234000002,
- 0.12632168734000002,
- 0.12785821434000003,
- 0.12731149434000003,
- 0.12891333234000002,
- 0.12743041234000002,
- 0.12680749034000002,
- 0.12473336634,
- 0.12483021834000001,
- 0.13007386834,
- 0.12909832034000002,
- 0.12901733434,
- 0.12940269134000001,
- 0.12715191034,
- 0.12851855334,
- 0.12762605134000002,
- 0.13077005034000003,
- 0.12568601534,
- 0.12738594234,
- 0.13162889034000003,
- 0.12515017434,
- 0.12653772634000002,
- 0.12557007734,
- 0.12547031334000003,
- 0.12695055034000002,
- 0.12790071834000002,
- 0.12543737734000002,
- 0.12555533434000002,
- 0.12951044434,
- 0.12769166334,
- 0.12759169534,
- 0.12788730234,
- 0.12782468734000002,
- 0.12683145534,
- 0.12511537234,
- 0.12804161634000003,
- 0.12652413134,
- 0.12640579034000002,
- 0.12822792534000002,
- 0.12963931134,
- 0.12901060434000003,
- 0.13053810434000002,
- 0.12599571734,
- 0.12660204534000002,
- 0.12567934634000003,
- 0.12635418034,
- 0.12623169334,
- 0.13373038934,
- 0.12688536134,
- 0.12535313234,
- 0.12599578534000003,
- 0.12655190034000002,
- 0.12980068434000003,
- 0.12456903434000001,
- 0.12730251034,
- 0.12560139734,
- 0.12819560834000002,
- 0.12594822634000002,
- 0.12569246734,
- 0.13085695134000003,
- 0.12570623234,
- 0.12652511234000002,
- 0.12822100734000003,
- 0.12797505034,
- 0.12645839334,
- 0.12647544334000002,
- 0.12547535334
- ]
- }
- ]
-}
diff --git a/benchmark/data/root_object.json b/benchmark/data/root_object.json
deleted file mode 100644
index 51792846..00000000
--- a/benchmark/data/root_object.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 -f benchmark/data.json",
- "mean": 0.006560224705000003,
- "stddev": 0.00023596029237584623,
- "median": 0.006540232895000001,
- "user": 0.0034437749999999987,
- "system": 0.0019316349999999993,
- "min": 0.006116507395000002,
- "max": 0.007203835395000002,
- "times": [
- 0.006567834395000001,
- 0.006594772395000002,
- 0.006734405395,
- 0.006556529395000002,
- 0.006576964395000001,
- 0.006520937395000001,
- 0.006330649395,
- 0.006530635395000001,
- 0.006985826395000002,
- 0.006740282395000002,
- 0.007102037395000002,
- 0.006566879395000001,
- 0.006835496395000001,
- 0.0066586773950000015,
- 0.0063945843950000015,
- 0.006224949395000002,
- 0.006378850395000002,
- 0.006632221395,
- 0.0064963383950000005,
- 0.0062735653950000015,
- 0.006130217395000002,
- 0.006196721395000002,
- 0.006628445395000002,
- 0.006538455395000002,
- 0.006433977395000002,
- 0.006520898395,
- 0.0065377473950000015,
- 0.0063974073950000005,
- 0.006752623395000001,
- 0.006296350395000001,
- 0.006631488395000001,
- 0.006285373395000002,
- 0.006788259395000001,
- 0.007078114395000001,
- 0.0064901753950000005,
- 0.006467350395000002,
- 0.006440913395000001,
- 0.006580683395000002,
- 0.006485750395000002,
- 0.006470874395000001,
- 0.006441796395000001,
- 0.006265928395000002,
- 0.007203835395000002,
- 0.0069039733950000005,
- 0.006491220395,
- 0.006489775395000001,
- 0.0063583893950000005,
- 0.006617871395000001,
- 0.006899467395000001,
- 0.006812983395000001,
- 0.006350959395000001,
- 0.006616313395000002,
- 0.006726820395000001,
- 0.006414514395000001,
- 0.006447211395000001,
- 0.006290211395000002,
- 0.006354181395000002,
- 0.006289226395000002,
- 0.007063264395000001,
- 0.006520625395000002,
- 0.006211097395000002,
- 0.006624722395000001,
- 0.006453677395000001,
- 0.006784115395000002,
- 0.006542010395000001,
- 0.006609316395000002,
- 0.006722085395000001,
- 0.006538371395000001,
- 0.006367511395000001,
- 0.006116507395000002,
- 0.006229110395000001,
- 0.006638824395000001,
- 0.006497498395000001,
- 0.006404678395000002,
- 0.006885262395000001,
- 0.006571402395000002,
- 0.0065670713950000004,
- 0.007016068395000002,
- 0.006807108395000001,
- 0.006472902395000001,
- 0.006336071395000002,
- 0.006645642395000002,
- 0.006705204395000001,
- 0.0066509223950000015,
- 0.006241939395000002,
- 0.006874149395000001,
- 0.0066784303950000005,
- 0.006571269395000001,
- 0.006628870395000002,
- 0.006159877395000001,
- 0.006377138395000001,
- 0.006182463395000001,
- 0.007065109395000001,
- 0.006392855395000001,
- 0.006639454395000001,
- 0.006266390395000002,
- 0.0066228413950000006,
- 0.006899119395000002,
- 0.006809273395000001,
- 0.006803273395000002
- ]
- },
- {
- "command": "dasel -f benchmark/data.json",
- "mean": 0.008721853545000002,
- "stddev": 0.0005206001180551711,
- "median": 0.008610990895,
- "user": 0.004674445,
- "system": 0.0027560949999999996,
- "min": 0.008019583395000002,
- "max": 0.010257668395,
- "times": [
- 0.008834420395,
- 0.008388022395000002,
- 0.010141051395000001,
- 0.008525065395000002,
- 0.008616365395,
- 0.008210266395,
- 0.008278044395000001,
- 0.008385545395000001,
- 0.008949411395,
- 0.008274936395000002,
- 0.008245365395,
- 0.008228149395000002,
- 0.008847754395,
- 0.008805193395,
- 0.008403587395000002,
- 0.010132482395000001,
- 0.009021594395,
- 0.008189383395000001,
- 0.010038834395000001,
- 0.009145862395,
- 0.008870982395000001,
- 0.008397042395000002,
- 0.009557629395000001,
- 0.009349802395,
- 0.008978578395,
- 0.008342795395000002,
- 0.008244044395,
- 0.008351229395000001,
- 0.008872860395000001,
- 0.008325318395000001,
- 0.008767890395000002,
- 0.008764965395000001,
- 0.008931943395000001,
- 0.008482727395000002,
- 0.008161548395,
- 0.008311093395000002,
- 0.008711631395,
- 0.008402891395,
- 0.008166006395000001,
- 0.008605616395000001,
- 0.008412710395000002,
- 0.008401700395000001,
- 0.008070974395000001,
- 0.008465424395,
- 0.008588913395000002,
- 0.008737300395000001,
- 0.008284441395000001,
- 0.009965046395000001,
- 0.008556311395000002,
- 0.009200428395000002,
- 0.008620994395000001,
- 0.008780138395000001,
- 0.008413018395000001,
- 0.008839649395000001,
- 0.008325368395000001,
- 0.009079632395000002,
- 0.009514870395000001,
- 0.008480242395,
- 0.008019583395000002,
- 0.008322638395000002,
- 0.008702758395000001,
- 0.008996964395000001,
- 0.008519861395000002,
- 0.008650518395,
- 0.008706880395000002,
- 0.008775020395000001,
- 0.008179697395000001,
- 0.008794986395000001,
- 0.008321829395,
- 0.009842835395000001,
- 0.008152023395000001,
- 0.008360102395000001,
- 0.008749333395000001,
- 0.009138742395000002,
- 0.008219834395000002,
- 0.009502691395000002,
- 0.008683299395000002,
- 0.010257668395,
- 0.008203278395000001,
- 0.009986183395,
- 0.008509681395000001,
- 0.009044321395,
- 0.008361539395000001,
- 0.008857812395000001,
- 0.008535987395000002,
- 0.009176784395000002,
- 0.008320130395000002,
- 0.010073778395000002,
- 0.008711177395000002,
- 0.008778289395000001,
- 0.008394469395000001,
- 0.008546210395000002,
- 0.008297527395,
- 0.008947252395,
- 0.008129016395,
- 0.008299666395,
- 0.008681595395000002,
- 0.009252908395000002,
- 0.008271906395,
- 0.008913471395
- ]
- },
- {
- "command": "jq '.' benchmark/data.json",
- "mean": 0.028097767815000004,
- "stddev": 0.0007492098365173988,
- "median": 0.027899321895000002,
- "user": 0.023576415000000003,
- "system": 0.0010904649999999999,
- "min": 0.026982224395000004,
- "max": 0.031510457395,
- "times": [
- 0.031510457395,
- 0.028370955395000005,
- 0.027849638395000004,
- 0.027802898395,
- 0.027629859395,
- 0.028092254395000005,
- 0.027679744395000004,
- 0.027428224395000003,
- 0.027940475395,
- 0.027461962395,
- 0.027572239395000003,
- 0.028725923395000002,
- 0.027846106395000004,
- 0.027799792395000005,
- 0.028047964395000003,
- 0.027897221395,
- 0.027865078395000003,
- 0.027859685395000004,
- 0.027876710395,
- 0.028298880395,
- 0.027951727395000004,
- 0.028172074395,
- 0.027616521395000003,
- 0.027849657395,
- 0.028517203395,
- 0.027594388395000002,
- 0.029102616395,
- 0.027960897395,
- 0.027853847395000002,
- 0.027693930395,
- 0.027448496395,
- 0.027872028395,
- 0.028529754395000006,
- 0.027508139395000002,
- 0.027901422395000004,
- 0.027414456395,
- 0.027819005395000002,
- 0.027598881395000004,
- 0.029974354395000003,
- 0.027704042395,
- 0.027495236395,
- 0.027975416395000004,
- 0.029172349395,
- 0.028630498395000006,
- 0.027506106395000004,
- 0.027387203395000004,
- 0.027950992395000002,
- 0.028265581395,
- 0.027910680395,
- 0.028000458395,
- 0.027881816395000005,
- 0.027659263395,
- 0.028334439395000004,
- 0.028853144395000006,
- 0.027948113395000005,
- 0.028215505395000003,
- 0.027185575395,
- 0.027569745395,
- 0.027707447395000005,
- 0.027600329395,
- 0.028135097395,
- 0.028406717395,
- 0.028145914395000003,
- 0.028554875395000002,
- 0.027382387395000003,
- 0.028914260395000006,
- 0.028067894395000004,
- 0.027486960395000003,
- 0.027607274395000003,
- 0.028054896395000004,
- 0.027780814395000002,
- 0.028686025395000005,
- 0.027827651395000003,
- 0.027913641395000004,
- 0.027530665395,
- 0.027876622395000005,
- 0.027968138395000005,
- 0.027856587395000004,
- 0.027644859395000002,
- 0.027685054395000002,
- 0.027595011395000004,
- 0.028630052395000005,
- 0.029345750395000005,
- 0.029136327395000005,
- 0.028887625395,
- 0.031398376395,
- 0.028032640395000006,
- 0.027963829395000004,
- 0.028004414395,
- 0.027939705395000004,
- 0.027589536395000003,
- 0.028681414395000004,
- 0.027627449395000003,
- 0.026982224395000004,
- 0.028161376395000003,
- 0.027592776395,
- 0.027453757395000004,
- 0.030623943395000002,
- 0.028563546395000004,
- 0.029151266395000004
- ]
- },
- {
- "command": "yq --yaml-output '.' benchmark/data.yaml",
- "mean": 0.127936466745,
- "stddev": 0.0030504504074422385,
- "median": 0.12732412939499999,
- "user": 0.10109137500000007,
- "system": 0.022681795000000008,
- "min": 0.12454818439500003,
- "max": 0.151617090395,
- "times": [
- 0.126020138395,
- 0.127092934395,
- 0.125877971395,
- 0.127085930395,
- 0.125176883395,
- 0.127202369395,
- 0.131799086395,
- 0.128546751395,
- 0.128087112395,
- 0.126591394395,
- 0.127841590395,
- 0.125481516395,
- 0.129833084395,
- 0.126245308395,
- 0.125842218395,
- 0.126434407395,
- 0.130430079395,
- 0.127171833395,
- 0.126081132395,
- 0.12815765839499998,
- 0.126159084395,
- 0.127363824395,
- 0.129321385395,
- 0.130149975395,
- 0.126854810395,
- 0.128767807395,
- 0.126770359395,
- 0.151617090395,
- 0.127874442395,
- 0.127236020395,
- 0.129352732395,
- 0.128764454395,
- 0.125533678395,
- 0.126200755395,
- 0.12596059639499999,
- 0.128172710395,
- 0.125902279395,
- 0.125206180395,
- 0.128549377395,
- 0.127280055395,
- 0.128536370395,
- 0.126439517395,
- 0.126663426395,
- 0.128162620395,
- 0.12630688339499999,
- 0.132538141395,
- 0.130066040395,
- 0.129044376395,
- 0.127797294395,
- 0.129187813395,
- 0.129096153395,
- 0.129613741395,
- 0.129271941395,
- 0.126403193395,
- 0.13138635939499999,
- 0.126705557395,
- 0.127695397395,
- 0.133381583395,
- 0.126624526395,
- 0.12454818439500003,
- 0.127930087395,
- 0.127235159395,
- 0.127284434395,
- 0.125313427395,
- 0.130966019395,
- 0.130876592395,
- 0.126365450395,
- 0.128947098395,
- 0.130258513395,
- 0.133456651395,
- 0.125279712395,
- 0.127611447395,
- 0.126431386395,
- 0.125592156395,
- 0.129050438395,
- 0.13162539139499999,
- 0.125723364395,
- 0.127783610395,
- 0.127395588395,
- 0.126037908395,
- 0.125753619395,
- 0.125909852395,
- 0.130244560395,
- 0.125210748395,
- 0.126058809395,
- 0.127099143395,
- 0.128182861395,
- 0.125297908395,
- 0.128088664395,
- 0.125362466395,
- 0.12823981239499999,
- 0.127251429395,
- 0.126552255395,
- 0.125944000395,
- 0.127958247395,
- 0.127579846395,
- 0.129828011395,
- 0.127573313395,
- 0.127948911395,
- 0.126891632395
- ]
- }
- ]
-}
diff --git a/benchmark/data/top_level_property.json b/benchmark/data/top_level_property.json
deleted file mode 100644
index 940f3f2c..00000000
--- a/benchmark/data/top_level_property.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 -f benchmark/data.json 'id'",
- "mean": 0.006550416384999996,
- "stddev": 0.00024601099511433844,
- "median": 0.006507407264999997,
- "user": 0.0034442450000000003,
- "system": 0.0019110250000000007,
- "min": 0.006124006764999997,
- "max": 0.007435303764999996,
- "times": [
- 0.006409424764999998,
- 0.0067714407649999975,
- 0.0067454507649999974,
- 0.0064456847649999975,
- 0.006382688764999997,
- 0.0066826097649999965,
- 0.006436487764999997,
- 0.006437265764999997,
- 0.006535118764999997,
- 0.0066288807649999976,
- 0.0070239787649999975,
- 0.006495777764999997,
- 0.006400754764999997,
- 0.006299325764999997,
- 0.006271667764999997,
- 0.006837474764999997,
- 0.006608922764999997,
- 0.006496156764999997,
- 0.006171421764999997,
- 0.006360695764999997,
- 0.006200042764999997,
- 0.0073152667649999965,
- 0.006571944764999997,
- 0.006483469764999998,
- 0.006305482764999997,
- 0.006493079764999998,
- 0.006372934764999998,
- 0.006313692764999997,
- 0.006124006764999997,
- 0.006342037764999997,
- 0.006847285764999997,
- 0.0067479717649999976,
- 0.006698294764999998,
- 0.0062721417649999976,
- 0.006291056764999997,
- 0.0064289147649999965,
- 0.007022751764999996,
- 0.006424592764999997,
- 0.006377070764999998,
- 0.0065125977649999976,
- 0.006405452764999996,
- 0.006540477764999997,
- 0.006421203764999998,
- 0.006546451764999997,
- 0.006511866764999997,
- 0.006506857764999997,
- 0.0063986647649999975,
- 0.006357210764999998,
- 0.006140095764999997,
- 0.006626021764999997,
- 0.006485051764999998,
- 0.007093897764999997,
- 0.006641092764999997,
- 0.006593332764999998,
- 0.006553825764999997,
- 0.006805259764999997,
- 0.0067520077649999965,
- 0.006749774764999997,
- 0.0062603187649999965,
- 0.006552321764999997,
- 0.007125473764999998,
- 0.006698240764999997,
- 0.006433365764999997,
- 0.006395765764999997,
- 0.006966909764999998,
- 0.0068169367649999965,
- 0.0068186487649999974,
- 0.006284971764999998,
- 0.006410533764999997,
- 0.006626721764999998,
- 0.007015845764999997,
- 0.0068382977649999975,
- 0.006570978764999997,
- 0.006443504764999997,
- 0.006627231764999996,
- 0.006457617764999998,
- 0.0065755757649999975,
- 0.006454909764999997,
- 0.006558081764999998,
- 0.006756486764999996,
- 0.006507956764999998,
- 0.006349025764999997,
- 0.006149813764999998,
- 0.006326886764999998,
- 0.0065465437649999975,
- 0.007435303764999996,
- 0.0066341607649999974,
- 0.006532906764999997,
- 0.006325576764999997,
- 0.006516942764999997,
- 0.006425754764999998,
- 0.006800023764999998,
- 0.006333842764999997,
- 0.006432710764999997,
- 0.006469709764999998,
- 0.006279320764999997,
- 0.006579716764999997,
- 0.006486502764999997,
- 0.006560812764999997,
- 0.006944972764999996
- ]
- },
- {
- "command": "dasel -f benchmark/data.json '.id'",
- "mean": 0.008324318624999995,
- "stddev": 0.0002552376514065979,
- "median": 0.008294593764999997,
- "user": 0.004518525,
- "system": 0.002555445000000001,
- "min": 0.007806666764999998,
- "max": 0.009730712764999998,
- "times": [
- 0.007952981764999997,
- 0.008194517764999997,
- 0.008203830764999998,
- 0.008595876764999998,
- 0.008142087764999997,
- 0.008135427764999998,
- 0.009730712764999998,
- 0.008734463764999997,
- 0.008223304764999996,
- 0.008183735764999997,
- 0.008370059764999997,
- 0.008302982764999998,
- 0.008535462764999998,
- 0.008113701764999997,
- 0.008482074764999997,
- 0.008621676764999997,
- 0.008354809764999997,
- 0.007806666764999998,
- 0.008177029764999998,
- 0.008492614764999996,
- 0.008201580764999997,
- 0.007996902764999998,
- 0.008311124764999997,
- 0.008049495764999998,
- 0.008380895764999997,
- 0.008294717764999997,
- 0.008102381764999998,
- 0.008199158764999997,
- 0.008638351764999998,
- 0.008187583764999997,
- 0.008136487764999997,
- 0.008144617764999997,
- 0.008307908764999997,
- 0.008227715764999997,
- 0.008155664764999997,
- 0.008155552764999997,
- 0.008356408764999997,
- 0.008557816764999997,
- 0.008225594764999997,
- 0.008378913764999997,
- 0.008202654764999998,
- 0.008514292764999997,
- 0.008401451764999996,
- 0.008179008764999997,
- 0.008636295764999998,
- 0.008434292764999997,
- 0.008035087764999997,
- 0.008390621764999997,
- 0.008299827764999997,
- 0.008476040764999998,
- 0.008441905764999997,
- 0.008250349764999998,
- 0.008448662764999997,
- 0.008302803764999997,
- 0.008268075764999997,
- 0.008424627764999998,
- 0.008634410764999998,
- 0.008369244764999997,
- 0.008153790764999997,
- 0.008101582764999997,
- 0.008536264764999997,
- 0.008568117764999997,
- 0.008279750764999998,
- 0.008080794764999998,
- 0.008208901764999997,
- 0.008098129764999996,
- 0.008082657764999997,
- 0.008191190764999997,
- 0.008276742764999998,
- 0.008360571764999998,
- 0.008299517764999997,
- 0.008184424764999997,
- 0.008119439764999997,
- 0.008448582764999997,
- 0.009151519764999998,
- 0.008034101764999997,
- 0.008172693764999997,
- 0.008511641764999998,
- 0.008400721764999997,
- 0.008294469764999998,
- 0.008362365764999997,
- 0.008344852764999997,
- 0.008283781764999998,
- 0.008652207764999998,
- 0.008094122764999998,
- 0.008244314764999996,
- 0.008147705764999997,
- 0.008306131764999997,
- 0.007917241764999998,
- 0.008461015764999998,
- 0.008488163764999997,
- 0.009008179764999997,
- 0.008530072764999997,
- 0.008096654764999997,
- 0.008147692764999997,
- 0.008445677764999998,
- 0.008381138764999997,
- 0.008254616764999997,
- 0.008514989764999998,
- 0.008122774764999997
- ]
- },
- {
- "command": "jq '.id' benchmark/data.json",
- "mean": 0.028229297375000004,
- "stddev": 0.0008569398182191514,
- "median": 0.027930878764999997,
- "user": 0.023590864999999992,
- "system": 0.0011729950000000013,
- "min": 0.027056496765,
- "max": 0.031523936764999996,
- "times": [
- 0.028150794764999994,
- 0.027760064764999995,
- 0.027836681764999996,
- 0.027894943765,
- 0.027757178764999994,
- 0.030488815764999996,
- 0.027491311765,
- 0.027809394764999995,
- 0.027333003764999995,
- 0.028818708765,
- 0.029067745765,
- 0.027773127764999996,
- 0.027503405764999996,
- 0.028013080764999997,
- 0.027294267764999998,
- 0.027647466764999996,
- 0.028569303764999998,
- 0.027847931764999993,
- 0.028149714765,
- 0.027864605764999995,
- 0.027523819765,
- 0.031461682765,
- 0.027526306765,
- 0.027599963765,
- 0.027600968764999997,
- 0.027762554764999996,
- 0.028441223764999995,
- 0.028300975765,
- 0.027928310765,
- 0.028279630764999994,
- 0.027056496765,
- 0.028080635764999994,
- 0.027795632764999995,
- 0.027421897765,
- 0.027929237764999996,
- 0.027930041764999997,
- 0.027264766764999997,
- 0.027892522764999997,
- 0.027643479765,
- 0.028433959764999996,
- 0.027405744764999997,
- 0.027458264764999997,
- 0.027707037764999998,
- 0.027917712764999998,
- 0.027500607764999997,
- 0.029067875764999994,
- 0.027805670765,
- 0.027691401765,
- 0.030940199764999995,
- 0.028235278764999996,
- 0.027931715764999997,
- 0.027655886765,
- 0.027706172764999995,
- 0.028233705765,
- 0.027679379764999998,
- 0.027852460764999996,
- 0.028040724764999997,
- 0.027759307764999998,
- 0.028453048764999996,
- 0.028588329764999997,
- 0.027728493764999994,
- 0.028764716764999997,
- 0.027477060765,
- 0.028018997764999998,
- 0.031523936764999996,
- 0.028107145764999994,
- 0.029134737764999995,
- 0.029880420765,
- 0.029364351764999998,
- 0.029320509764999995,
- 0.029779757764999998,
- 0.029933042764999994,
- 0.027654918765,
- 0.027763894764999998,
- 0.027450659764999996,
- 0.028449209764999997,
- 0.027689131765,
- 0.027878022764999996,
- 0.028036176764999997,
- 0.027965869764999998,
- 0.027947193764999996,
- 0.029738297764999998,
- 0.029291946765,
- 0.028775971764999998,
- 0.029161355764999994,
- 0.028420651765,
- 0.028610678764999994,
- 0.029122159764999996,
- 0.028301313764999995,
- 0.028411058764999997,
- 0.029083622764999997,
- 0.028200845764999996,
- 0.028657506765,
- 0.027994793764999998,
- 0.028099175764999998,
- 0.027863432764999994,
- 0.028044536764999996,
- 0.027571356765,
- 0.027606647765,
- 0.027529947765
- ]
- },
- {
- "command": "yq --yaml-output '.id' benchmark/data.yaml",
- "mean": 0.12835159792500003,
- "stddev": 0.010089130332329561,
- "median": 0.126150159265,
- "user": 0.10081843500000001,
- "system": 0.022555625,
- "min": 0.124434808765,
- "max": 0.21169749476500002,
- "times": [
- 0.12693241376500003,
- 0.12598398276500003,
- 0.21169749476500002,
- 0.17417872276500002,
- 0.127540026765,
- 0.125897863765,
- 0.12503282976500002,
- 0.126176339765,
- 0.126536660765,
- 0.12465192176499999,
- 0.124897110765,
- 0.12531811876500001,
- 0.12566856176500002,
- 0.12634932576500002,
- 0.124660133765,
- 0.12709039876500003,
- 0.125437084765,
- 0.128998907765,
- 0.12549196076500002,
- 0.12595978476500003,
- 0.12588422776500002,
- 0.125576465765,
- 0.12951706276500002,
- 0.12528365876500003,
- 0.125631733765,
- 0.126737662765,
- 0.128495776765,
- 0.127303625765,
- 0.12698701176500002,
- 0.12559793576500003,
- 0.12674259776500002,
- 0.12666975976500003,
- 0.12512005876500001,
- 0.12537899576500003,
- 0.126250524765,
- 0.124855139765,
- 0.125111263765,
- 0.127306627765,
- 0.12721274176500003,
- 0.125394333765,
- 0.12569692376500002,
- 0.12614967676500002,
- 0.12542189076500002,
- 0.124653529765,
- 0.12601579276500002,
- 0.12685919576500002,
- 0.125027263765,
- 0.126288329765,
- 0.12779064276500002,
- 0.12523727776500002,
- 0.12568267676500003,
- 0.12788820176500001,
- 0.127346185765,
- 0.12615064176500002,
- 0.12562583876500003,
- 0.124434808765,
- 0.12485411476500001,
- 0.12869135276500002,
- 0.126116951765,
- 0.12596006476500002,
- 0.124653065765,
- 0.12730440676500002,
- 0.125092436765,
- 0.131659413765,
- 0.12520082176500003,
- 0.12558192376500002,
- 0.127521421765,
- 0.126022836765,
- 0.126022785765,
- 0.132629669765,
- 0.12944566976500002,
- 0.13018733076500003,
- 0.12955609176500002,
- 0.13097883076500003,
- 0.129508235765,
- 0.13060793676500002,
- 0.130792193765,
- 0.12969651676500002,
- 0.129121444765,
- 0.12936682276500003,
- 0.12605445176500002,
- 0.125716564765,
- 0.12550641776500002,
- 0.12513027776500002,
- 0.127033002765,
- 0.125618012765,
- 0.128243197765,
- 0.15014448976500003,
- 0.13062317276500002,
- 0.12600829276500003,
- 0.12629204276500003,
- 0.13053616076500002,
- 0.12630749976500003,
- 0.12558466976500002,
- 0.128302790765,
- 0.12897018076500003,
- 0.125859204765,
- 0.12597339276500003,
- 0.12688513176500002,
- 0.12597277076500002
- ]
- }
- ]
-}
diff --git a/benchmark/data/update_string.json b/benchmark/data/update_string.json
deleted file mode 100644
index 57af0cba..00000000
--- a/benchmark/data/update_string.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "results": [
- {
- "command": "daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]'",
- "mean": 0.006574598319999999,
- "stddev": 0.0002567829726151101,
- "median": 0.006507481229999999,
- "user": 0.003468090000000002,
- "system": 0.001919109999999999,
- "min": 0.0060936412299999985,
- "max": 0.0073594002299999985,
- "times": [
- 0.006508482229999999,
- 0.00667546223,
- 0.0070476082299999986,
- 0.00658238823,
- 0.006416818229999999,
- 0.007076544229999999,
- 0.00644592823,
- 0.006498118229999999,
- 0.006772679229999998,
- 0.00653950423,
- 0.006447516229999999,
- 0.006428137229999999,
- 0.0065064802299999985,
- 0.00630173723,
- 0.006444141229999999,
- 0.00671026223,
- 0.006686136229999999,
- 0.006752340229999999,
- 0.006709370229999999,
- 0.006816884229999999,
- 0.007300495229999999,
- 0.006614820229999999,
- 0.00671437823,
- 0.006477968229999999,
- 0.006465355229999999,
- 0.00670829823,
- 0.006676275229999999,
- 0.006331348229999999,
- 0.00623990923,
- 0.006823571229999999,
- 0.006398838229999999,
- 0.006514375229999999,
- 0.006355667229999999,
- 0.006181164229999999,
- 0.00663012323,
- 0.0072106312299999985,
- 0.00670738123,
- 0.006669443229999999,
- 0.006284765229999999,
- 0.00632453023,
- 0.006371354229999999,
- 0.00629729823,
- 0.0063768672299999996,
- 0.006166249229999999,
- 0.0064860632299999985,
- 0.0066769792299999995,
- 0.00648823623,
- 0.006977415229999999,
- 0.006338235229999999,
- 0.00625367223,
- 0.006498987229999999,
- 0.00696681423,
- 0.00654343223,
- 0.006321695229999999,
- 0.00647472323,
- 0.006673124229999999,
- 0.00668090423,
- 0.00643350023,
- 0.00654756223,
- 0.00649328823,
- 0.0063800132299999986,
- 0.006323642229999999,
- 0.006368507229999999,
- 0.00639039923,
- 0.006304803229999999,
- 0.007064060229999999,
- 0.0070125022299999985,
- 0.006231804229999999,
- 0.00631734623,
- 0.00644078723,
- 0.0062989022299999985,
- 0.006499539229999999,
- 0.006852715229999999,
- 0.0064666942299999985,
- 0.007033320229999999,
- 0.0073594002299999985,
- 0.006565490229999998,
- 0.006794377229999999,
- 0.00627824223,
- 0.0066632262299999995,
- 0.006373599229999999,
- 0.00646909423,
- 0.006271544229999999,
- 0.0064087832299999994,
- 0.00661281123,
- 0.006790426229999999,
- 0.006676818229999999,
- 0.006312856229999999,
- 0.006454350229999999,
- 0.006629442229999999,
- 0.006737081229999999,
- 0.00708443923,
- 0.006822252229999999,
- 0.0064669282299999985,
- 0.006879202229999999,
- 0.006686017229999999,
- 0.00664213923,
- 0.0060936412299999985,
- 0.006579762229999999,
- 0.006760592229999999
- ]
- },
- {
- "command": "dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue",
- "mean": 0.00951898981,
- "stddev": 0.0017225102963251645,
- "median": 0.00854833073,
- "user": 0.004997020000000001,
- "system": 0.003031049999999998,
- "min": 0.008015123229999999,
- "max": 0.01287717023,
- "times": [
- 0.00821318523,
- 0.00894063423,
- 0.00876873223,
- 0.00888657123,
- 0.00911273723,
- 0.01125576623,
- 0.012431395230000001,
- 0.012192480229999999,
- 0.012503209230000001,
- 0.012336281230000001,
- 0.011628729229999999,
- 0.012152274230000001,
- 0.01231120923,
- 0.012016926229999999,
- 0.01242725123,
- 0.012324456230000001,
- 0.01206357623,
- 0.012365973229999999,
- 0.012784075229999999,
- 0.01270946423,
- 0.01260855023,
- 0.012829796230000001,
- 0.01251797723,
- 0.01181978323,
- 0.01267650223,
- 0.012214772229999999,
- 0.01200814723,
- 0.01256976623,
- 0.01255546323,
- 0.01190315323,
- 0.01287717023,
- 0.01136526123,
- 0.008957634229999999,
- 0.00929474223,
- 0.00950400023,
- 0.008707574229999999,
- 0.008323313229999999,
- 0.008269586229999999,
- 0.008528736229999999,
- 0.008256986229999999,
- 0.00811713523,
- 0.008334235229999999,
- 0.00874261623,
- 0.00932214923,
- 0.00846270823,
- 0.008244466229999999,
- 0.00838076623,
- 0.008200653229999999,
- 0.008305744229999999,
- 0.00824571023,
- 0.00907160523,
- 0.008437339229999999,
- 0.00818395123,
- 0.008185578229999999,
- 0.00841649823,
- 0.00845421023,
- 0.008515226229999999,
- 0.00844964423,
- 0.008417066229999999,
- 0.008494160229999999,
- 0.008153554229999999,
- 0.00814737923,
- 0.008511383229999999,
- 0.008565272229999999,
- 0.00967279823,
- 0.008341950229999999,
- 0.008015123229999999,
- 0.00855768023,
- 0.00864674023,
- 0.00842890423,
- 0.008480120229999999,
- 0.008282252229999999,
- 0.00830620423,
- 0.008503487229999999,
- 0.00868239123,
- 0.008546387229999999,
- 0.008408158229999999,
- 0.00832261923,
- 0.00806083323,
- 0.00821423223,
- 0.00854229323,
- 0.00856298623,
- 0.008247696229999999,
- 0.00832277523,
- 0.00861195123,
- 0.008564946229999999,
- 0.008315726229999999,
- 0.008485519229999999,
- 0.008498016229999999,
- 0.00855027423,
- 0.008265312229999999,
- 0.00804678123,
- 0.008601419229999999,
- 0.008550365229999999,
- 0.00833349823,
- 0.00811315023,
- 0.00854384823,
- 0.00910500823,
- 0.008357236229999999,
- 0.008237400229999999
- ]
- },
- {
- "command": "jq '.favouriteColours[0] = \"blue\"' benchmark/data.json",
- "mean": 0.02846197351,
- "stddev": 0.0012764748490390692,
- "median": 0.027919681230000003,
- "user": 0.023889290000000004,
- "system": 0.0011117999999999998,
- "min": 0.027330357229999998,
- "max": 0.03306570423,
- "times": [
- 0.027894430230000003,
- 0.027746254230000003,
- 0.027955349230000003,
- 0.028067233230000002,
- 0.028926768230000004,
- 0.02887793223,
- 0.03134301123,
- 0.02824778823,
- 0.028242973230000003,
- 0.027930654230000004,
- 0.027969887230000004,
- 0.02898230023,
- 0.027487028230000002,
- 0.02774380523,
- 0.031752768230000006,
- 0.02846507223,
- 0.02755052223,
- 0.027330357229999998,
- 0.028078946230000006,
- 0.027852178230000003,
- 0.02898427723,
- 0.028402340230000003,
- 0.02997495923,
- 0.02773111123,
- 0.02789157723,
- 0.02789533423,
- 0.028675530230000003,
- 0.027815301230000006,
- 0.03027325023,
- 0.028190308230000002,
- 0.02808739123,
- 0.028776548230000004,
- 0.027678795229999997,
- 0.031579968230000005,
- 0.030911136230000004,
- 0.027804677230000005,
- 0.028202950230000003,
- 0.02764073023,
- 0.027847816230000003,
- 0.028717526230000005,
- 0.03152657423,
- 0.02784010723,
- 0.028108562230000002,
- 0.027634619229999997,
- 0.027614873230000003,
- 0.027599764230000004,
- 0.027848408230000003,
- 0.02843871823,
- 0.03167757123,
- 0.02759215023,
- 0.027891683230000006,
- 0.02759530023,
- 0.028425594230000004,
- 0.027640023229999998,
- 0.02843564323,
- 0.027787908230000005,
- 0.027836637230000003,
- 0.028619985230000006,
- 0.02794124723,
- 0.03306570423,
- 0.02849251623,
- 0.02789543823,
- 0.029186881230000003,
- 0.027905235230000006,
- 0.027742234230000003,
- 0.027731174230000002,
- 0.02761589223,
- 0.027626630229999997,
- 0.02758218323,
- 0.027738300230000006,
- 0.03240315723,
- 0.03204120323,
- 0.02842035423,
- 0.02798019223,
- 0.02840546823,
- 0.030319187230000003,
- 0.02788768723,
- 0.02756192223,
- 0.028613107230000004,
- 0.02767977723,
- 0.028140408230000004,
- 0.027888797230000005,
- 0.027695763230000003,
- 0.027959094230000002,
- 0.02781614023,
- 0.027399465230000003,
- 0.028712661230000006,
- 0.027803387230000004,
- 0.027609352230000002,
- 0.03216698623,
- 0.02948010223,
- 0.027926486230000006,
- 0.027514092230000002,
- 0.02826437123,
- 0.02783546923,
- 0.02791287623,
- 0.027692035230000002,
- 0.027563090230000004,
- 0.027458956229999998,
- 0.02788141223
- ]
- },
- {
- "command": "yq --yaml-output '.favouriteColours[0] = \"blue\"' benchmark/data.yaml",
- "mean": 0.12731649453000007,
- "stddev": 0.0026900046135057403,
- "median": 0.12662870373000001,
- "user": 0.10077686,
- "system": 0.02249842,
- "min": 0.12496164923000001,
- "max": 0.14938530723000001,
- "times": [
- 0.12625821323000003,
- 0.12616111523,
- 0.12510531923,
- 0.12701650523000002,
- 0.12608100623000001,
- 0.12591849123,
- 0.12503278123000003,
- 0.12717050623,
- 0.12600768123,
- 0.12665182323000002,
- 0.12620209523,
- 0.12649509923000002,
- 0.12616235623000002,
- 0.12496164923000001,
- 0.12947022323000001,
- 0.12656453523000002,
- 0.12639286023000001,
- 0.12619358323000002,
- 0.12659386323000002,
- 0.12712297923000002,
- 0.12624938623,
- 0.13030876723,
- 0.13051090623000003,
- 0.12534885723000003,
- 0.12559348023000003,
- 0.12661680123000002,
- 0.12593376323000002,
- 0.13015959623,
- 0.12880130123000003,
- 0.12962229223000002,
- 0.12964488423,
- 0.12600318323,
- 0.12605403323000003,
- 0.12840993523000002,
- 0.12895786023000003,
- 0.12785699423000002,
- 0.12964867423,
- 0.14938530723000001,
- 0.12571257923,
- 0.12864212123000002,
- 0.12620285023000002,
- 0.12752929623,
- 0.12694289923000002,
- 0.12698428723000002,
- 0.12511889823000003,
- 0.12618057123,
- 0.12876998023000003,
- 0.12952903623,
- 0.13148400723,
- 0.12707122923000003,
- 0.12702110423000001,
- 0.12565490323,
- 0.12687027923000002,
- 0.12593925023000002,
- 0.12561652723000002,
- 0.12744777623,
- 0.12642652323,
- 0.12666415923000002,
- 0.12550447523000002,
- 0.12541628423,
- 0.12748333623000002,
- 0.12669978223,
- 0.12758566723,
- 0.12929110823,
- 0.12837133323000002,
- 0.12598774223,
- 0.12829949723,
- 0.12757209623000001,
- 0.12573498523,
- 0.12750033723,
- 0.12554053823000003,
- 0.12603109123,
- 0.12845883323,
- 0.12638048823,
- 0.12605912923,
- 0.12524437823,
- 0.12578552023,
- 0.13052670723,
- 0.12793777023000003,
- 0.12600084823000002,
- 0.12640200023,
- 0.12654134723000002,
- 0.12743096223000003,
- 0.12660315223000002,
- 0.12597433923,
- 0.13026696923000003,
- 0.12682443123,
- 0.12956876923000002,
- 0.12552745123,
- 0.12642705823000003,
- 0.12594537023000002,
- 0.12513864523,
- 0.12852205523000002,
- 0.12745734423000002,
- 0.12664060623,
- 0.12664932123000003,
- 0.12845450223000002,
- 0.12999480023,
- 0.12660916823,
- 0.12678222223000002
- ]
- }
- ]
-}
diff --git a/benchmark/diagrams/append_array_of_strings.jpg b/benchmark/diagrams/append_array_of_strings.jpg
deleted file mode 100644
index 3fe8ed0c..00000000
Binary files a/benchmark/diagrams/append_array_of_strings.jpg and /dev/null differ
diff --git a/benchmark/diagrams/array_index.jpg b/benchmark/diagrams/array_index.jpg
deleted file mode 100644
index 1337523c..00000000
Binary files a/benchmark/diagrams/array_index.jpg and /dev/null differ
diff --git a/benchmark/diagrams/delete_property.jpg b/benchmark/diagrams/delete_property.jpg
deleted file mode 100644
index 78bd4b27..00000000
Binary files a/benchmark/diagrams/delete_property.jpg and /dev/null differ
diff --git a/benchmark/diagrams/list_array_keys.jpg b/benchmark/diagrams/list_array_keys.jpg
deleted file mode 100644
index 2e1cef60..00000000
Binary files a/benchmark/diagrams/list_array_keys.jpg and /dev/null differ
diff --git a/benchmark/diagrams/nested_property.jpg b/benchmark/diagrams/nested_property.jpg
deleted file mode 100644
index bfea3641..00000000
Binary files a/benchmark/diagrams/nested_property.jpg and /dev/null differ
diff --git a/benchmark/diagrams/overwrite_object.jpg b/benchmark/diagrams/overwrite_object.jpg
deleted file mode 100644
index 6906e055..00000000
Binary files a/benchmark/diagrams/overwrite_object.jpg and /dev/null differ
diff --git a/benchmark/diagrams/root_object.jpg b/benchmark/diagrams/root_object.jpg
deleted file mode 100644
index 6118771d..00000000
Binary files a/benchmark/diagrams/root_object.jpg and /dev/null differ
diff --git a/benchmark/diagrams/top_level_property.jpg b/benchmark/diagrams/top_level_property.jpg
deleted file mode 100644
index b464bbe2..00000000
Binary files a/benchmark/diagrams/top_level_property.jpg and /dev/null differ
diff --git a/benchmark/diagrams/update_string.jpg b/benchmark/diagrams/update_string.jpg
deleted file mode 100644
index 12468ac8..00000000
Binary files a/benchmark/diagrams/update_string.jpg and /dev/null differ
diff --git a/benchmark/partials/bottom.md b/benchmark/partials/bottom.md
deleted file mode 100644
index e69de29b..00000000
diff --git a/benchmark/partials/top.md b/benchmark/partials/top.md
deleted file mode 100644
index 8a3d86fb..00000000
--- a/benchmark/partials/top.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Benchmarks
-
-These benchmarks are auto generated using `./benchmark/run.sh`.
-
-```
-brew install hyperfine
-pip install matplotlib
-./benchmark/run.sh
-```
-
-I have put together what I believe to be equivalent commands in dasel/jq/yq.
-
-If you have any feedback or wish to add new benchmarks please submit a PR.
diff --git a/benchmark/plot_barchart.py b/benchmark/plot_barchart.py
deleted file mode 100644
index e5b83067..00000000
--- a/benchmark/plot_barchart.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python
-
-import argparse
-import json
-import matplotlib.pyplot as plt
-
-parser = argparse.ArgumentParser(description=__doc__)
-parser.add_argument("file", help="JSON file with benchmark results")
-parser.add_argument("--title", help="Plot title")
-parser.add_argument("--out", help="Outfile file")
-args = parser.parse_args()
-
-with open(args.file) as f:
- results = json.load(f)["results"]
-
-x = ["daselv2", "dasel", "jq", "yq"]
-x_pos = [i for i, _ in enumerate(x)]
-
-# mean is in seconds. convert to ms.
-mean_times = [b["mean"] * 1000 for b in results]
-
-plt.bar(x_pos, mean_times, color='green', align='center')
-
-plt.ylabel("Execution Time (ms)")
-if args.title is not None:
- plt.title(args.title)
-
-plt.xticks(x_pos, x, horizontalalignment='center')
-
-plt.savefig(args.out, dpi=None, facecolor='w', edgecolor='w',
- orientation='portrait', format=None,
- transparent=False, bbox_inches=None, pad_inches=0.1)
diff --git a/benchmark/run.sh b/benchmark/run.sh
deleted file mode 100755
index e6cb3683..00000000
--- a/benchmark/run.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-outputFile="benchmark/README.md"
-mdOutputFile="benchmark/tmp_results.md"
-
-function run_file() {
- counter=0
- echo "## ${1}" >> "${outputFile}"
-
- name=""
- key=""
- daselV2Cmd=""
- daselCmd=""
- jqCmd=""
- yqCmd=""
-
- while IFS= read -r line
- do
- if [ "$line" == "END" ]
- then
- jsonFile="benchmark/data/${key}.json"
- imagePath="benchmark/diagrams/${key}.jpg"
- readmeImagePath="diagrams/${key}.jpg"
-
- hyperfine --warmup 10 --runs 100 --export-json="${jsonFile}" --export-markdown="${mdOutputFile}" "${daselV2Cmd}" "${daselCmd}" "${jqCmd}" "${yqCmd}"
- python benchmark/plot_barchart.py "${jsonFile}" --title "${name}" --out "${imagePath}"
-
- echo "\n### ${name}\n" >> "${outputFile}"
- echo "
\n" >> "${outputFile}"
- cat "${mdOutputFile}" >> "${outputFile}"
-
- rm "${mdOutputFile}"
-
- elif [ "$line" == "START" ]
- then
- counter=0
- else
- counter=$(($counter+1))
- case $counter in
- 1) name=$line
- ;;
- 2) key=$line
- ;;
- 3) daselV2Cmd=$line
- ;;
- 4) daselCmd=$line
- ;;
- 5) jqCmd=$line
- ;;
- 6) yqCmd=$line
- ;;
- esac
- fi
- done < $2
-}
-
-rm -rf benchmark/data
-rm -rf benchmark/diagrams
-
-mkdir -p benchmark/data
-mkdir -p benchmark/diagrams
-
-cat benchmark/partials/top.md > "${outputFile}"
-
-run_file "Benchmarks" "benchmark/tests.txt"
-
-cat benchmark/partials/bottom.md >> "${outputFile}"
diff --git a/benchmark/tests.txt b/benchmark/tests.txt
deleted file mode 100644
index 14e3a562..00000000
--- a/benchmark/tests.txt
+++ /dev/null
@@ -1,72 +0,0 @@
-START
-Root Object
-root_object
-daselv2 -f benchmark/data.json
-dasel -f benchmark/data.json
-jq '.' benchmark/data.json
-yq --yaml-output '.' benchmark/data.yaml
-END
-START
-Top level property
-top_level_property
-daselv2 -f benchmark/data.json 'id'
-dasel -f benchmark/data.json '.id'
-jq '.id' benchmark/data.json
-yq --yaml-output '.id' benchmark/data.yaml
-END
-START
-Nested property
-nested_property
-daselv2 -f benchmark/data.json 'user.name.first'
-dasel -f benchmark/data.json '.user.name.first'
-jq '.user.name.first' benchmark/data.json
-yq --yaml-output '.user.name.first' benchmark/data.yaml
-END
-START
-Array index
-array_index
-daselv2 -f benchmark/data.json 'favouriteNumbers.[1]'
-dasel -f benchmark/data.json '.favouriteNumbers.[1]'
-jq '.favouriteNumbers[1]' benchmark/data.json
-yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml
-END
-START
-Append to array of strings
-append_array_of_strings
-daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]'
-dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue
-jq '.favouriteColours += ["blue"]' benchmark/data.json
-yq --yaml-output '.favouriteColours += ["blue"]' benchmark/data.yaml
-END
-START
-Update a string value
-update_string
-daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]'
-dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue
-jq '.favouriteColours[0] = "blue"' benchmark/data.json
-yq --yaml-output '.favouriteColours[0] = "blue"' benchmark/data.yaml
-END
-START
-Overwrite an object
-overwrite_object
-daselv2 put -f benchmark/data.json -o - -t json -v '{"first":"Frank","last":"Jones"}' 'user.name'
-dasel put document -f benchmark/data.json -o - -d json '.user.name' '{"first":"Frank","last":"Jones"}'
-jq '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.json
-yq --yaml-output '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.yaml
-END
-START
-List keys of an array
-list_array_keys
-daselv2 -f benchmark/data.json 'all().key()'
-dasel -f benchmark/data.json -m '.-'
-jq 'keys[]' benchmark/data.json
-yq --yaml-output 'keys[]' benchmark/data.yaml
-END
-START
-Delete property
-delete_property
-daselv2 delete -f benchmark/data.json -o - 'id'
-dasel delete -f benchmark/data.json -o - '.id'
-jq 'del(.id)' benchmark/data.json
-yq --yaml-output 'del(.id)' benchmark/data.yaml
-END
diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go
index 8066eceb..b6151f41 100644
--- a/cmd/dasel/main.go
+++ b/cmd/dasel/main.go
@@ -1,14 +1,23 @@
package main
import (
- "github.com/tomwright/dasel/v2/internal/command"
+ "io"
"os"
+
+ "github.com/tomwright/dasel/v3/internal/cli"
+ _ "github.com/tomwright/dasel/v3/parsing/d"
+ _ "github.com/tomwright/dasel/v3/parsing/hcl"
+ _ "github.com/tomwright/dasel/v3/parsing/json"
+ _ "github.com/tomwright/dasel/v3/parsing/toml"
+ _ "github.com/tomwright/dasel/v3/parsing/xml"
+ _ "github.com/tomwright/dasel/v3/parsing/yaml"
)
func main() {
- cmd := command.NewRootCMD()
- if err := cmd.Execute(); err != nil {
- cmd.PrintErrln("Error:", err.Error())
- os.Exit(1)
+ var stdin io.Reader = os.Stdin
+ fi, err := os.Stdin.Stat()
+ if err != nil || (fi.Mode()&os.ModeNamedPipe == 0) {
+ stdin = nil
}
+ cli.MustRun(stdin, os.Stdout, os.Stderr)
}
diff --git a/context.go b/context.go
deleted file mode 100644
index 80c38020..00000000
--- a/context.go
+++ /dev/null
@@ -1,240 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-// Context has scope over the entire query.
-// Each individual function has its own step within the context.
-// The context holds the entire data structure we're accessing/modifying.
-type Context struct {
- selector string
- selectorResolver SelectorResolver
- steps []*Step
- data Value
- functions *FunctionCollection
- createWhenMissing bool
- metadata map[string]interface{}
-}
-
-func (c *Context) WithMetadata(key string, value interface{}) *Context {
- if c.metadata == nil {
- c.metadata = map[string]interface{}{}
- }
- c.metadata[key] = value
- return c
-}
-
-func (c *Context) Metadata(key string) interface{} {
- if c.metadata == nil {
- return nil
- }
- if val, ok := c.metadata[key]; ok {
- return val
- }
- return nil
-}
-
-func newContextWithFunctions(value interface{}, selector string, functions *FunctionCollection) *Context {
- var v Value
- if val, ok := value.(Value); ok {
- v = val
- } else {
- var reflectVal reflect.Value
- if val, ok := value.(reflect.Value); ok {
- reflectVal = val
- } else {
- reflectVal = reflect.ValueOf(value)
- }
-
- v = Value{
- Value: reflectVal,
- }
- }
-
- v.Value = makeAddressable(v.Value)
-
- // v.SetMapIndex(reflect.ValueOf("users"), v.MapIndex(ValueOf("users")))
- // v.MapIndex("users")
-
- v.setFn = func(value Value) {
- v.Unpack().Set(value.Value)
- }
-
- if v.Metadata("key") == nil {
- v.WithMetadata("key", "root")
- }
-
- return &Context{
- selector: selector,
- data: v,
- steps: []*Step{
- {
- selector: Selector{
- funcName: "root",
- funcArgs: []string{},
- },
- index: 0,
- output: Values{v},
- },
- },
- functions: functions,
- selectorResolver: NewSelectorResolver(selector, functions),
- }
-}
-
-func newSelectContext(value interface{}, selector string) *Context {
- return newContextWithFunctions(value, selector, standardFunctions())
-}
-
-func newPutContext(value interface{}, selector string) *Context {
- return newContextWithFunctions(value, selector, standardFunctions()).
- WithCreateWhenMissing(true)
-}
-
-func newDeleteContext(value interface{}, selector string) *Context {
- return newContextWithFunctions(value, selector, standardFunctions())
-}
-
-func derefValue(v Value) Value {
- res := ValueOf(deref(v.Value))
- res.metadata = v.metadata
- return res
-}
-
-func derefValues(values Values) Values {
- results := make(Values, len(values))
- for k, v := range values {
- results[k] = derefValue(v)
- }
- return results
-}
-
-// Select resolves the given selector and returns the resulting values.
-func Select(root interface{}, selector string) (Values, error) {
- c := newSelectContext(root, selector)
- values, err := c.Run()
- if err != nil {
- return nil, err
- }
- return derefValues(values), nil
-}
-
-// Put resolves the given selector and writes the given value in their place.
-// The root value may be changed in-place. If this is not desired you should copy the input
-// value before passing it to Put.
-func Put(root interface{}, selector string, value interface{}) (Value, error) {
- toSet := ValueOf(value)
- c := newPutContext(root, selector)
- values, err := c.Run()
- if err != nil {
- return Value{}, err
- }
- for _, v := range values {
- v.Set(toSet)
- }
- return c.Data(), nil
-}
-
-// Delete resolves the given selector and deletes any found values.
-// The root value may be changed in-place. If this is not desired you should copy the input
-// value before passing it to Delete.
-func Delete(root interface{}, selector string) (Value, error) {
- c := newDeleteContext(root, selector)
- values, err := c.Run()
- if err != nil {
- return Value{}, err
- }
- for _, v := range values {
- v.Delete()
- }
- return c.Data(), nil
-}
-
-func (c *Context) subSelectContext(value interface{}, selector string) *Context {
- subC := newContextWithFunctions(value, selector, c.functions)
- subC.metadata = c.metadata
- return subC
-}
-
-func (c *Context) subSelect(value interface{}, selector string) (Values, error) {
- return c.subSelectContext(value, selector).Run()
-}
-
-// WithSelector updates c with the given selector.
-func (c *Context) WithSelector(s string) *Context {
- c.selector = s
- c.selectorResolver = NewSelectorResolver(s, c.functions)
- return c
-}
-
-// WithCreateWhenMissing updates c with the given create value.
-// If this value is true, elements (such as properties) will be initialised instead
-// of return not found errors.
-func (c *Context) WithCreateWhenMissing(create bool) *Context {
- c.createWhenMissing = create
- return c
-}
-
-// CreateWhenMissing returns true if the internal createWhenMissing value is true.
-func (c *Context) CreateWhenMissing() bool {
- return c.createWhenMissing
-}
-
-// Data returns the root element of the context.
-func (c *Context) Data() Value {
- return derefValue(c.data)
-}
-
-// Run calls Next repeatedly until no more steps are left.
-// Returns the final Step.
-func (c *Context) Run() (Values, error) {
- var res *Step
- for {
- step, err := c.Next()
- if err != nil {
- return nil, err
- }
- if step == nil {
- break
- }
- res = step
- }
- return res.output, nil
-}
-
-// Next returns the next Step, or nil if we have reached the final Selector.
-func (c *Context) Next() (*Step, error) {
- nextSelector, err := c.selectorResolver.Next()
- if err != nil {
- return nil, fmt.Errorf("could not resolve selector: %w", err)
- }
-
- if nextSelector == nil {
- return nil, nil
- }
-
- nextStep := &Step{
- context: c,
- selector: *nextSelector,
- index: len(c.steps),
- output: nil,
- }
-
- c.steps = append(c.steps, nextStep)
-
- if err := nextStep.execute(); err != nil {
- return nextStep, err
- }
-
- return nextStep, nil
-}
-
-// Step returns the step at the given index.
-func (c *Context) Step(i int) *Step {
- if i < 0 || i > (len(c.steps)-1) {
- return nil
- }
- return c.steps[i]
-}
diff --git a/context_test.go b/context_test.go
deleted file mode 100644
index 215ee637..00000000
--- a/context_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package dasel
-
-import (
- "errors"
- "reflect"
- "testing"
-)
-
-func sameSlice(x, y []interface{}) bool {
- if len(x) != len(y) {
- return false
- }
-
- if reflect.DeepEqual(x, y) {
- return true
- }
-
- // Test for equality ignoring ordering
- diff := make([]interface{}, len(y))
- copy(diff, y)
- for _, xv := range x {
- for di, dv := range diff {
- if reflect.DeepEqual(xv, dv) {
- diff = append(diff[0:di], diff[di+1:]...)
- break
- }
- }
- }
-
- return len(diff) == 0
-}
-
-func selectTest(selector string, original interface{}, exp []interface{}) func(t *testing.T) {
- return func(t *testing.T) {
- c := newSelectContext(original, selector)
-
- values, err := c.Run()
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- got := values.Interfaces()
- if !sameSlice(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- return
- }
- }
-}
-
-func selectTestAssert(selector string, original interface{}, assertFn func(t *testing.T, got []any)) func(t *testing.T) {
- return func(t *testing.T) {
- c := newSelectContext(original, selector)
-
- values, err := c.Run()
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- got := values.Interfaces()
- assertFn(t, got)
- }
-}
-
-func selectTestErr(selector string, original interface{}, expErr error) func(t *testing.T) {
- return func(t *testing.T) {
- c := newSelectContext(original, selector)
-
- _, err := c.Run()
-
- if !errors.Is(err, expErr) {
- t.Errorf("expected error: %v, got %v", expErr, err)
- return
- }
- }
-}
-
-func TestContext_Step(t *testing.T) {
- step1 := &Step{index: 0}
- step2 := &Step{index: 1}
- c := &Context{
- steps: []*Step{
- step1, step2,
- },
- }
- expSteps := map[int]*Step{
- -1: nil,
- 0: step1,
- 1: step2,
- 2: nil,
- }
-
- for index, exp := range expSteps {
- got := c.Step(index)
- if exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- }
-}
-
-func TestContext_WithMetadata(t *testing.T) {
- c := (&Context{}).
- WithMetadata("x", 1).
- WithMetadata("y", 2)
-
- expMetadata := map[string]interface{}{
- "x": 1,
- "y": 2,
- "z": nil,
- }
-
- for index, exp := range expMetadata {
- got := c.Metadata(index)
- if exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- }
-}
diff --git a/dencoding/README.md b/dencoding/README.md
deleted file mode 100644
index 83ff2345..00000000
--- a/dencoding/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# dencoding - Dasel Encoding
-
-This package provides encoding implementations for all supported file formats.
-
-The main difference is that it aims to keep maps ordered, where the default encoders/decoders do not.
-
-## Support formats
-
-### Decoding
-
-Custom decoders are required to ensure that map/object values are decoded into the `Map` type rather than a standard `map[string]any`.
-
-### Encoding
-
-The `Map` type must have the appropriate Marshal func on it to ensure marshalling it in the desired format retains the ordering.
diff --git a/dencoding/json.go b/dencoding/json.go
deleted file mode 100644
index 5a82c3d1..00000000
--- a/dencoding/json.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package dencoding
-
-import "encoding/json"
-
-const (
- jsonOpenObject = json.Delim('{')
- jsonCloseObject = json.Delim('}')
- jsonOpenArray = json.Delim('[')
- jsonCloseArray = json.Delim(']')
-)
-
-// JSONEncoderOption is identifies an option that can be applied to a JSON encoder.
-type JSONEncoderOption interface {
- ApplyEncoder(encoder *JSONEncoder)
-}
-
-// JSONDecoderOption is identifies an option that can be applied to a JSON decoder.
-type JSONDecoderOption interface {
- ApplyDecoder(decoder *JSONDecoder)
-}
diff --git a/dencoding/json_decoder.go b/dencoding/json_decoder.go
deleted file mode 100644
index 26b5f788..00000000
--- a/dencoding/json_decoder.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package dencoding
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "reflect"
- "strings"
-)
-
-// JSONDecoder wraps a standard json encoder to implement custom ordering logic.
-type JSONDecoder struct {
- decoder *json.Decoder
-}
-
-// NewJSONDecoder returns a new dencoding JSONDecoder.
-func NewJSONDecoder(r io.Reader, options ...JSONDecoderOption) *JSONDecoder {
- jsonDecoder := json.NewDecoder(r)
- jsonDecoder.UseNumber()
- decoder := &JSONDecoder{
- decoder: jsonDecoder,
- }
- for _, o := range options {
- o.ApplyDecoder(decoder)
- }
- return decoder
-}
-
-// Decode decodes the next item found in the decoder and writes it to v.
-func (decoder *JSONDecoder) Decode(v any) error {
- rv := reflect.ValueOf(v)
- if rv.Kind() != reflect.Pointer || rv.IsNil() {
- return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v))
- }
-
- rve := rv.Elem()
-
- t, err := decoder.decoder.Token()
- if err != nil {
- return err
- }
-
- switch t {
- case jsonOpenObject:
- object, err := decoder.decodeObject()
- if err != nil {
- return fmt.Errorf("could not decode object: %w", err)
- }
- rve.Set(reflect.ValueOf(object))
- case jsonOpenArray:
- arr, err := decoder.decodeArray()
- if err != nil {
- return fmt.Errorf("could not decode array: %w", err)
- }
- rve.Set(reflect.ValueOf(arr))
- default:
- value, err := decoder.decodeValue(t)
- if err != nil {
- return err
- }
- rve.Set(reflect.ValueOf(value))
- }
-
- return nil
-}
-
-func (decoder *JSONDecoder) decodeObject() (*Map, error) {
- res := NewMap()
-
- var key any = nil
-
- for {
- t, err := decoder.decoder.Token()
- if err != nil {
- // We don't expect an EOF here since we're in the middle of processing an object.
- return res, err
- }
-
- switch t {
- case jsonOpenArray:
- if key == nil {
- return res, fmt.Errorf("unexpected token: %v", t)
- }
- value, err := decoder.decodeArray()
- if err != nil {
- return res, err
- }
- res.Set(key.(string), value)
- key = nil
- case jsonCloseArray:
- return res, fmt.Errorf("unexpected token: %v", t)
- case jsonCloseObject:
- return res, nil
- case jsonOpenObject:
- if key == nil {
- return res, fmt.Errorf("unexpected token: %v", t)
- }
- value, err := decoder.decodeObject()
- if err != nil {
- return res, err
- }
- res.Set(key.(string), value)
- key = nil
- default:
- if key == nil {
- key = t
- } else {
- value, err := decoder.decodeValue(t)
- if err != nil {
- return nil, err
- }
- res.Set(key.(string), value)
- key = nil
- }
- }
- }
-}
-
-func (decoder *JSONDecoder) decodeValue(t json.Token) (any, error) {
- switch tv := t.(type) {
- case json.Number:
- strNum := tv.String()
- if strings.Contains(strNum, ".") {
- floatNum, err := tv.Float64()
- if err == nil {
- return floatNum, nil
- }
- return nil, err
- }
- intNum, err := tv.Int64()
- if err == nil {
- return intNum, nil
- }
-
- return nil, err
- }
- return t, nil
-}
-
-func (decoder *JSONDecoder) decodeArray() ([]any, error) {
- res := make([]any, 0)
- for {
- t, err := decoder.decoder.Token()
- if err != nil {
- // We don't expect an EOF here since we're in the middle of processing an object.
- return res, err
- }
-
- switch t {
- case jsonOpenArray:
- value, err := decoder.decodeArray()
- if err != nil {
- return res, err
- }
- res = append(res, value)
- case jsonCloseArray:
- return res, nil
- case jsonCloseObject:
- return res, fmt.Errorf("unexpected token: %t", t)
- case jsonOpenObject:
- value, err := decoder.decodeObject()
- if err != nil {
- return res, err
- }
- res = append(res, value)
- default:
- value, err := decoder.decodeValue(t)
- if err != nil {
- return nil, err
- }
- res = append(res, value)
- }
- }
-}
diff --git a/dencoding/json_decoder_test.go b/dencoding/json_decoder_test.go
deleted file mode 100644
index 2aca6aca..00000000
--- a/dencoding/json_decoder_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "github.com/tomwright/dasel/v2/dencoding"
- "io"
- "reflect"
- "testing"
-)
-
-func TestJSONDecoder_Decode(t *testing.T) {
- b := []byte(`{"x":1,"a":"hello"}{"x":2,"a":"there"}{"a":"Tom","x":3}`)
- dec := dencoding.NewJSONDecoder(bytes.NewReader(b))
-
- maps := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- maps = append(maps, v)
- }
-
- exp := [][]dencoding.KeyValue{
- {
- {Key: "x", Value: int64(1)},
- {Key: "a", Value: "hello"},
- },
- {
- {Key: "x", Value: int64(2)},
- {Key: "a", Value: "there"},
- },
- {
- {Key: "a", Value: "Tom"},
- {Key: "x", Value: int64(3)},
- },
- }
-
- got := make([][]dencoding.KeyValue, 0)
- for _, v := range maps {
- if m, ok := v.(*dencoding.Map); ok {
- got = append(got, m.KeyValues())
- }
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
-}
diff --git a/dencoding/json_encoder.go b/dencoding/json_encoder.go
deleted file mode 100644
index da060767..00000000
--- a/dencoding/json_encoder.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package dencoding
-
-import (
- "bytes"
- "encoding/json"
- "io"
-)
-
-// lastOptions contains the options that the last JSONEncoder was created with.
-// Find a better way of passing this information into nested MarshalJSON calls.
-var lastOptions []JSONEncoderOption
-
-// JSONEncoder wraps a standard json encoder to implement custom ordering logic.
-type JSONEncoder struct {
- encoder *json.Encoder
-}
-
-// NewJSONEncoder returns a new dencoding JSONEncoder.
-func NewJSONEncoder(w io.Writer, options ...JSONEncoderOption) *JSONEncoder {
- jsonEncoder := json.NewEncoder(w)
- encoder := &JSONEncoder{
- encoder: jsonEncoder,
- }
- for _, o := range options {
- o.ApplyEncoder(encoder)
- }
- lastOptions = options
- return encoder
-}
-
-// Encode encodes the given value and writes the encodes bytes to the stream.
-func (encoder *JSONEncoder) Encode(v any) error {
- // We rely on Map.MarshalJSON to ensure ordering.
- return encoder.encoder.Encode(v)
-}
-
-// Close cleans up the encoder.
-func (encoder *JSONEncoder) Close() error {
- return nil
-}
-
-// JSONEscapeHTML enables or disables html escaping when encoding JSON.
-func JSONEscapeHTML(escape bool) JSONEncoderOption {
- return jsonEncodeHTMLOption{escapeHTML: escape}
-}
-
-type jsonEncodeHTMLOption struct {
- escapeHTML bool
-}
-
-func (option jsonEncodeHTMLOption) ApplyEncoder(encoder *JSONEncoder) {
- encoder.encoder.SetEscapeHTML(option.escapeHTML)
-}
-
-// JSONEncodeIndent sets the indentation when encoding JSON.
-func JSONEncodeIndent(prefix string, indent string) JSONEncoderOption {
- return jsonEncodeIndent{prefix: prefix, indent: indent}
-}
-
-type jsonEncodeIndent struct {
- prefix string
- indent string
-}
-
-func (option jsonEncodeIndent) ApplyEncoder(encoder *JSONEncoder) {
- encoder.encoder.SetIndent(option.prefix, option.indent)
-}
-
-// MarshalJSON JSON encodes the map and returns the bytes.
-// This maintains ordering.
-func (m *Map) MarshalJSON() ([]byte, error) {
-
- buf := new(bytes.Buffer)
- buf.Write([]byte(`{`))
- encoder := NewJSONEncoder(buf, lastOptions...)
- for i, key := range m.keys {
- last := i == len(m.keys)-1
-
- if err := encoder.Encode(key); err != nil {
- return nil, err
- }
- buf.Write([]byte(`:`))
- if err := encoder.Encode(m.data[key]); err != nil {
- return nil, err
- }
- if !last {
- buf.Write([]byte(`,`))
- }
- }
- buf.Write([]byte(`}`))
- return buf.Bytes(), nil
-}
diff --git a/dencoding/json_encoder_test.go b/dencoding/json_encoder_test.go
deleted file mode 100644
index 513e1837..00000000
--- a/dencoding/json_encoder_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "github.com/tomwright/dasel/v2/dencoding"
- "testing"
-)
-
-func TestJSONEncoder_Encode(t *testing.T) {
- orig := dencoding.NewMap().
- Set("c", "x").
- Set("b", "y").
- Set("a", "z")
-
- exp := `{
- "c": "x",
- "b": "y",
- "a": "z"
-}
-`
-
- gotBuffer := new(bytes.Buffer)
-
- encoder := dencoding.NewJSONEncoder(gotBuffer, dencoding.JSONEncodeIndent("", " "))
- if err := encoder.Encode(orig); err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- got := gotBuffer.String()
-
- if exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
-}
diff --git a/dencoding/keyvalue.go b/dencoding/keyvalue.go
deleted file mode 100644
index e3e4f281..00000000
--- a/dencoding/keyvalue.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package dencoding
-
-// KeyValue is a single key value pair from a *Map.
-type KeyValue struct {
- Key string
- Value any
-}
diff --git a/dencoding/toml.go b/dencoding/toml.go
deleted file mode 100644
index 8cc64781..00000000
--- a/dencoding/toml.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package dencoding
-
-// TOMLEncoderOption is identifies an option that can be applied to a TOML encoder.
-type TOMLEncoderOption interface {
- ApplyEncoder(encoder *TOMLEncoder)
-}
-
-// TOMLDecoderOption is identifies an option that can be applied to a TOML decoder.
-type TOMLDecoderOption interface {
- ApplyDecoder(decoder *TOMLDecoder)
-}
diff --git a/dencoding/toml_decoder.go b/dencoding/toml_decoder.go
deleted file mode 100644
index 8066088f..00000000
--- a/dencoding/toml_decoder.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package dencoding
-
-import (
- "github.com/pelletier/go-toml/v2"
- "github.com/pelletier/go-toml/v2/unstable"
- "io"
-)
-
-// TOMLDecoder wraps a standard toml encoder to implement custom ordering logic.
-type TOMLDecoder struct {
- reader io.Reader
- p *unstable.Parser
-}
-
-// NewTOMLDecoder returns a new dencoding TOMLDecoder.
-func NewTOMLDecoder(r io.Reader, options ...TOMLDecoderOption) *TOMLDecoder {
- decoder := &TOMLDecoder{
- reader: r,
- }
- for _, o := range options {
- o.ApplyDecoder(decoder)
- }
- return decoder
-}
-
-// Decode decodes the next item found in the decoder and writes it to v.
-func (decoder *TOMLDecoder) Decode(v any) error {
- data, err := io.ReadAll(decoder.reader)
- if err != nil {
- return err
- }
- if len(data) == 0 {
- return io.EOF
- }
- return toml.Unmarshal(data, v)
-}
diff --git a/dencoding/toml_decoder_test.go b/dencoding/toml_decoder_test.go
deleted file mode 100644
index 6ade2976..00000000
--- a/dencoding/toml_decoder_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "github.com/tomwright/dasel/v2/dencoding"
- "io"
- "reflect"
- "testing"
-)
-
-func TestTOMLDecoder_Decode(t *testing.T) {
-
- t.Run("KeyValue", func(t *testing.T) {
- b := []byte(`x = 1
-a = 'hello'`)
- dec := dencoding.NewTOMLDecoder(bytes.NewReader(b))
-
- maps := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- maps = append(maps, v)
- }
-
- exp := []any{
- map[string]any{
- "x": int64(1),
- "a": "hello",
- },
- }
-
- got := maps
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- t.Run("Table", func(t *testing.T) {
- b := []byte(`
-[user]
-name = "Tom"
-age = 29
-`)
- dec := dencoding.NewTOMLDecoder(bytes.NewReader(b))
-
- got := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- got = append(got, v)
- }
-
- exp := []any{
- map[string]any{
- "user": map[string]any{
- "age": int64(29),
- "name": "Tom",
- },
- },
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-}
diff --git a/dencoding/toml_encoder.go b/dencoding/toml_encoder.go
deleted file mode 100644
index 998d7b17..00000000
--- a/dencoding/toml_encoder.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package dencoding
-
-import (
- "bytes"
- "github.com/pelletier/go-toml/v2"
- "io"
-)
-
-// TOMLEncoder wraps a standard toml encoder to implement custom ordering logic.
-type TOMLEncoder struct {
- encoder *toml.Encoder
- writer io.Writer
- buffer *bytes.Buffer
-}
-
-// NewTOMLEncoder returns a new dencoding TOMLEncoder.
-func NewTOMLEncoder(w io.Writer, options ...TOMLEncoderOption) *TOMLEncoder {
- buffer := new(bytes.Buffer)
- tomlEncoder := toml.NewEncoder(buffer)
- tomlEncoder.SetIndentTables(false)
- encoder := &TOMLEncoder{
- writer: w,
- encoder: tomlEncoder,
- buffer: buffer,
- }
- for _, o := range options {
- o.ApplyEncoder(encoder)
- }
- return encoder
-}
-
-// Encode encodes the given value and writes the encodes bytes to the stream.
-func (encoder *TOMLEncoder) Encode(v any) error {
- // No ordering is done here.
- adjusted := removeDencodingMap(v)
- if err := encoder.encoder.Encode(adjusted); err != nil {
- return err
- }
- data, err := io.ReadAll(encoder.buffer)
- if err != nil {
- return err
- }
- if _, err := encoder.writer.Write(data); err != nil {
- return err
- }
- newline := []byte("\n")
- if !bytes.HasSuffix(data, newline) {
- if _, err := encoder.writer.Write(newline); err != nil {
- return err
- }
- }
- return nil
-}
-
-// Close cleans up the encoder.
-func (encoder *TOMLEncoder) Close() error {
- return nil
-}
-
-func removeDencodingMap(value any) any {
- switch v := value.(type) {
- case []any:
- return removeDencodingMapFromArray(v)
- case map[string]any:
- return removeDencodingMapFromMap(v)
- case *Map:
- return removeDencodingMap(v.data)
- default:
- return v
- }
-}
-
-func removeDencodingMapFromArray(value []any) []any {
- for k, v := range value {
- value[k] = removeDencodingMap(v)
- }
- return value
-}
-
-func removeDencodingMapFromMap(value map[string]any) map[string]any {
- for k, v := range value {
- value[k] = removeDencodingMap(v)
- }
- return value
-}
-
-// TOMLIndentSymbol sets the indentation when encoding TOML.
-func TOMLIndentSymbol(symbol string) TOMLEncoderOption {
- return tomlEncodeSymbol{symbol: symbol}
-}
-
-type tomlEncodeSymbol struct {
- symbol string
-}
-
-func (option tomlEncodeSymbol) ApplyEncoder(encoder *TOMLEncoder) {
- encoder.encoder.SetIndentSymbol(option.symbol)
- encoder.encoder.SetIndentTables(option.symbol != "")
-}
diff --git a/dencoding/toml_encoder_test.go b/dencoding/toml_encoder_test.go
deleted file mode 100644
index eb66f741..00000000
--- a/dencoding/toml_encoder_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "github.com/tomwright/dasel/v2/dencoding"
- "testing"
-)
-
-func TestTOMLEncoder_Encode(t *testing.T) {
- orig := dencoding.NewMap().
- Set("c", "x").
- Set("b", "y").
- Set("a", []any{"a", "c", "b"})
-
- exp := `a = ['a', 'c', 'b']
-b = 'y'
-c = 'x'
-`
-
- gotBuffer := new(bytes.Buffer)
-
- encoder := dencoding.NewTOMLEncoder(gotBuffer, dencoding.TOMLIndentSymbol(" "))
- if err := encoder.Encode(orig); err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- got := gotBuffer.String()
-
- if exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
-}
diff --git a/dencoding/yaml.go b/dencoding/yaml.go
deleted file mode 100644
index cb0d46dc..00000000
--- a/dencoding/yaml.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package dencoding
-
-const (
- yamlTagString = "!!str"
- yamlTagMap = "!!map"
- yamlTagArray = "!!seq"
- yamlTagNull = "!!null"
- yamlTagBinary = "!!binary"
- yamlTagBool = "!!bool"
- yamlTagInt = "!!int"
- yamlTagFloat = "!!float"
- yamlTagTimestamp = "!!timestamp"
- yamlTagMerge = "!!merge"
-)
-
-// YAMLEncoderOption is identifies an option that can be applied to a YAML encoder.
-type YAMLEncoderOption interface {
- ApplyEncoder(encoder *YAMLEncoder)
-}
-
-// YAMLDecoderOption is identifies an option that can be applied to a YAML decoder.
-type YAMLDecoderOption interface {
- ApplyDecoder(decoder *YAMLDecoder)
-}
diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go
deleted file mode 100644
index ce1975f7..00000000
--- a/dencoding/yaml_decoder.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package dencoding
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/util"
- "gopkg.in/yaml.v3"
- "io"
- "reflect"
- "strconv"
- "time"
-)
-
-// YAMLDecoder wraps a standard yaml encoder to implement custom ordering logic.
-type YAMLDecoder struct {
- decoder *yaml.Decoder
-}
-
-// NewYAMLDecoder returns a new dencoding YAMLDecoder.
-func NewYAMLDecoder(r io.Reader, options ...YAMLDecoderOption) *YAMLDecoder {
- yamlDecoder := yaml.NewDecoder(r)
- decoder := &YAMLDecoder{
- decoder: yamlDecoder,
- }
- for _, o := range options {
- o.ApplyDecoder(decoder)
- }
- return decoder
-}
-
-// Decode decodes the next item found in the decoder and writes it to v.
-func (decoder *YAMLDecoder) Decode(v any) error {
- rv := reflect.ValueOf(v)
- if rv.Kind() != reflect.Pointer || rv.IsNil() {
- return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v))
- }
-
- rve := rv.Elem()
-
- node, err := decoder.nextNode()
- if err != nil {
- return err
- }
-
- if node.Kind == yaml.DocumentNode && len(node.Content) == 1 && node.Content[0].ShortTag() == yamlTagNull {
- return io.EOF
- }
-
- val, err := decoder.getNodeValue(node)
- if err != nil {
- return err
- }
-
- rve.Set(reflect.ValueOf(val))
- return nil
-}
-
-func (decoder *YAMLDecoder) getNodeValue(node *yaml.Node) (any, error) {
- switch node.Kind {
- case yaml.DocumentNode:
- return decoder.getNodeValue(node.Content[0])
- case yaml.MappingNode:
- return decoder.getMappingNodeValue(node)
- case yaml.SequenceNode:
- return decoder.getSequenceNodeValue(node)
- case yaml.ScalarNode:
- return decoder.getScalarNodeValue(node)
- case yaml.AliasNode:
- return decoder.getNodeValue(node.Alias)
- default:
- return nil, fmt.Errorf("unhandled node kind: %v", node.Kind)
- }
-}
-
-func (decoder *YAMLDecoder) getMappingNodeValue(node *yaml.Node) (any, error) {
- res := NewMap()
-
- content := make([]*yaml.Node, 0)
- content = append(content, node.Content...)
-
- var keyNode *yaml.Node
- var valueNode *yaml.Node
- for {
- if len(content) == 0 {
- break
- }
-
- keyNode, valueNode, content = content[0], content[1], content[2:]
-
- if keyNode.ShortTag() == yamlTagMerge {
- content = append(valueNode.Alias.Content, content...)
- continue
- }
-
- keyValue, err := decoder.getNodeValue(keyNode)
- if err != nil {
- return nil, err
- }
-
- value, err := decoder.getNodeValue(valueNode)
- if err != nil {
- return nil, err
- }
-
- key := util.ToString(keyValue)
-
- res.Set(key, value)
- }
-
- return res, nil
-}
-
-func (decoder *YAMLDecoder) getSequenceNodeValue(node *yaml.Node) (any, error) {
- res := make([]any, len(node.Content))
- for k, n := range node.Content {
- val, err := decoder.getNodeValue(n)
- if err != nil {
- return nil, err
- }
- res[k] = val
- }
- return res, nil
-}
-
-func (decoder *YAMLDecoder) getScalarNodeValue(node *yaml.Node) (any, error) {
- switch node.ShortTag() {
- case yamlTagNull:
- return nil, nil
- case yamlTagBool:
- return node.Value == "true", nil
- case yamlTagFloat:
- return strconv.ParseFloat(node.Value, 64)
- case yamlTagInt:
- return strconv.ParseInt(node.Value, 0, 64)
- case yamlTagString:
- return node.Value, nil
- case yamlTagTimestamp:
- value, ok := parseTimestamp(node.Value)
- if !ok {
- return value, fmt.Errorf("could not parse timestamp: %v", node.Value)
- }
- return value, nil
- default:
- return nil, fmt.Errorf("unhandled scalar node tag: %v", node.ShortTag())
- }
-}
-
-func (decoder *YAMLDecoder) nextNode() (*yaml.Node, error) {
- var node yaml.Node
- if err := decoder.decoder.Decode(&node); err != nil {
- return nil, err
- }
- return &node, nil
-}
-
-// This is a subset of the formats allowed by the regular expression
-// defined at http://yaml.org/type/timestamp.html.
-var allowedTimestampFormats = []string{
- "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields.
- "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t".
- "2006-1-2 15:4:5.999999999", // space separated with no time zone
- "2006-1-2", // date only
- // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
- // from the set of examples.
-}
-
-// parseTimestamp parses s as a timestamp string and
-// returns the timestamp and reports whether it succeeded.
-// Timestamp formats are defined at http://yaml.org/type/timestamp.html
-// Copied from yaml.v3.
-func parseTimestamp(s string) (time.Time, bool) {
- // TODO write code to check all the formats supported by
- // http://yaml.org/type/timestamp.html instead of using time.Parse.
-
- // Quick check: all date formats start with YYYY-.
- i := 0
- for ; i < len(s); i++ {
- if c := s[i]; c < '0' || c > '9' {
- break
- }
- }
- if i != 4 || i == len(s) || s[i] != '-' {
- return time.Time{}, false
- }
- for _, format := range allowedTimestampFormats {
- if t, err := time.Parse(format, s); err == nil {
- return t, true
- }
- }
- return time.Time{}, false
-}
diff --git a/dencoding/yaml_decoder_test.go b/dencoding/yaml_decoder_test.go
deleted file mode 100644
index 172bc53c..00000000
--- a/dencoding/yaml_decoder_test.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "io"
- "reflect"
- "testing"
-
- "github.com/tomwright/dasel/v2/dencoding"
-)
-
-func TestYAMLDecoder_Decode(t *testing.T) {
-
- t.Run("Basic", func(t *testing.T) {
-
- b := []byte(`
-x: 1
-a: hello
----
-x: 2
-a: there
----
-a: Tom
-x: 3
----`)
- dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
-
- maps := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- maps = append(maps, v)
- }
-
- exp := [][]dencoding.KeyValue{
- {
- {Key: "x", Value: int64(1)},
- {Key: "a", Value: "hello"},
- },
- {
- {Key: "x", Value: int64(2)},
- {Key: "a", Value: "there"},
- },
- {
- {Key: "a", Value: "Tom"},
- {Key: "x", Value: int64(3)},
- },
- }
-
- got := make([][]dencoding.KeyValue, 0)
- for _, v := range maps {
- if m, ok := v.(*dencoding.Map); ok {
- got = append(got, m.KeyValues())
- }
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- // https://github.com/TomWright/dasel/issues/278
- t.Run("Issue278", func(t *testing.T) {
- b := []byte(`
-key1: [value1,value2,value3,value4,value5]
-key2: value6
-`)
- dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
-
- got := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- got = append(got, v)
- }
-
- exp := []any{
- dencoding.NewMap().
- Set("key1", []any{"value1", "value2", "value3", "value4", "value5"}).
- Set("key2", "value6"),
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- t.Run("YamlAliases", func(t *testing.T) {
- b := []byte(`foo: &foofoo
- bar: 1
- baz: &baz "baz"
-spam:
- ham: "eggs"
- bar: 0
- <<: *foofoo
- baz: "bazbaz"
-
-baz: *baz
-`)
-
- dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
-
- got := make([]any, 0)
- for {
- var v any
- if err := dec.Decode(&v); err != nil {
- if err == io.EOF {
- break
- }
- t.Errorf("unexpected error: %v", err)
- return
- }
- got = append(got, v)
- }
-
- exp := dencoding.NewMap().
- Set("foo", dencoding.NewMap().
- Set("bar", int64(1)).
- Set("baz", "baz")).
- Set("spam", dencoding.NewMap().
- Set("ham", "eggs").
- Set("bar", int64(1)).
- Set("baz", "bazbaz")).
- Set("baz", "baz")
-
- if len(got) != 1 {
- t.Errorf("expected result len of %d, got %d", 1, len(got))
- return
- }
-
- gotMap, ok := got[0].(*dencoding.Map)
- if !ok {
- t.Errorf("expected result to be of type %T, got %T", exp, got[0])
- return
- }
-
- if !reflect.DeepEqual(exp, gotMap) {
- t.Errorf("expected %v, got %v", exp, gotMap)
- }
- })
-
-}
diff --git a/dencoding/yaml_encoder.go b/dencoding/yaml_encoder.go
deleted file mode 100644
index cb0d718a..00000000
--- a/dencoding/yaml_encoder.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package dencoding
-
-import (
- "github.com/tomwright/dasel/v2/util"
- "gopkg.in/yaml.v3"
- "io"
- "strconv"
-)
-
-// YAMLEncoder wraps a standard yaml encoder to implement custom ordering logic.
-type YAMLEncoder struct {
- encoder *yaml.Encoder
-}
-
-// NewYAMLEncoder returns a new dencoding YAMLEncoder.
-func NewYAMLEncoder(w io.Writer, options ...YAMLEncoderOption) *YAMLEncoder {
- yamlEncoder := yaml.NewEncoder(w)
- encoder := &YAMLEncoder{
- encoder: yamlEncoder,
- }
- for _, o := range options {
- o.ApplyEncoder(encoder)
- }
- return encoder
-}
-
-// Encode encodes the given value and writes the encodes bytes to the stream.
-func (encoder *YAMLEncoder) Encode(v any) error {
- // We rely on Map.MarshalYAML to ensure ordering.
- return encoder.encoder.Encode(v)
-}
-
-// Close cleans up the encoder.
-func (encoder *YAMLEncoder) Close() error {
- return encoder.encoder.Close()
-}
-
-// MarshalYAML YAML encodes the map and returns the bytes.
-// This maintains ordering.
-func (m *Map) MarshalYAML() (any, error) {
- return yamlOrderedMapToNode(m)
-}
-
-// YAMLEncodeIndent sets the indentation when encoding YAML.
-func YAMLEncodeIndent(spaces int) YAMLEncoderOption {
- return yamlEncodeIndent{spaces: spaces}
-}
-
-type yamlEncodeIndent struct {
- spaces int
-}
-
-func (option yamlEncodeIndent) ApplyEncoder(encoder *YAMLEncoder) {
- encoder.encoder.SetIndent(option.spaces)
-}
-
-func yamlValueToNode(value any) (*yaml.Node, error) {
- switch v := value.(type) {
- case *Map:
- return yamlOrderedMapToNode(v)
- case []any:
- return yamlSliceToNode(v)
- default:
- return yamlScalarToNode(v)
- }
-}
-
-func yamlOrderedMapToNode(value *Map) (*yaml.Node, error) {
- mapNode := &yaml.Node{
- Kind: yaml.MappingNode,
- Style: yaml.TaggedStyle & yaml.DoubleQuotedStyle & yaml.SingleQuotedStyle & yaml.LiteralStyle & yaml.FoldedStyle & yaml.FlowStyle,
- Content: make([]*yaml.Node, 0),
- }
-
- for _, key := range value.keys {
- keyNode, err := yamlValueToNode(key)
- if err != nil {
- return nil, err
- }
- valueNode, err := yamlValueToNode(value.data[key])
- if err != nil {
- return nil, err
- }
- mapNode.Content = append(mapNode.Content, keyNode, valueNode)
- }
-
- return mapNode, nil
-}
-
-func yamlSliceToNode(value []any) (*yaml.Node, error) {
- node := &yaml.Node{
- Kind: yaml.SequenceNode,
- Content: make([]*yaml.Node, len(value)),
- }
-
- for i, v := range value {
- indexNode, err := yamlValueToNode(v)
- if err != nil {
- return nil, err
- }
- node.Content[i] = indexNode
- }
-
- return node, nil
-}
-
-func yamlScalarToNode(value any) (*yaml.Node, error) {
- res := &yaml.Node{
- Kind: yaml.ScalarNode,
- Value: util.ToString(value),
- }
- switch v := value.(type) {
- case string:
- if v == "true" || v == "false" {
- // If the string can be evaluated as a bool, quote it.
- res.Style = yaml.DoubleQuotedStyle
- } else if _, err := strconv.ParseInt(v, 0, 64); err == nil {
- // If the string can be evaluated as a number, quote it.
- res.Style = yaml.DoubleQuotedStyle
- }
- }
- return res, nil
-}
diff --git a/dencoding/yaml_encoder_test.go b/dencoding/yaml_encoder_test.go
deleted file mode 100644
index 66061b0b..00000000
--- a/dencoding/yaml_encoder_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package dencoding_test
-
-import (
- "bytes"
- "github.com/tomwright/dasel/v2/dencoding"
- "testing"
-)
-
-func TestYAMLEncoder_Encode(t *testing.T) {
- orig := dencoding.NewMap().
- Set("c", "x").
- Set("b", "y").
- Set("a", []any{"a", "c", "b"})
-
- exp := `c: x
-b: y
-a:
- - a
- - c
- - b
-`
-
- gotBuffer := new(bytes.Buffer)
-
- encoder := dencoding.NewYAMLEncoder(gotBuffer, dencoding.YAMLEncodeIndent(2))
- if err := encoder.Encode(orig); err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- got := gotBuffer.String()
-
- if exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
-}
diff --git a/error.go b/error.go
deleted file mode 100644
index 84156822..00000000
--- a/error.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package dasel
-
-import (
- "errors"
- "fmt"
- "reflect"
-)
-
-// ErrMissingPreviousNode is returned when findValue doesn't have access to the previous node.
-var ErrMissingPreviousNode = errors.New("missing previous node")
-
-// UnknownComparisonOperatorErr is returned when
-type UnknownComparisonOperatorErr struct {
- Operator string
-}
-
-// Error returns the error message.
-func (e UnknownComparisonOperatorErr) Error() string {
- return fmt.Sprintf("unknown comparison operator: %s", e.Operator)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e UnknownComparisonOperatorErr) Is(err error) bool {
- _, ok := err.(*UnknownComparisonOperatorErr)
- return ok
-}
-
-// InvalidIndexErr is returned when a selector targets an index that does not exist.
-type InvalidIndexErr struct {
- Index string
-}
-
-// Error returns the error message.
-func (e InvalidIndexErr) Error() string {
- return fmt.Sprintf("invalid index: %s", e.Index)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e InvalidIndexErr) Is(err error) bool {
- _, ok := err.(*InvalidIndexErr)
- return ok
-}
-
-// UnsupportedSelector is returned when a specific selector type is used in the wrong context.
-type UnsupportedSelector struct {
- Selector string
-}
-
-// Error returns the error message.
-func (e UnsupportedSelector) Error() string {
- return fmt.Sprintf("selector is not supported here: %s", e.Selector)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e UnsupportedSelector) Is(err error) bool {
- _, ok := err.(*UnsupportedSelector)
- return ok
-}
-
-// ValueNotFound is returned when a selector string cannot be fully resolved.
-type ValueNotFound struct {
- Selector string
- PreviousValue reflect.Value
-}
-
-// Error returns the error message.
-func (e ValueNotFound) Error() string {
- return fmt.Sprintf("no value found for selector: %s: %v", e.Selector, e.PreviousValue)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e ValueNotFound) Is(err error) bool {
- _, ok := err.(*ValueNotFound)
- return ok
-}
-
-// UnexpectedPreviousNilValue is returned when the previous node contains a nil value.
-type UnexpectedPreviousNilValue struct {
- Selector string
-}
-
-// Error returns the error message.
-func (e UnexpectedPreviousNilValue) Error() string {
- return fmt.Sprintf("previous value is nil: %s", e.Selector)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e UnexpectedPreviousNilValue) Is(err error) bool {
- _, ok := err.(*UnexpectedPreviousNilValue)
- return ok
-}
-
-// UnhandledCheckType is returned when the a check doesn't know how to deal with the given type
-type UnhandledCheckType struct {
- Value interface{}
-}
-
-// Error returns the error message.
-func (e UnhandledCheckType) Error() string {
- return fmt.Sprintf("unhandled check type: %T", e.Value)
-}
-
-// Is implements the errors interface, so the errors.Is() function can be used.
-func (e UnhandledCheckType) Is(err error) bool {
- _, ok := err.(*UnhandledCheckType)
- return ok
-}
diff --git a/error_test.go b/error_test.go
deleted file mode 100644
index d5118507..00000000
--- a/error_test.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package dasel_test
-
-import (
- "errors"
- "fmt"
- "reflect"
- "testing"
-
- "github.com/tomwright/dasel/v2"
-)
-
-func TestErrorMessages(t *testing.T) {
- tests := []struct {
- In error
- Out string
- }{
- {In: dasel.ErrMissingPreviousNode, Out: "missing previous node"},
- {In: &dasel.UnknownComparisonOperatorErr{Operator: "<"}, Out: "unknown comparison operator: <"},
- {In: &dasel.InvalidIndexErr{Index: "1"}, Out: "invalid index: 1"},
- {In: &dasel.UnsupportedSelector{Selector: "..."}, Out: "selector is not supported here: ..."},
- {In: &dasel.ValueNotFound{
- Selector: ".name",
- }, Out: "no value found for selector: .name: "},
- {In: &dasel.ValueNotFound{
- Selector: ".name",
- PreviousValue: reflect.ValueOf(map[string]interface{}{}),
- }, Out: "no value found for selector: .name: map[]"},
- {In: &dasel.UnexpectedPreviousNilValue{Selector: ".name"}, Out: "previous value is nil: .name"},
- {In: &dasel.UnhandledCheckType{Value: ""}, Out: "unhandled check type: string"},
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run("ErrorMessage", func(t *testing.T) {
- if exp, got := tc.Out, tc.In.Error(); exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
- })
- }
-}
-
-func TestErrorsIs(t *testing.T) {
- type args struct {
- Err error
- Target error
- }
-
- tests := []struct {
- In args
- Out bool
- }{
- {
- In: args{
- Err: &dasel.UnknownComparisonOperatorErr{},
- Target: &dasel.UnknownComparisonOperatorErr{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.UnknownComparisonOperatorErr{}),
- Target: &dasel.UnknownComparisonOperatorErr{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.UnknownComparisonOperatorErr{},
- },
- Out: false,
- },
- {
- In: args{
- Err: &dasel.InvalidIndexErr{},
- Target: &dasel.InvalidIndexErr{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.InvalidIndexErr{}),
- Target: &dasel.InvalidIndexErr{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.InvalidIndexErr{},
- },
- Out: false,
- },
- {
- In: args{
- Err: &dasel.UnsupportedSelector{},
- Target: &dasel.UnsupportedSelector{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.UnsupportedSelector{}),
- Target: &dasel.UnsupportedSelector{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.UnsupportedSelector{},
- },
- Out: false,
- },
- {
- In: args{
- Err: &dasel.ValueNotFound{},
- Target: &dasel.ValueNotFound{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.ValueNotFound{}),
- Target: &dasel.ValueNotFound{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.ValueNotFound{},
- },
- Out: false,
- },
- {
- In: args{
- Err: &dasel.UnexpectedPreviousNilValue{},
- Target: &dasel.UnexpectedPreviousNilValue{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.UnexpectedPreviousNilValue{}),
- Target: &dasel.UnexpectedPreviousNilValue{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.UnexpectedPreviousNilValue{},
- },
- Out: false,
- },
- {
- In: args{
- Err: &dasel.UnhandledCheckType{},
- Target: &dasel.UnhandledCheckType{},
- },
- Out: true,
- },
- {
- In: args{
- Err: fmt.Errorf("some error: %w", &dasel.UnhandledCheckType{}),
- Target: &dasel.UnhandledCheckType{},
- },
- Out: true,
- },
- {
- In: args{
- Err: errors.New("some error"),
- Target: &dasel.UnhandledCheckType{},
- },
- Out: false,
- },
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run("ErrorMessage", func(t *testing.T) {
- if exp, got := tc.Out, errors.Is(tc.In.Err, tc.In.Target); exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- }
-}
diff --git a/execution/README.md b/execution/README.md
new file mode 100644
index 00000000..590ea6d4
--- /dev/null
+++ b/execution/README.md
@@ -0,0 +1,3 @@
+# Execution
+
+The execution package accepts a `model.Value`, parses a selector and executes the resulting AST on the value.
diff --git a/execution/execute.go b/execution/execute.go
new file mode 100644
index 00000000..88d120aa
--- /dev/null
+++ b/execution/execute.go
@@ -0,0 +1,163 @@
+package execution
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "slices"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+// ExecuteSelector parses the selector and executes the resulting AST with the given input.
+func ExecuteSelector(selectorStr string, value *model.Value, opts *Options) (*model.Value, error) {
+ if selectorStr == "" {
+ return value, nil
+ }
+
+ expr, err := selector.Parse(selectorStr)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing selector: %w", err)
+ }
+
+ res, err := ExecuteAST(expr, value, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error executing selector: %w", err)
+ }
+
+ return res, nil
+}
+
+type expressionExecutor func(data *model.Value) (*model.Value, error)
+
+// ExecuteAST executes the given AST with the given input.
+func ExecuteAST(expr ast.Expr, value *model.Value, options *Options) (*model.Value, error) {
+ if expr == nil {
+ return value, nil
+ }
+
+ executor, err := exprExecutor(options, expr)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating expression: %w", err)
+ }
+
+ if !value.IsBranch() {
+ res, err := executor(value)
+ if err != nil {
+ return nil, fmt.Errorf("execution error: %w", err)
+ }
+ return res, nil
+ }
+
+ res := model.NewSliceValue()
+ res.MarkAsBranch()
+
+ if err := value.RangeSlice(func(i int, v *model.Value) error {
+ r, err := executor(v)
+ if err != nil {
+ return err
+ }
+ if r.IsIgnore() {
+ return nil
+ }
+ return res.Append(r)
+ }); err != nil {
+ return nil, fmt.Errorf("branch execution error: %w", err)
+ }
+
+ return res, nil
+}
+
+var unstableAstTypes = []reflect.Type{
+ reflect.TypeFor[ast.BranchExpr](),
+}
+
+func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) {
+ if !opts.Unstable && (slices.Contains(unstableAstTypes, reflect.TypeOf(expr)) ||
+ slices.Contains(unstableAstTypes, reflect.ValueOf(expr).Type())) {
+ return nil, errors.New("unstable ast types are not enabled. to enable them use --unstable")
+ }
+
+ switch e := expr.(type) {
+ case ast.BinaryExpr:
+ return binaryExprExecutor(opts, e)
+ case ast.UnaryExpr:
+ return unaryExprExecutor(opts, e)
+ case ast.CallExpr:
+ return callExprExecutor(opts, e)
+ case ast.ChainedExpr:
+ return chainedExprExecutor(opts, e)
+ case ast.SpreadExpr:
+ return spreadExprExecutor()
+ case ast.RangeExpr:
+ return rangeExprExecutor(opts, e)
+ case ast.IndexExpr:
+ return indexExprExecutor(opts, e)
+ case ast.PropertyExpr:
+ return propertyExprExecutor(opts, e)
+ case ast.VariableExpr:
+ return variableExprExecutor(opts, e)
+ case ast.NumberIntExpr:
+ return numberIntExprExecutor(e)
+ case ast.NumberFloatExpr:
+ return numberFloatExprExecutor(e)
+ case ast.StringExpr:
+ return stringExprExecutor(e)
+ case ast.BoolExpr:
+ return boolExprExecutor(e)
+ case ast.ObjectExpr:
+ return objectExprExecutor(opts, e)
+ case ast.MapExpr:
+ return mapExprExecutor(opts, e)
+ case ast.FilterExpr:
+ return filterExprExecutor(opts, e)
+ case ast.ConditionalExpr:
+ return conditionalExprExecutor(opts, e)
+ case ast.BranchExpr:
+ return branchExprExecutor(opts, e)
+ case ast.ArrayExpr:
+ return arrayExprExecutor(opts, e)
+ case ast.RegexExpr:
+ // Noop
+ return func(data *model.Value) (*model.Value, error) {
+ return data, nil
+ }, nil
+ case ast.SortByExpr:
+ return sortByExprExecutor(opts, e)
+ case ast.NullExpr:
+ return func(data *model.Value) (*model.Value, error) {
+ return model.NewNullValue(), nil
+ }, nil
+ default:
+ return nil, fmt.Errorf("unhandled expression type: %T", e)
+ }
+}
+
+func chainedExprExecutor(options *Options, e ast.ChainedExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ for _, expr := range e.Exprs {
+ res, err := ExecuteAST(expr, data, options)
+ if err != nil {
+ return nil, fmt.Errorf("error executing expression: %w", err)
+ }
+ data = res
+ }
+ return data, nil
+ }, nil
+}
+
+func variableExprExecutor(opts *Options, e ast.VariableExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ varName := e.Name
+ if varName == "this" {
+ return data, nil
+ }
+ res, ok := opts.Vars[varName]
+ if !ok {
+ return nil, fmt.Errorf("variable %s not found", varName)
+ }
+ return res, nil
+ }, nil
+}
diff --git a/execution/execute_array.go b/execution/execute_array.go
new file mode 100644
index 00000000..99f89e27
--- /dev/null
+++ b/execution/execute_array.go
@@ -0,0 +1,89 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func arrayExprExecutor(opts *Options, e ast.ArrayExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ res := model.NewSliceValue()
+
+ for _, expr := range e.Exprs {
+ el, err := ExecuteAST(expr, data, opts)
+ if err != nil {
+ return nil, err
+ }
+ if err := res.Append(el); err != nil {
+ return nil, err
+ }
+ }
+
+ return res, nil
+ }, nil
+}
+
+func rangeExprExecutor(opts *Options, e ast.RangeExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ var start, end int64 = 0, -1
+ if e.Start != nil {
+ startE, err := ExecuteAST(e.Start, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating start expression: %w", err)
+ }
+
+ start, err = startE.IntValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting start int value: %w", err)
+ }
+ }
+
+ if e.End != nil {
+ endE, err := ExecuteAST(e.End, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating end expression: %w", err)
+ }
+
+ end, err = endE.IntValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting end int value: %w", err)
+ }
+ }
+
+ var res *model.Value
+ var err error
+
+ switch data.Type() {
+ case model.TypeString:
+ res, err = data.StringIndexRange(int(start), int(end))
+ case model.TypeSlice:
+ res, err = data.SliceIndexRange(int(start), int(end))
+ default:
+ err = fmt.Errorf("range expects a slice or string, got %s", data.Type())
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+ }, nil
+}
+
+func indexExprExecutor(opts *Options, e ast.IndexExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ indexE, err := ExecuteAST(e.Index, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating index expression: %w", err)
+ }
+
+ index, err := indexE.IntValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting index int value: %w", err)
+ }
+
+ return data.GetSliceIndex(int(index))
+ }, nil
+}
diff --git a/execution/execute_array_test.go b/execution/execute_array_test.go
new file mode 100644
index 00000000..1b13bdda
--- /dev/null
+++ b/execution/execute_array_test.go
@@ -0,0 +1,111 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestArray(t *testing.T) {
+ inSlice := func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ }
+ inMap := func() *model.Value {
+ m := model.NewMapValue()
+ if err := m.SetMapKey("numbers", inSlice()); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return m
+ }
+
+ runArrayTests := func(in func() *model.Value, prefix string) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Run("1:2", testCase{
+ s: prefix + `[1:2]`,
+ inFn: in,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("1:0", testCase{
+ s: prefix + `[1:0]`,
+ inFn: in,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("1:", testCase{
+ s: prefix + `[1:]`,
+ inFn: in,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ }.run)
+ t.Run(":1", testCase{
+ s: prefix + `[:1]`,
+ inFn: in,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("reverse", testCase{
+ s: prefix + `[len($this)-1:0]`,
+ inFn: in,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ }.run)
+ }
+ }
+
+ t.Run("direct to slice", runArrayTests(inSlice, "$this"))
+ t.Run("property to slice", runArrayTests(inMap, "numbers"))
+}
diff --git a/execution/execute_binary.go b/execution/execute_binary.go
new file mode 100644
index 00000000..1ea37c4d
--- /dev/null
+++ b/execution/execute_binary.go
@@ -0,0 +1,200 @@
+package execution
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+type binaryExpressionExecutorFn func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error)
+
+func basicBinaryExpressionExecutorFn(handler func(left *model.Value, right *model.Value, e ast.BinaryExpr) (*model.Value, error)) binaryExpressionExecutorFn {
+ return func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error) {
+ left, err := ExecuteAST(expr.Left, value, options)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating left expression: %w", err)
+ }
+
+ if !left.IsBranch() {
+ right, err := ExecuteAST(expr.Right, value, options)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating right expression: %w", err)
+ }
+ res, err := handler(left, right, expr)
+ if err != nil {
+ return nil, err
+ }
+ return res, nil
+ }
+
+ res := model.NewSliceValue()
+ res.MarkAsBranch()
+ if err := left.RangeSlice(func(i int, v *model.Value) error {
+ right, err := ExecuteAST(expr.Right, v, options)
+ if err != nil {
+ return fmt.Errorf("error evaluating right expression: %w", err)
+ }
+ r, err := handler(v, right, expr)
+ if err != nil {
+ return err
+ }
+ if err := res.Append(r); err != nil {
+ return err
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return res, nil
+ }
+}
+
+var binaryExpressionExecutors = map[lexer.TokenKind]binaryExpressionExecutorFn{}
+
+func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ if e.Left == nil || e.Right == nil {
+ return nil, fmt.Errorf("left and right expressions must be provided")
+ }
+
+ exec, ok := binaryExpressionExecutors[e.Operator.Kind]
+ if !ok {
+ return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value)
+ }
+
+ return exec(e, data, opts)
+ }, nil
+}
+
+func init() {
+ binaryExpressionExecutors[lexer.Plus] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Add(right)
+ })
+ binaryExpressionExecutors[lexer.Dash] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Subtract(right)
+ })
+ binaryExpressionExecutors[lexer.Star] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Multiply(right)
+ })
+ binaryExpressionExecutors[lexer.Slash] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Divide(right)
+ })
+ binaryExpressionExecutors[lexer.Percent] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Modulo(right)
+ })
+ binaryExpressionExecutors[lexer.GreaterThan] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.GreaterThan(right)
+ })
+ binaryExpressionExecutors[lexer.GreaterThanOrEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.GreaterThanOrEqual(right)
+ })
+ binaryExpressionExecutors[lexer.LessThan] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.LessThan(right)
+ })
+ binaryExpressionExecutors[lexer.LessThanOrEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.LessThanOrEqual(right)
+ })
+ binaryExpressionExecutors[lexer.Equal] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.Equal(right)
+ })
+ binaryExpressionExecutors[lexer.NotEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ return left.NotEqual(right)
+ })
+ binaryExpressionExecutors[lexer.Equals] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ err := left.Set(right)
+ if err != nil {
+ return nil, fmt.Errorf("error setting value: %w", err)
+ }
+ switch left.Type() {
+ case model.TypeMap:
+ return left, nil
+ case model.TypeSlice:
+ return left, nil
+ default:
+ return right, nil
+ }
+ })
+ binaryExpressionExecutors[lexer.And] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ leftBool, err := left.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting left bool value: %w", err)
+ }
+ rightBool, err := right.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting right bool value: %w", err)
+ }
+ return model.NewBoolValue(leftBool && rightBool), nil
+ })
+ binaryExpressionExecutors[lexer.Or] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) {
+ leftBool, err := left.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting left bool value: %w", err)
+ }
+ rightBool, err := right.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting right bool value: %w", err)
+ }
+ return model.NewBoolValue(leftBool || rightBool), nil
+ })
+ binaryExpressionExecutors[lexer.Like] = basicBinaryExpressionExecutorFn(func(left *model.Value, _ *model.Value, e ast.BinaryExpr) (*model.Value, error) {
+ leftStr, err := left.StringValue()
+ if err != nil {
+ return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String())
+ }
+ rightPatt, ok := e.Right.(ast.RegexExpr)
+ if !ok {
+ return nil, fmt.Errorf("like requires right side to be a regex pattern")
+ }
+ res := rightPatt.Regex.MatchString(leftStr)
+ return model.NewBoolValue(res), nil
+ })
+ binaryExpressionExecutors[lexer.NotLike] = basicBinaryExpressionExecutorFn(func(left *model.Value, _ *model.Value, e ast.BinaryExpr) (*model.Value, error) {
+ leftStr, err := left.StringValue()
+ if err != nil {
+ return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String())
+ }
+ rightPatt, ok := e.Right.(ast.RegexExpr)
+ if !ok {
+ return nil, fmt.Errorf("like requires right side to be a regex pattern")
+ }
+ res := rightPatt.Regex.MatchString(leftStr)
+ return model.NewBoolValue(!res), nil
+ })
+ binaryExpressionExecutors[lexer.DoubleQuestionMark] = func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error) {
+ left, err := ExecuteAST(expr.Left, value, options)
+
+ if err == nil && !left.IsNull() {
+ return left, nil
+ }
+
+ if err != nil {
+ handleErrs := []any{
+ model.ErrIncompatibleTypes{},
+ model.ErrUnexpectedType{},
+ model.ErrUnexpectedTypes{},
+ model.SliceIndexOutOfRange{},
+ model.MapKeyNotFound{},
+ }
+ for _, e := range handleErrs {
+ if errors.As(err, &e) {
+ err = nil
+ break
+ }
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating left expression: %w", err)
+ }
+ }
+
+ // Do we need to handle branches here?
+ right, err := ExecuteAST(expr.Right, value, options)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating right expression: %w", err)
+ }
+ return right, nil
+ }
+}
diff --git a/execution/execute_binary_test.go b/execution/execute_binary_test.go
new file mode 100644
index 00000000..d1665337
--- /dev/null
+++ b/execution/execute_binary_test.go
@@ -0,0 +1,282 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestBinary(t *testing.T) {
+ t.Run("math", func(t *testing.T) {
+ t.Run("literals", func(t *testing.T) {
+ t.Run("addition", testCase{
+ s: `1 + 2`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("subtraction", testCase{
+ s: `5 - 2`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("multiplication", testCase{
+ s: `5 * 2`,
+ out: model.NewIntValue(10),
+ }.run)
+ t.Run("division", testCase{
+ s: `10 / 2`,
+ out: model.NewIntValue(5),
+ }.run)
+ t.Run("modulus", testCase{
+ s: `10 % 3`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("ordering", testCase{
+ s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2
+ out: model.NewFloatValue(64.2),
+ }.run)
+ t.Run("ordering with groups", testCase{
+ s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2
+ out: model.NewFloatValue(50.2),
+ }.run)
+ t.Run("ordering with groups", testCase{
+ s: `1 + 1 - 1 + 1 * 2`, // 1 + 1 - 1 + (1 * 2) = 1 + 1 - 1 + 2 = 3
+ out: model.NewIntValue(3),
+ }.run)
+ })
+ t.Run("variables", func(t *testing.T) {
+ in := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("one", 1).
+ Set("two", 2).
+ Set("three", 3).
+ Set("four", 4).
+ Set("five", 5).
+ Set("six", 6).
+ Set("seven", 7).
+ Set("eight", 8).
+ Set("nine", 9).
+ Set("ten", 10).
+ Set("fortyfivepoint2", 45.2))
+ }
+ t.Run("addition", testCase{
+ inFn: in,
+ s: `one + two`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("subtraction", testCase{
+ inFn: in,
+ s: `five - two`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("multiplication", testCase{
+ inFn: in,
+ s: `five * two`,
+ out: model.NewIntValue(10),
+ }.run)
+ t.Run("division", testCase{
+ inFn: in,
+ s: `ten / two`,
+ out: model.NewIntValue(5),
+ }.run)
+ t.Run("modulus", testCase{
+ inFn: in,
+ s: `ten % three`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("ordering", testCase{
+ inFn: in,
+ s: `fortyfivepoint2 + five * four - two / two`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2
+ out: model.NewFloatValue(64.2),
+ }.run)
+ t.Run("ordering with groups", testCase{
+ inFn: in,
+ s: `(fortyfivepoint2 + five) * ((four - two) / two)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2
+ out: model.NewFloatValue(50.2),
+ }.run)
+ })
+ })
+ t.Run("comparison", func(t *testing.T) {
+ t.Run("literals", func(t *testing.T) {
+ t.Run("equal", testCase{
+ s: `1 == 1`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not equal", testCase{
+ s: `1 != 1`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("greater than", testCase{
+ s: `2 > 1`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("greater than or equal", testCase{
+ s: `2 >= 2`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("less than", testCase{
+ s: `1 < 2`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("less than or equal", testCase{
+ s: `2 <= 2`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("like", testCase{
+ s: `"hello world" =~ r/ello/`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not like", testCase{
+ s: `"hello world" !~ r/helloworld/`,
+ out: model.NewBoolValue(true),
+ }.run)
+ })
+
+ t.Run("variables", func(t *testing.T) {
+ in := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("one", 1).
+ Set("two", 2).
+ Set("nested", orderedmap.NewMap().
+ Set("three", 3).
+ Set("four", 4)))
+ }
+ t.Run("equal", testCase{
+ inFn: in,
+ s: `one == one`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not equal", testCase{
+ inFn: in,
+ s: `one != one`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("greater than", testCase{
+ inFn: in,
+ s: `two > one`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("greater than or equal", testCase{
+ inFn: in,
+ s: `two >= two`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("less than", testCase{
+ inFn: in,
+ s: `one < two`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("less than or equal", testCase{
+ inFn: in,
+ s: `two <= two`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("nested with math more than", testCase{
+ inFn: in,
+ s: `nested.three + nested.four * 0 > one * 1`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("nested with grouped math more than", testCase{
+ inFn: in,
+ s: `(nested.three + nested.four) * 0 > one * 1`,
+ out: model.NewBoolValue(false),
+ }.run)
+ })
+
+ t.Run("coalesce", func(t *testing.T) {
+ t.Run("literals", func(t *testing.T) {
+ t.Run("coalesce", testCase{
+ s: `null ?? 1`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("coalesce with null", testCase{
+ s: `null ?? null`,
+ out: model.NewNullValue(),
+ }.run)
+ t.Run("coalesce with null and value", testCase{
+ s: `null ?? 2`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("coalesce with value", testCase{
+ s: `1 ?? 2`,
+ out: model.NewIntValue(1),
+ }.run)
+ })
+ t.Run("variables", func(t *testing.T) {
+ in := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("one", 1).
+ Set("two", 2).
+ Set("nested", orderedmap.NewMap().
+ Set("one", 1).
+ Set("two", 2).
+ Set("three", 3).
+ Set("four", 4)).
+ Set("list", []any{1, 2, 3}))
+ }
+ t.Run("coalesce", testCase{
+ inFn: in,
+ s: `nested.five ?? one`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("coalesce with null", testCase{
+ inFn: in,
+ s: `nested.five ?? null`,
+ out: model.NewNullValue(),
+ }.run)
+ t.Run("coalesce with null and value", testCase{
+ inFn: in,
+ s: `nested.five ?? 2`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("coalesce with value", testCase{
+ inFn: in,
+ s: `nested.three ?? 2`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("coalesce with bad map key", testCase{
+ inFn: in,
+ s: `nope ?? 2`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("coalesce with nested bad map key", testCase{
+ inFn: in,
+ s: `nested.nope ?? 2`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("coalesce with list index", testCase{
+ inFn: in,
+ s: `list[1] ?? 5`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("coalesce with list bad index", testCase{
+ inFn: in,
+ s: `list[3] ?? 5`,
+ out: model.NewIntValue(5),
+ }.run)
+ t.Run("chained coalesce execute left to right", func(t *testing.T) {
+ // These tests ensure the coalesces run in order.
+ t.Run("no match", testCase{
+ inFn: in,
+ s: `nested.five ?? nested.six ?? nested.seven ?? 10`,
+ out: model.NewIntValue(10),
+ }.run)
+ t.Run("first match when all exist", testCase{
+ inFn: in,
+ s: `nested.one ?? nested.two ?? nested.three ?? 10`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("second match", testCase{
+ inFn: in,
+ s: `nested.five ?? nested.two ?? nested.three ?? 10`,
+ out: model.NewIntValue(2),
+ }.run)
+ t.Run("third match", testCase{
+ inFn: in,
+ s: `nested.five ?? nested.six ?? nested.three ?? 10`,
+ out: model.NewIntValue(3),
+ }.run)
+ })
+ })
+ })
+ })
+}
diff --git a/execution/execute_branch.go b/execution/execute_branch.go
new file mode 100644
index 00000000..95d5a060
--- /dev/null
+++ b/execution/execute_branch.go
@@ -0,0 +1,47 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func branchExprExecutor(opts *Options, e ast.BranchExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ res := model.NewSliceValue()
+ res.MarkAsBranch()
+
+ if len(e.Exprs) == 0 {
+ // No expressions given. We'll branch on the input data.
+ if err := data.RangeSlice(func(_ int, value *model.Value) error {
+ if err := res.Append(value); err != nil {
+ return fmt.Errorf("failed to append branch result: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("failed to range slice: %w", err)
+ }
+ } else {
+ for _, expr := range e.Exprs {
+ r, err := ExecuteAST(expr, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute branch expr: %w", err)
+ }
+
+ // This deals with the spread operator in the branch expression.
+ valsToAppend, err := prepareSpreadValues(r)
+ if err != nil {
+ return nil, fmt.Errorf("error handling spread values: %w", err)
+ }
+ for _, v := range valsToAppend {
+ if err := res.Append(v); err != nil {
+ return nil, fmt.Errorf("failed to append branch result: %w", err)
+ }
+ }
+ }
+ }
+
+ return res, nil
+ }, nil
+}
diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go
new file mode 100644
index 00000000..eb70160a
--- /dev/null
+++ b/execution/execute_branch_test.go
@@ -0,0 +1,148 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestBranch(t *testing.T) {
+ t.Run("single branch", testCase{
+ s: "branch(1)",
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("many branches", testCase{
+ s: "branch(1, 1+1, 3/1, 123)",
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(123)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("spread into many branches", testCase{
+ s: "[1,2,3].branch(...)",
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("chained branch set", testCase{
+ s: "branch(1, 2, 3).$this=5",
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(5)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(5)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(5)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("chained branch math", testCase{
+ s: "(branch(1, 2, 3)) * 2",
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(4)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(6)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("chained branch math using branched value", testCase{
+ s: `branch({"x":1}, {"x":2}, {"x":3}).x * $this`,
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(4)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(9)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+ t.Run("map on branch", testCase{
+ s: `branch([1], [2], [3]).map($this * 2).branch()`,
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ r.MarkAsBranch()
+ if err := r.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(4)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewIntValue(6)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ opts: []execution.ExecuteOptionFn{
+ execution.WithUnstable(),
+ },
+ }.run)
+}
diff --git a/execution/execute_conditional.go b/execution/execute_conditional.go
new file mode 100644
index 00000000..bda61c60
--- /dev/null
+++ b/execution/execute_conditional.go
@@ -0,0 +1,40 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func conditionalExprExecutor(opts *Options, e ast.ConditionalExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ cond, err := ExecuteAST(e.Cond, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating condition: %w", err)
+ }
+
+ condBool, err := cond.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error converting condition to boolean: %w", err)
+ }
+
+ if condBool {
+ res, err := ExecuteAST(e.Then, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error executing then block: %w", err)
+ }
+ return res, nil
+ }
+
+ if e.Else != nil {
+ res, err := ExecuteAST(e.Else, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error executing else block: %w", err)
+ }
+ return res, nil
+ }
+
+ return model.NewNullValue(), nil
+ }, nil
+}
diff --git a/execution/execute_conditional_test.go b/execution/execute_conditional_test.go
new file mode 100644
index 00000000..ff77ae66
--- /dev/null
+++ b/execution/execute_conditional_test.go
@@ -0,0 +1,56 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestConditional(t *testing.T) {
+ t.Run("true", testCase{
+ s: `if (true) { "yes" } else { "no" }`,
+ out: model.NewStringValue("yes"),
+ }.run)
+ t.Run("false", testCase{
+ s: `if (false) { "yes" } else { "no" }`,
+ out: model.NewStringValue("no"),
+ }.run)
+ t.Run("nested", testCase{
+ s: `
+ if (true) {
+ if (true) { "yes" }
+ else { "no" }
+ } else { "no" }`,
+ out: model.NewStringValue("yes"),
+ }.run)
+ t.Run("nested false", testCase{
+ s: `
+ if (true) {
+ if (false) { "yes" }
+ else { "no" }
+ } else { "no" }`,
+ out: model.NewStringValue("no"),
+ }.run)
+ t.Run("else if", testCase{
+ s: `
+ if (false) { "yes" }
+ elseif (true) { "no" }
+ else { "maybe" }`,
+ out: model.NewStringValue("no"),
+ }.run)
+ t.Run("else if else", testCase{
+ s: `
+ if (false) { "yes" }
+ elseif (false) { "no" }
+ else { "maybe" }`,
+ out: model.NewStringValue("maybe"),
+ }.run)
+ t.Run("if elseif elseif else", testCase{
+ s: `
+ if (false) { "yes" }
+ elseif (false) { "no" }
+ elseif (false) { "maybe" }
+ else { "nope" }`,
+ out: model.NewStringValue("nope"),
+ }.run)
+}
diff --git a/execution/execute_filter.go b/execution/execute_filter.go
new file mode 100644
index 00000000..a86c8f1e
--- /dev/null
+++ b/execution/execute_filter.go
@@ -0,0 +1,41 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func filterExprExecutor(opts *Options, e ast.FilterExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ if !data.IsSlice() {
+ return nil, fmt.Errorf("cannot filter over non-array")
+ }
+ res := model.NewSliceValue()
+
+ if err := data.RangeSlice(func(i int, item *model.Value) error {
+ v, err := ExecuteAST(e.Expr, item, opts)
+ if err != nil {
+ return err
+ }
+
+ boolV, err := v.BoolValue()
+ if err != nil {
+ return err
+ }
+
+ if !boolV {
+ return nil
+ }
+ if err := res.Append(item); err != nil {
+ return fmt.Errorf("error appending item to result: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging over slice: %w", err)
+ }
+
+ return res, nil
+ }, nil
+}
diff --git a/execution/execute_filter_test.go b/execution/execute_filter_test.go
new file mode 100644
index 00000000..c3cd78cf
--- /dev/null
+++ b/execution/execute_filter_test.go
@@ -0,0 +1,98 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFilter(t *testing.T) {
+ inSlice := func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ }
+ t.Run("all true", testCase{
+ inFn: inSlice,
+ s: "filter(true)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ },
+ }.run)
+ t.Run("all !false", testCase{
+ inFn: inSlice,
+ s: "filter(!false)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ },
+ }.run)
+ t.Run("all false", testCase{
+ inFn: inSlice,
+ s: "filter(false)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ return s
+ },
+ }.run)
+ t.Run("all !true", testCase{
+ inFn: inSlice,
+ s: "filter(!true)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ return s
+ },
+ }.run)
+ t.Run("equal 2", testCase{
+ inFn: inSlice,
+ s: "filter($this == 2)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ },
+ }.run)
+ t.Run("not equal 2", testCase{
+ inFn: inSlice,
+ s: "filter($this != 2)",
+ outFn: func() *model.Value {
+ s := model.NewSliceValue()
+ if err := s.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := s.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return s
+ },
+ }.run)
+}
diff --git a/execution/execute_func.go b/execution/execute_func.go
new file mode 100644
index 00000000..32466df0
--- /dev/null
+++ b/execution/execute_func.go
@@ -0,0 +1,63 @@
+package execution
+
+import (
+ "errors"
+ "fmt"
+ "slices"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func prepareArgs(opts *Options, data *model.Value, argsE ast.Expressions) (model.Values, error) {
+ args := make(model.Values, 0)
+ for i, arg := range argsE {
+ res, err := ExecuteAST(arg, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating argument %d: %w", i, err)
+ }
+
+ argVals, err := prepareSpreadValues(res)
+ if err != nil {
+ return nil, fmt.Errorf("error handling spread values: %w", err)
+ }
+
+ args = append(args, argVals...)
+ }
+ return args, nil
+}
+
+func callFnExecutor(opts *Options, f FuncFn, argsE ast.Expressions) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ args, err := prepareArgs(opts, data, argsE)
+ if err != nil {
+ return nil, fmt.Errorf("error preparing arguments: %w", err)
+ }
+
+ res, err := f(data, args)
+ if err != nil {
+ return nil, fmt.Errorf("error executing function: %w", err)
+ }
+
+ return res, nil
+ }, nil
+}
+
+var unstableFuncs = []string{
+ "ignore",
+}
+
+func callExprExecutor(opts *Options, e ast.CallExpr) (expressionExecutor, error) {
+ if !opts.Unstable && (slices.Contains(unstableFuncs, e.Function)) {
+ return nil, errors.New("unstable function are not enabled. to enable them use --unstable")
+ }
+ if f, ok := opts.Funcs.Get(e.Function); ok {
+ res, err := callFnExecutor(opts, f, e.Args)
+ if err != nil {
+ return nil, fmt.Errorf("error executing function %q: %w", e.Function, err)
+ }
+ return res, nil
+ }
+
+ return nil, fmt.Errorf("unknown function: %q", e.Function)
+}
diff --git a/execution/execute_func_test.go b/execution/execute_func_test.go
new file mode 100644
index 00000000..75339d52
--- /dev/null
+++ b/execution/execute_func_test.go
@@ -0,0 +1,49 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFunc(t *testing.T) {
+ returnInputData := execution.NewFunc(
+ "returnInputData",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ return data, nil
+ },
+ execution.ValidateArgsExactly(0),
+ )
+
+ returnFirstArg := execution.NewFunc(
+ "returnFirstArg",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ return args[0], nil
+ },
+ execution.ValidateArgsExactly(1),
+ )
+
+ funcs := execution.NewFuncCollection(
+ returnInputData,
+ returnFirstArg,
+ )
+
+ opts := []execution.ExecuteOptionFn{
+ func(options *execution.Options) {
+ options.Funcs = funcs
+ },
+ }
+
+ t.Run("returnInputData", testCase{
+ s: `1.returnInputData()`,
+ out: model.NewIntValue(1),
+ opts: opts,
+ }.run)
+
+ t.Run("returnFirstArg", testCase{
+ s: `1.returnFirstArg(2)`,
+ out: model.NewIntValue(2),
+ opts: opts,
+ }.run)
+}
diff --git a/execution/execute_literal.go b/execution/execute_literal.go
new file mode 100644
index 00000000..97e5ea12
--- /dev/null
+++ b/execution/execute_literal.go
@@ -0,0 +1,30 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func numberIntExprExecutor(e ast.NumberIntExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ return model.NewIntValue(e.Value), nil
+ }, nil
+}
+
+func numberFloatExprExecutor(e ast.NumberFloatExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ return model.NewFloatValue(e.Value), nil
+ }, nil
+}
+
+func stringExprExecutor(e ast.StringExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ return model.NewStringValue(e.Value), nil
+ }, nil
+}
+
+func boolExprExecutor(e ast.BoolExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ return model.NewBoolValue(e.Value), nil
+ }, nil
+}
diff --git a/execution/execute_literal_test.go b/execution/execute_literal_test.go
new file mode 100644
index 00000000..677bb7ff
--- /dev/null
+++ b/execution/execute_literal_test.go
@@ -0,0 +1,113 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestLiteral(t *testing.T) {
+ t.Run("string", testCase{
+ s: `"hello"`,
+ out: model.NewStringValue("hello"),
+ }.run)
+ t.Run("int", testCase{
+ s: `123`,
+ out: model.NewIntValue(123),
+ }.run)
+ t.Run("float", testCase{
+ s: `123.4`,
+ out: model.NewFloatValue(123.4),
+ }.run)
+ t.Run("true", testCase{
+ s: `true`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("false", testCase{
+ s: `false`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("empty array", testCase{
+ s: `[]`,
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ return r
+ },
+ }.run)
+ t.Run("array with one element", testCase{
+ s: `[1]`,
+ outFn: func() *model.Value {
+ r := model.NewSliceValue()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ }.run)
+ t.Run("array with many elements", testCase{
+ s: `[1, 2.2, "foo", true, [1, 2, 3]]`,
+ outFn: func() *model.Value {
+ nested := model.NewSliceValue()
+ if err := nested.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := nested.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := nested.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ r := model.NewSliceValue()
+ if err := r.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewFloatValue(2.2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewStringValue("foo")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewBoolValue(true)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(nested); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ }.run)
+ t.Run("array with expressions", testCase{
+ s: `[1 + 1, 2f - 2, "foo" + "bar", true || false, [1 + 1, 2 * 2, 3 / 3]]`,
+ outFn: func() *model.Value {
+ nested := model.NewSliceValue()
+ if err := nested.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := nested.Append(model.NewIntValue(4)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := nested.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ r := model.NewSliceValue()
+ if err := r.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewFloatValue(0)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewStringValue("foobar")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(model.NewBoolValue(true)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := r.Append(nested); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return r
+ },
+ }.run)
+}
diff --git a/execution/execute_map.go b/execution/execute_map.go
new file mode 100644
index 00000000..6059063a
--- /dev/null
+++ b/execution/execute_map.go
@@ -0,0 +1,31 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func mapExprExecutor(opts *Options, e ast.MapExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ if !data.IsSlice() {
+ return nil, fmt.Errorf("cannot map over non-array")
+ }
+ res := model.NewSliceValue()
+
+ if err := data.RangeSlice(func(i int, item *model.Value) error {
+ item, err := ExecuteAST(e.Expr, item, opts)
+ if err != nil {
+ return err
+ }
+ if err := res.Append(item); err != nil {
+ return fmt.Errorf("error appending item to result: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging over slice: %w", err)
+ }
+ return res, nil
+ }, nil
+}
diff --git a/execution/execute_map_test.go b/execution/execute_map_test.go
new file mode 100644
index 00000000..c03fc979
--- /dev/null
+++ b/execution/execute_map_test.go
@@ -0,0 +1,53 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestMap(t *testing.T) {
+ t.Run("property from slice of maps", testCase{
+ inFn: func() *model.Value {
+ return model.NewValue([]any{
+ orderedmap.NewMap().Set("number", 1),
+ orderedmap.NewMap().Set("number", 2),
+ orderedmap.NewMap().Set("number", 3),
+ })
+ },
+ s: `map(number)`,
+ outFn: func() *model.Value {
+ return model.NewValue([]any{1, 2, 3})
+ },
+ }.run)
+ t.Run("with chain of selectors", testCase{
+ inFn: func() *model.Value {
+ return model.NewValue([]any{
+ orderedmap.NewMap().Set("foo", 1).Set("bar", 4),
+ orderedmap.NewMap().Set("foo", 2).Set("bar", 5),
+ orderedmap.NewMap().Set("foo", 3).Set("bar", 6),
+ })
+ },
+ s: `
+ map (
+ {
+ total: add( foo, bar, 1 )
+ }
+ )
+ .map ( total )`,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewValue(6)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewValue(8)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewValue(10)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ }.run)
+}
diff --git a/execution/execute_object.go b/execution/execute_object.go
new file mode 100644
index 00000000..ad41de51
--- /dev/null
+++ b/execution/execute_object.go
@@ -0,0 +1,99 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func objectExprExecutor(opts *Options, e ast.ObjectExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ obj := model.NewMapValue()
+ for _, p := range e.Pairs {
+
+ if ast.IsType[ast.SpreadExpr](p.Key) {
+ var val *model.Value
+ var err error
+ if p.Value != nil {
+ // We need to spread the resulting value.
+ val, err = ExecuteAST(p.Value, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating spread values: %w", err)
+ }
+ } else {
+ val = data
+ }
+
+ if err := val.RangeMap(func(key string, value *model.Value) error {
+ if err := obj.SetMapKey(key, value); err != nil {
+ return fmt.Errorf("error setting map key: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error spreading into object: %w", err)
+ }
+ continue
+ }
+
+ //if ast.IsType[ast.SpreadExpr](p.Key) && ast.IsType[ast.SpreadExpr](p.Value) {
+ // if err := data.RangeMap(func(key string, value *model.Value) error {
+ // if err := obj.SetMapKey(key, value); err != nil {
+ // return fmt.Errorf("error setting map key: %w", err)
+ // }
+ // return nil
+ // }); err != nil {
+ // return nil, fmt.Errorf("error ranging map: %w", err)
+ // }
+ // continue
+ //}
+
+ //if ast.IsSpreadExpr(p.Key) {
+ // return nil, fmt.Errorf("cannot spread object key name")
+ //}
+
+ key, err := ExecuteAST(p.Key, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating key: %w", err)
+ }
+ if !key.IsString() {
+ return nil, fmt.Errorf("expected key to resolve to string, got %s", key.Type())
+ }
+ val, err := ExecuteAST(p.Value, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating value: %w", err)
+ }
+ keyStr, err := key.StringValue()
+ if err := obj.SetMapKey(keyStr, val); err != nil {
+ return nil, fmt.Errorf("error setting map key: %w", err)
+ }
+ }
+ return obj, nil
+ }, nil
+}
+
+func propertyExprExecutor(opts *Options, e ast.PropertyExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ key, err := ExecuteAST(e.Property, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating property: %w", err)
+ }
+ switch {
+ case key.IsString():
+ keyStr, err := key.StringValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting string value: %w", err)
+ }
+
+ return data.GetMapKey(keyStr)
+ case key.IsInt():
+ keyInt, err := key.IntValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting int value: %w", err)
+ }
+ return data.GetSliceIndex(int(keyInt))
+ default:
+ return nil, fmt.Errorf("expected key to be a string or int, got %s", key.Type())
+ }
+ }, nil
+}
diff --git a/execution/execute_object_test.go b/execution/execute_object_test.go
new file mode 100644
index 00000000..37edb2a5
--- /dev/null
+++ b/execution/execute_object_test.go
@@ -0,0 +1,102 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestObject(t *testing.T) {
+ inputMap := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("title", "Mr").
+ Set("age", int64(30)).
+ Set("name", orderedmap.NewMap().
+ Set("first", "Tom").
+ Set("last", "Wright")))
+ }
+ t.Run("get", testCase{
+ in: inputMap(),
+ s: `{title}`,
+ outFn: func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().Set("title", "Mr"))
+ },
+ }.run)
+ t.Run("get multiple", testCase{
+ in: inputMap(),
+ s: `{title, age}`,
+ outFn: func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().Set("title", "Mr").Set("age", int64(30)))
+ },
+ }.run)
+ t.Run("get with spread", testCase{
+ in: inputMap(),
+ s: `{...}`,
+ outFn: func() *model.Value {
+ res := inputMap()
+ return res
+ },
+ }.run)
+ t.Run("set", testCase{
+ in: inputMap(),
+ s: `{title:"Mrs"}`,
+ outFn: func() *model.Value {
+ res := model.NewMapValue()
+ _ = res.SetMapKey("title", model.NewStringValue("Mrs"))
+ return res
+ },
+ }.run)
+ t.Run("set with spread", testCase{
+ in: inputMap(),
+ s: `{..., title:"Mrs"}`,
+ outFn: func() *model.Value {
+ res := inputMap()
+ _ = res.SetMapKey("title", model.NewStringValue("Mrs"))
+ return res
+ },
+ }.run)
+ t.Run("merge with spread", testCase{
+ inFn: func() *model.Value {
+ a := model.NewMapValue()
+ if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ b := model.NewMapValue()
+ if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ res := model.NewMapValue()
+ if err := res.SetMapKey("a", a); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := res.SetMapKey("b", b); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ return res
+ },
+ s: `{a..., b..., x: 1}`,
+ outFn: func() *model.Value {
+ b := model.NewMapValue()
+ if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("x", model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return b
+ },
+ }.run)
+}
diff --git a/execution/execute_sort_by.go b/execution/execute_sort_by.go
new file mode 100644
index 00000000..9424d067
--- /dev/null
+++ b/execution/execute_sort_by.go
@@ -0,0 +1,154 @@
+package execution
+
+import (
+ "fmt"
+ "slices"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func sortByExprExecutor(opts *Options, e ast.SortByExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ if !data.IsSlice() {
+ return nil, fmt.Errorf("cannot sort by on non-slice data")
+ }
+
+ type sortableValue struct {
+ index int
+ value *model.Value
+ }
+ values := make([]sortableValue, 0)
+
+ if err := data.RangeSlice(func(i int, item *model.Value) error {
+ item, err := ExecuteAST(e.Expr, item, opts)
+ if err != nil {
+ return err
+ }
+ values = append(values, sortableValue{
+ index: i,
+ value: item,
+ })
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging over slice: %w", err)
+ }
+
+ slices.SortFunc(values, func(i, j sortableValue) int {
+ res, err := i.value.Compare(j.value)
+ if err != nil {
+ return 0
+ }
+ if e.Descending {
+ return -res
+ }
+ return res
+ })
+
+ res := model.NewSliceValue()
+
+ for _, i := range values {
+ item, err := data.GetSliceIndex(i.index)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ if err := res.Append(item); err != nil {
+ return nil, fmt.Errorf("error appending item to result: %w", err)
+ }
+ }
+
+ return res, nil
+ }, nil
+}
+
+func sortByExprExecutor2(opts *Options, e ast.SortByExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ if !data.IsSlice() {
+ return nil, fmt.Errorf("cannot sort by on non-slice data")
+ }
+
+ sortedValues := model.NewSliceValue()
+ sortedIndexes := make([]int, 0)
+
+ if err := data.RangeSlice(func(i int, item *model.Value) error {
+ item, err := ExecuteAST(e.Expr, item, opts)
+ if err != nil {
+ return err
+ }
+ if err := sortedValues.Append(item); err != nil {
+ return fmt.Errorf("error appending item to result: %w", err)
+ }
+ sortedIndexes = append(sortedIndexes, i)
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging over slice: %w", err)
+ }
+
+ l, err := sortedValues.Len()
+ if err != nil {
+ return nil, fmt.Errorf("error getting length of slice: %w", err)
+ }
+
+ for i := 0; i < l-1; i++ {
+ cur, err := sortedValues.GetSliceIndex(i)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ curIndex := sortedIndexes[i]
+ next, err := sortedValues.GetSliceIndex(i + 1)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ nextIndex := sortedIndexes[i+1]
+
+ cmp, err := cur.Compare(next)
+ if err != nil {
+ return nil, fmt.Errorf("error comparing values: %w", err)
+ }
+
+ if cmp == 0 {
+ continue
+ }
+
+ if !e.Descending {
+ if cmp > 0 {
+ if err := sortedValues.SetSliceIndex(i, next); err != nil {
+ return nil, fmt.Errorf("error setting slice index: %w", err)
+ }
+ sortedIndexes[i] = nextIndex
+ if err := sortedValues.SetSliceIndex(i+1, cur); err != nil {
+ return nil, fmt.Errorf("error setting slice index: %w", err)
+ }
+ sortedIndexes[i+1] = curIndex
+ i -= 1
+ }
+ } else {
+ if cmp < 0 {
+ if err := sortedValues.SetSliceIndex(i, next); err != nil {
+ return nil, fmt.Errorf("error setting slice index: %w", err)
+ }
+ sortedIndexes[i] = nextIndex
+ if err := sortedValues.SetSliceIndex(i+1, cur); err != nil {
+ return nil, fmt.Errorf("error setting slice index: %w", err)
+ }
+ sortedIndexes[i+1] = curIndex
+ i -= 1
+ }
+ }
+ }
+
+ res := model.NewSliceValue()
+
+ for _, i := range sortedIndexes {
+ item, err := data.GetSliceIndex(i)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ if err := res.Append(item); err != nil {
+ return nil, fmt.Errorf("error appending item to result: %w", err)
+ }
+ }
+
+ return res, nil
+ }, nil
+}
diff --git a/execution/execute_sort_by_test.go b/execution/execute_sort_by_test.go
new file mode 100644
index 00000000..07dc5436
--- /dev/null
+++ b/execution/execute_sort_by_test.go
@@ -0,0 +1,181 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncSortBy(t *testing.T) {
+ runSortTests := func(in func() *model.Value, outAsc func() *model.Value, outDesc func() *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ t.Run("asc default", testCase{
+ inFn: in,
+ s: `sortBy($this)`,
+ outFn: outAsc,
+ }.run)
+ t.Run("asc", testCase{
+ inFn: in,
+ s: `sortBy($this, asc)`,
+ outFn: outAsc,
+ }.run)
+ t.Run("desc", testCase{
+ inFn: in,
+ s: `sortBy($this, desc)`,
+ outFn: outDesc,
+ }.run)
+ }
+ }
+
+ t.Run("int", runSortTests(
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(4)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(4)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(4)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ ))
+
+ t.Run("float", runSortTests(
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewFloatValue(2.23)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(5.123)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(4.2)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewFloatValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(2.23)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(4.2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(5.123)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewFloatValue(5.123)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(4.2)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(2.23)); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewFloatValue(2)); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ ))
+ t.Run("string", runSortTests(
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("def")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("abc")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("cde")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("bcd")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("abc")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("bcd")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("cde")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("def")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("def")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("cde")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("bcd")); err != nil {
+ t.Fatal(err)
+ }
+ if err := res.Append(model.NewStringValue("abc")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ ))
+}
diff --git a/execution/execute_spread.go b/execution/execute_spread.go
new file mode 100644
index 00000000..152684e0
--- /dev/null
+++ b/execution/execute_spread.go
@@ -0,0 +1,60 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func spreadExprExecutor() (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ s := model.NewSliceValue()
+
+ s.MarkAsSpread()
+
+ switch {
+ case data.IsSlice():
+ if err := data.RangeSlice(func(key int, value *model.Value) error {
+ if err := s.Append(value); err != nil {
+ return fmt.Errorf("error appending value to slice: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging slice: %w", err)
+ }
+ case data.IsMap():
+ if err := data.RangeMap(func(key string, value *model.Value) error {
+ if err := s.Append(value); err != nil {
+ return fmt.Errorf("error appending value to slice: %w", err)
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("error ranging map: %w", err)
+ }
+ default:
+ return nil, fmt.Errorf("cannot spread on type %s", data.Type())
+ }
+
+ return s, nil
+ }, nil
+}
+
+// prepareSpreadValues looks at the incoming value, and if we detect a spread value, we return the individual values.
+func prepareSpreadValues(val *model.Value) (model.Values, error) {
+ if val.IsSlice() && val.IsSpread() {
+ sliceLen, err := val.SliceLen()
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice length: %w", err)
+ }
+ values := make(model.Values, sliceLen)
+ for i := 0; i < sliceLen; i++ {
+ v, err := val.GetSliceIndex(i)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index %d: %w", i, err)
+ }
+ values[i] = v
+ }
+ return values, nil
+ }
+ return model.Values{val}, nil
+}
diff --git a/execution/execute_test.go b/execution/execute_test.go
new file mode 100644
index 00000000..62189ed4
--- /dev/null
+++ b/execution/execute_test.go
@@ -0,0 +1,148 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+type testCase struct {
+ in *model.Value
+ inFn func() *model.Value
+ s string
+ out *model.Value
+ outFn func() *model.Value
+ compareRoot bool
+ opts []execution.ExecuteOptionFn
+}
+
+func (tc testCase) run(t *testing.T) {
+ in := tc.in
+ if tc.inFn != nil {
+ in = tc.inFn()
+ }
+ if in == nil {
+ in = model.NewValue(nil)
+ }
+ exp := tc.out
+ if tc.outFn != nil {
+ exp = tc.outFn()
+ }
+ res, err := execution.ExecuteSelector(tc.s, in, execution.NewOptions(tc.opts...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if tc.compareRoot {
+ res = in
+ }
+
+ equal, err := res.EqualTypeValue(exp)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !equal {
+ t.Errorf("unexpected output: %v\nexp: %s\ngot: %s", cmp.Diff(exp.Interface(), res.Interface()), exp.String(), res.String())
+ }
+
+ expMeta := exp.Metadata
+ gotMeta := res.Metadata
+ if !cmp.Equal(expMeta, gotMeta) {
+ t.Errorf("unexpected output metadata: %v", cmp.Diff(expMeta, gotMeta))
+ }
+}
+
+func TestExecuteSelector_HappyPath(t *testing.T) {
+ t.Run("get", func(t *testing.T) {
+ inputMap := func() *model.Value {
+ return model.NewValue(
+ orderedmap.NewMap().
+ Set("title", "Mr").
+ Set("age", int64(31)).
+ Set("name", orderedmap.NewMap().
+ Set("first", "Tom").
+ Set("last", "Wright")),
+ )
+ }
+ t.Run("property", testCase{
+ in: inputMap(),
+ s: `title`,
+ out: model.NewStringValue("Mr"),
+ }.run)
+ t.Run("nested property", testCase{
+ in: inputMap(),
+ s: `name.first`,
+ out: model.NewStringValue("Tom"),
+ }.run)
+ t.Run("concat with grouping", testCase{
+ in: inputMap(),
+ s: `title + " " + (name.first) + " " + (name.last)`,
+ out: model.NewStringValue("Mr Tom Wright"),
+ }.run)
+ t.Run("concat", testCase{
+ in: inputMap(),
+ s: `title + " " + name.first + " " + name.last`,
+ out: model.NewStringValue("Mr Tom Wright"),
+ }.run)
+ t.Run("add evaluated fields", testCase{
+ in: inputMap(),
+ s: `{..., "over30": age > 30}`,
+ outFn: func() *model.Value {
+ return model.NewValue(
+ orderedmap.NewMap().
+ Set("title", "Mr").
+ Set("age", int64(31)).
+ Set("name", orderedmap.NewMap().
+ Set("first", "Tom").
+ Set("last", "Wright")).
+ Set("over30", true),
+ )
+ },
+ }.run)
+ })
+
+ t.Run("set", func(t *testing.T) {
+ inputMap := func() *model.Value {
+ return model.NewValue(
+ orderedmap.NewMap().
+ Set("title", "Mr").
+ Set("age", int64(31)).
+ Set("name", orderedmap.NewMap().
+ Set("first", "Tom").
+ Set("last", "Wright")),
+ )
+ }
+ inputSlice := func() *model.Value {
+ return model.NewValue([]any{1, 2, 3})
+ }
+
+ t.Run("set property", testCase{
+ in: inputMap(),
+ s: `title = "Mrs"`,
+ outFn: func() *model.Value {
+ res := inputMap()
+ if err := res.SetMapKey("title", model.NewStringValue("Mrs")); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ compareRoot: true,
+ }.run)
+
+ t.Run("set index", testCase{
+ in: inputSlice(),
+ s: `$this[1] = 4`,
+ outFn: func() *model.Value {
+ res := inputSlice()
+ if err := res.SetSliceIndex(1, model.NewIntValue(4)); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ },
+ compareRoot: true,
+ }.run)
+ })
+}
diff --git a/execution/execute_unary.go b/execution/execute_unary.go
new file mode 100644
index 00000000..8ed02ae3
--- /dev/null
+++ b/execution/execute_unary.go
@@ -0,0 +1,29 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func unaryExprExecutor(opts *Options, e ast.UnaryExpr) (expressionExecutor, error) {
+ return func(data *model.Value) (*model.Value, error) {
+ right, err := ExecuteAST(e.Right, data, opts)
+ if err != nil {
+ return nil, fmt.Errorf("error evaluating right expression: %w", err)
+ }
+
+ switch e.Operator.Kind {
+ case lexer.Exclamation:
+ boolV, err := right.BoolValue()
+ if err != nil {
+ return nil, fmt.Errorf("error converting value to boolean: %w", err)
+ }
+ return model.NewBoolValue(!boolV), nil
+ default:
+ return nil, fmt.Errorf("unhandled unary operator: %s", e.Operator.Value)
+ }
+ }, nil
+}
diff --git a/execution/execute_unary_test.go b/execution/execute_unary_test.go
new file mode 100644
index 00000000..68a80866
--- /dev/null
+++ b/execution/execute_unary_test.go
@@ -0,0 +1,76 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestUnary(t *testing.T) {
+ t.Run("not", func(t *testing.T) {
+ t.Run("literals", func(t *testing.T) {
+ t.Run("not true", testCase{
+ s: `!true`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not not true", testCase{
+ s: `!!true`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not not not true", testCase{
+ s: `!!!true`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not false", testCase{
+ s: `!false`,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not not false", testCase{
+ s: `!!false`,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not not not false", testCase{
+ s: `!!!false`,
+ out: model.NewBoolValue(true),
+ }.run)
+ })
+ t.Run("variables", func(t *testing.T) {
+ in := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("t", true).
+ Set("f", false))
+ }
+ t.Run("not true", testCase{
+ s: `!t`,
+ inFn: in,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not not true", testCase{
+ s: `!!t`,
+ inFn: in,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not not not true", testCase{
+ s: `!!!t`,
+ inFn: in,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not false", testCase{
+ s: `!f`,
+ inFn: in,
+ out: model.NewBoolValue(true),
+ }.run)
+ t.Run("not not false", testCase{
+ s: `!!f`,
+ inFn: in,
+ out: model.NewBoolValue(false),
+ }.run)
+ t.Run("not not not false", testCase{
+ s: `!!!f`,
+ inFn: in,
+ out: model.NewBoolValue(true),
+ }.run)
+ })
+ })
+}
diff --git a/execution/func.go b/execution/func.go
new file mode 100644
index 00000000..452059d5
--- /dev/null
+++ b/execution/func.go
@@ -0,0 +1,144 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+var (
+ // DefaultFuncCollection is the default collection of functions that can be executed.
+ DefaultFuncCollection = NewFuncCollection(
+ FuncLen,
+ FuncAdd,
+ FuncToString,
+ FuncToInt,
+ FuncToFloat,
+ FuncMerge,
+ FuncReverse,
+ FuncTypeOf,
+ FuncMax,
+ FuncMin,
+ FuncIgnore,
+ FuncBase64Encode,
+ FuncBase64Decode,
+ FuncParse,
+ )
+)
+
+// ArgsValidator is a function that validates the arguments passed to a function.
+type ArgsValidator func(name string, args model.Values) error
+
+// ValidateArgsExactly returns an ArgsValidator that validates that the number of arguments passed to a function is exactly the expected number.
+func ValidateArgsExactly(expected int) ArgsValidator {
+ return func(name string, args model.Values) error {
+ if len(args) == expected {
+ return nil
+ }
+ return fmt.Errorf("func %q expects exactly %d arguments, got %d", name, expected, len(args))
+ }
+}
+
+// ValidateArgsMin returns an ArgsValidator that validates that the number of arguments passed to a function is at least the expected number.
+func ValidateArgsMin(expected int) ArgsValidator {
+ return func(name string, args model.Values) error {
+ if len(args) >= expected {
+ return nil
+ }
+ return fmt.Errorf("func %q expects at least %d arguments, got %d", name, expected, len(args))
+ }
+}
+
+// ValidateArgsMax returns an ArgsValidator that validates that the number of arguments passed to a function is at most the expected number.
+func ValidateArgsMax(expected int) ArgsValidator {
+ return func(name string, args model.Values) error {
+ if len(args) <= expected {
+ return nil
+ }
+ return fmt.Errorf("func %q expects no more than %d arguments, got %d", name, expected, len(args))
+ }
+}
+
+// ValidateArgsMinMax returns an ArgsValidator that validates that the number of arguments passed to a function is between the min and max expected numbers.
+func ValidateArgsMinMax(min int, max int) ArgsValidator {
+ return func(name string, args model.Values) error {
+ if len(args) >= min && len(args) <= max {
+ return nil
+ }
+ return fmt.Errorf("func %q expects between %d and %d arguments, got %d", name, min, max, len(args))
+ }
+}
+
+// Func represents a function that can be executed.
+type Func struct {
+ name string
+ handler FuncFn
+ argsValidator ArgsValidator
+}
+
+// Handler returns a FuncFn that can be used to execute the function.
+func (f *Func) Handler() FuncFn {
+ return func(data *model.Value, args model.Values) (*model.Value, error) {
+ if f.argsValidator != nil {
+ if err := f.argsValidator(f.name, args); err != nil {
+ return nil, err
+ }
+ }
+ res, err := f.handler(data, args)
+ if err != nil {
+ return nil, fmt.Errorf("error execution func %q: %w", f.name, err)
+ }
+ return res, nil
+ }
+}
+
+// NewFunc creates a new Func.
+func NewFunc(name string, handler FuncFn, argsValidator ArgsValidator) *Func {
+ return &Func{
+ name: name,
+ handler: handler,
+ argsValidator: argsValidator,
+ }
+}
+
+// FuncFn is a function that can be executed.
+type FuncFn func(data *model.Value, args model.Values) (*model.Value, error)
+
+// FuncCollection is a collection of functions that can be executed.
+type FuncCollection map[string]FuncFn
+
+// NewFuncCollection creates a new FuncCollection with the given functions.
+func NewFuncCollection(funcs ...*Func) FuncCollection {
+ return FuncCollection{}.Register(funcs...)
+}
+
+// Register registers the given functions with the FuncCollection.
+func (fc FuncCollection) Register(funcs ...*Func) FuncCollection {
+ for _, f := range funcs {
+ fc[f.name] = f.Handler()
+ }
+ return fc
+}
+
+// Get returns the function with the given name.
+func (fc FuncCollection) Get(name string) (FuncFn, bool) {
+ fn, ok := fc[name]
+ return fn, ok
+}
+
+// Delete deletes the functions with the given names.
+func (fc FuncCollection) Delete(names ...string) FuncCollection {
+ for _, name := range names {
+ delete(fc, name)
+ }
+ return fc
+}
+
+// Copy returns a copy of the FuncCollection.
+func (fc FuncCollection) Copy() FuncCollection {
+ c := NewFuncCollection()
+ for k, v := range fc {
+ c[k] = v
+ }
+ return c
+}
diff --git a/execution/func_add.go b/execution/func_add.go
new file mode 100644
index 00000000..fdca815e
--- /dev/null
+++ b/execution/func_add.go
@@ -0,0 +1,43 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncAdd is a function that adds the given values together.
+var FuncAdd = NewFunc(
+ "add",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ var foundInts, foundFloats int
+ var intRes int64
+ var floatRes float64
+ for _, arg := range args {
+ if arg.IsFloat() {
+ foundFloats++
+ v, err := arg.FloatValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting float value: %w", err)
+ }
+ floatRes += v
+ continue
+ }
+ if arg.IsInt() {
+ foundInts++
+ v, err := arg.IntValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting int value: %w", err)
+ }
+ intRes += v
+ continue
+ }
+ return nil, fmt.Errorf("expected int or float, got %s", arg.Type())
+ }
+ if foundFloats > 0 {
+ return model.NewFloatValue(floatRes + float64(intRes)), nil
+ }
+ return model.NewIntValue(intRes), nil
+ },
+ ValidateArgsMin(1),
+)
diff --git a/execution/func_add_test.go b/execution/func_add_test.go
new file mode 100644
index 00000000..530e9303
--- /dev/null
+++ b/execution/func_add_test.go
@@ -0,0 +1,53 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestFuncAdd(t *testing.T) {
+ t.Run("int", testCase{
+ s: `add(1, 2, 3)`,
+ out: model.NewIntValue(6),
+ }.run)
+ t.Run("float", testCase{
+ s: `add(1f, 2.5, 3.5)`,
+ out: model.NewFloatValue(7),
+ }.run)
+ t.Run("mixed", testCase{
+ s: `add(1, 2f)`,
+ out: model.NewFloatValue(3),
+ }.run)
+ t.Run("properties", func(t *testing.T) {
+ in := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("numbers", orderedmap.NewMap().
+ Set("one", 1).
+ Set("two", 2).
+ Set("three", 3)).
+ Set("nums", []any{1, 2, 3}))
+ }
+ t.Run("nested props", testCase{
+ inFn: in,
+ s: `numbers.one + add(numbers.two, numbers.three)`,
+ out: model.NewIntValue(6),
+ }.run)
+ t.Run("add on end of chain", testCase{
+ inFn: in,
+ s: `numbers.one + numbers.add(two, three)`,
+ out: model.NewIntValue(6),
+ }.run)
+ t.Run("add with map and spread on slice with $this addition and grouping", testCase{
+ inFn: in,
+ s: `add(nums.map(($this + 1))...)`,
+ out: model.NewIntValue(9),
+ }.run)
+ t.Run("add with map and spread on slice with $this addition", testCase{
+ inFn: in,
+ s: `add(nums.map($this + 1 - 2)...)`,
+ out: model.NewIntValue(3),
+ }.run)
+ })
+}
diff --git a/execution/func_base64.go b/execution/func_base64.go
new file mode 100644
index 00000000..95b1466f
--- /dev/null
+++ b/execution/func_base64.go
@@ -0,0 +1,40 @@
+package execution
+
+import (
+ "encoding/base64"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncBase64Encode base64 encodes the given value.
+var FuncBase64Encode = NewFunc(
+ "base64e",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ arg := args[0]
+ strVal, err := arg.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ out := base64.StdEncoding.EncodeToString([]byte(strVal))
+ return model.NewStringValue(out), nil
+ },
+ ValidateArgsExactly(1),
+)
+
+// FuncBase64Decode base64 decodes the given value.
+var FuncBase64Decode = NewFunc(
+ "base64d",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ arg := args[0]
+ strVal, err := arg.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ out, err := base64.StdEncoding.DecodeString(strVal)
+ if err != nil {
+ return nil, err
+ }
+ return model.NewStringValue(string(out)), nil
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_ignore.go b/execution/func_ignore.go
new file mode 100644
index 00000000..351e9008
--- /dev/null
+++ b/execution/func_ignore.go
@@ -0,0 +1,15 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncIgnore is a function that ignores the value, causing it to be rejected from a branch.
+var FuncIgnore = NewFunc(
+ "ignore",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ data.MarkAsIgnore()
+ return data, nil
+ },
+ ValidateArgsExactly(0),
+)
diff --git a/execution/func_len.go b/execution/func_len.go
new file mode 100644
index 00000000..e11b14f6
--- /dev/null
+++ b/execution/func_len.go
@@ -0,0 +1,19 @@
+package execution
+
+import "github.com/tomwright/dasel/v3/model"
+
+// FuncLen is a function that returns the length of the given value.
+var FuncLen = NewFunc(
+ "len",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ arg := args[0]
+
+ l, err := arg.Len()
+ if err != nil {
+ return nil, err
+ }
+
+ return model.NewIntValue(int64(l)), nil
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_max.go b/execution/func_max.go
new file mode 100644
index 00000000..9de230c4
--- /dev/null
+++ b/execution/func_max.go
@@ -0,0 +1,32 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncMax is a function that returns the highest number.
+var FuncMax = NewFunc(
+ "max",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ res := model.NewNullValue()
+ for _, arg := range args {
+ if res.IsNull() {
+ res = arg
+ continue
+ }
+ gt, err := arg.GreaterThan(res)
+ if err != nil {
+ return nil, err
+ }
+ gtBool, err := gt.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ if gtBool {
+ res = arg
+ }
+ }
+ return res, nil
+ },
+ ValidateArgsMin(1),
+)
diff --git a/execution/func_max_test.go b/execution/func_max_test.go
new file mode 100644
index 00000000..d006368c
--- /dev/null
+++ b/execution/func_max_test.go
@@ -0,0 +1,22 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncMax(t *testing.T) {
+ t.Run("int", testCase{
+ s: `max(1, 2, 3)`,
+ out: model.NewIntValue(3),
+ }.run)
+ t.Run("float", testCase{
+ s: `max(1f, 2.5, 3.5)`,
+ out: model.NewFloatValue(3.5),
+ }.run)
+ t.Run("mixed", testCase{
+ s: `max(1, 2f)`,
+ out: model.NewFloatValue(2),
+ }.run)
+}
diff --git a/execution/func_merge.go b/execution/func_merge.go
new file mode 100644
index 00000000..b837d214
--- /dev/null
+++ b/execution/func_merge.go
@@ -0,0 +1,53 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncMerge is a function that merges two or more items together.
+var FuncMerge = NewFunc(
+ "merge",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ if len(args) == 1 {
+ return args[0], nil
+ }
+
+ expectedType := args[0].Type()
+
+ switch expectedType {
+ case model.TypeMap:
+ break
+ default:
+ return nil, fmt.Errorf("merge exects a map, found %s", expectedType)
+ }
+
+ // Validate types match
+ for _, a := range args {
+ if a.Type() != expectedType {
+ return nil, fmt.Errorf("merge expects all arguments to be of the same type. expected %s, got %s", expectedType.String(), a.Type().String())
+ }
+ }
+
+ base := model.NewMapValue()
+
+ for i := 0; i < len(args); i++ {
+ next := args[i]
+
+ nextKVs, err := next.MapKeyValues()
+ if err != nil {
+ return nil, fmt.Errorf("merge failed to extract key values for arg %d: %w", i, err)
+ }
+
+ for _, kv := range nextKVs {
+ if err := base.SetMapKey(kv.Key, kv.Value); err != nil {
+ return nil, fmt.Errorf("merge failed to set map key %s: %w", kv.Key, err)
+ }
+ }
+ }
+
+ return base, nil
+ },
+ ValidateArgsMin(1),
+)
diff --git a/execution/func_merge_test.go b/execution/func_merge_test.go
new file mode 100644
index 00000000..63262f2d
--- /dev/null
+++ b/execution/func_merge_test.go
@@ -0,0 +1,50 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncMerge(t *testing.T) {
+ t.Run("shallow", testCase{
+ inFn: func() *model.Value {
+ a := model.NewMapValue()
+ if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ b := model.NewMapValue()
+ if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ res := model.NewMapValue()
+ if err := res.SetMapKey("a", a); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if err := res.SetMapKey("b", b); err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ return res
+ },
+ s: `merge(a, b)`,
+ outFn: func() *model.Value {
+ b := model.NewMapValue()
+ if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return b
+ },
+ }.run)
+}
diff --git a/execution/func_min.go b/execution/func_min.go
new file mode 100644
index 00000000..e45af420
--- /dev/null
+++ b/execution/func_min.go
@@ -0,0 +1,32 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncMin is a function that returns the smalled number.
+var FuncMin = NewFunc(
+ "min",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ res := model.NewNullValue()
+ for _, arg := range args {
+ if res.IsNull() {
+ res = arg
+ continue
+ }
+ lt, err := arg.LessThan(res)
+ if err != nil {
+ return nil, err
+ }
+ ltBool, err := lt.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ if ltBool {
+ res = arg
+ }
+ }
+ return res, nil
+ },
+ ValidateArgsMin(1),
+)
diff --git a/execution/func_min_test.go b/execution/func_min_test.go
new file mode 100644
index 00000000..74f54bea
--- /dev/null
+++ b/execution/func_min_test.go
@@ -0,0 +1,22 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncMin(t *testing.T) {
+ t.Run("int", testCase{
+ s: `min(1, 2, 3)`,
+ out: model.NewIntValue(1),
+ }.run)
+ t.Run("float", testCase{
+ s: `min(1f, 2.5, 3.5)`,
+ out: model.NewFloatValue(1),
+ }.run)
+ t.Run("mixed", testCase{
+ s: `min(1, 2f)`,
+ out: model.NewIntValue(1),
+ }.run)
+}
diff --git a/execution/func_parse.go b/execution/func_parse.go
new file mode 100644
index 00000000..4214b2bd
--- /dev/null
+++ b/execution/func_parse.go
@@ -0,0 +1,42 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+// FuncParse parses the given data at runtime.
+var FuncParse = NewFunc(
+ "parse",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ var format parsing.Format
+ var content []byte
+ {
+ strVal, err := args[0].StringValue()
+ if err != nil {
+ return nil, err
+ }
+ format = parsing.Format(strVal)
+ }
+ {
+ strVal, err := args[1].StringValue()
+ if err != nil {
+ return nil, err
+ }
+ content = []byte(strVal)
+ }
+
+ reader, err := format.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := reader.Read(content)
+ if err != nil {
+ return nil, err
+ }
+
+ return doc, nil
+ },
+ ValidateArgsExactly(2),
+)
diff --git a/execution/func_reverse.go b/execution/func_reverse.go
new file mode 100644
index 00000000..17c1184c
--- /dev/null
+++ b/execution/func_reverse.go
@@ -0,0 +1,25 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncReverse is a function that reverses the input.
+var FuncReverse = NewFunc(
+ "reverse",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ arg := args[0]
+
+ switch arg.Type() {
+ case model.TypeString:
+ return arg.StringIndexRange(-1, 0)
+ case model.TypeSlice:
+ return arg.SliceIndexRange(-1, 0)
+ default:
+ return nil, fmt.Errorf("reverse expects a slice or string, got %s", arg.Type())
+ }
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_reverse_test.go b/execution/func_reverse_test.go
new file mode 100644
index 00000000..e907b88c
--- /dev/null
+++ b/execution/func_reverse_test.go
@@ -0,0 +1,31 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncReverse(t *testing.T) {
+ t.Run("array", testCase{
+ s: `reverse([1, 2, 3])`,
+ outFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewIntValue(3)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := res.Append(model.NewIntValue(2)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := res.Append(model.NewIntValue(1)); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return res
+ },
+ }.run)
+
+ t.Run("string", testCase{
+ s: `reverse("hello")`,
+ out: model.NewStringValue("olleh"),
+ }.run)
+}
diff --git a/execution/func_to_float.go b/execution/func_to_float.go
new file mode 100644
index 00000000..7a079908
--- /dev/null
+++ b/execution/func_to_float.go
@@ -0,0 +1,49 @@
+package execution
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncToFloat is a function that converts the given value to a string.
+var FuncToFloat = NewFunc(
+ "toFloat",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ switch args[0].Type() {
+ case model.TypeString:
+ stringValue, err := args[0].StringValue()
+ if err != nil {
+ return nil, err
+ }
+
+ i, err := strconv.ParseFloat(stringValue, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ return model.NewFloatValue(i), nil
+ case model.TypeInt:
+ i, err := args[0].IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return model.NewFloatValue(float64(i)), nil
+ case model.TypeFloat:
+ return args[0], nil
+ case model.TypeBool:
+ i, err := args[0].BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ if i {
+ return model.NewFloatValue(1), nil
+ }
+ return model.NewFloatValue(0), nil
+ default:
+ return nil, fmt.Errorf("cannot convert %s to float", args[0].Type())
+ }
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_to_int.go b/execution/func_to_int.go
new file mode 100644
index 00000000..b8a90eb5
--- /dev/null
+++ b/execution/func_to_int.go
@@ -0,0 +1,49 @@
+package execution
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncToInt is a function that converts the given value to a string.
+var FuncToInt = NewFunc(
+ "toInt",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ switch args[0].Type() {
+ case model.TypeString:
+ stringValue, err := args[0].StringValue()
+ if err != nil {
+ return nil, err
+ }
+
+ i, err := strconv.ParseInt(stringValue, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ return model.NewIntValue(i), nil
+ case model.TypeInt:
+ return args[0], nil
+ case model.TypeFloat:
+ i, err := args[0].FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return model.NewIntValue(int64(i)), nil
+ case model.TypeBool:
+ i, err := args[0].BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ if i {
+ return model.NewIntValue(1), nil
+ }
+ return model.NewIntValue(0), nil
+ default:
+ return nil, fmt.Errorf("cannot convert %s to int", args[0].Type())
+ }
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_to_string.go b/execution/func_to_string.go
new file mode 100644
index 00000000..0409f37d
--- /dev/null
+++ b/execution/func_to_string.go
@@ -0,0 +1,44 @@
+package execution
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncToString is a function that converts the given value to a string.
+var FuncToString = NewFunc(
+ "toString",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ switch args[0].Type() {
+ case model.TypeString:
+ stringValue, err := args[0].StringValue()
+ if err != nil {
+ return nil, err
+ }
+ model.NewStringValue(stringValue)
+ return args[0], nil
+ case model.TypeInt:
+ i, err := args[0].IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return model.NewStringValue(fmt.Sprintf("%d", i)), nil
+ case model.TypeFloat:
+ i, err := args[0].FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return model.NewStringValue(fmt.Sprintf("%g", i)), nil
+ case model.TypeBool:
+ i, err := args[0].BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return model.NewStringValue(fmt.Sprintf("%v", i)), nil
+ default:
+ return nil, fmt.Errorf("cannot convert %s to string", args[0].Type())
+ }
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_type_of.go b/execution/func_type_of.go
new file mode 100644
index 00000000..54258054
--- /dev/null
+++ b/execution/func_type_of.go
@@ -0,0 +1,14 @@
+package execution
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+)
+
+// FuncTypeOf is a function that returns the type of the first argument as a string.
+var FuncTypeOf = NewFunc(
+ "typeOf",
+ func(data *model.Value, args model.Values) (*model.Value, error) {
+ return model.NewStringValue(args[0].Type().String()), nil
+ },
+ ValidateArgsExactly(1),
+)
diff --git a/execution/func_type_of_test.go b/execution/func_type_of_test.go
new file mode 100644
index 00000000..d2e8e876
--- /dev/null
+++ b/execution/func_type_of_test.go
@@ -0,0 +1,38 @@
+package execution_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestFuncTypeOf(t *testing.T) {
+ t.Run("string", testCase{
+ s: `typeOf("hello")`,
+ out: model.NewStringValue("string"),
+ }.run)
+ t.Run("int", testCase{
+ s: `typeOf(123)`,
+ out: model.NewStringValue("int"),
+ }.run)
+ t.Run("float", testCase{
+ s: `typeOf(12.3)`,
+ out: model.NewStringValue("float"),
+ }.run)
+ t.Run("bool", testCase{
+ s: `typeOf(true)`,
+ out: model.NewStringValue("bool"),
+ }.run)
+ t.Run("array", testCase{
+ s: `typeOf([])`,
+ out: model.NewStringValue("array"),
+ }.run)
+ t.Run("map", testCase{
+ s: `typeOf({})`,
+ out: model.NewStringValue("map"),
+ }.run)
+ t.Run("null", testCase{
+ s: `typeOf(null)`,
+ out: model.NewStringValue("null"),
+ }.run)
+}
diff --git a/execution/options.go b/execution/options.go
new file mode 100644
index 00000000..9a286dfb
--- /dev/null
+++ b/execution/options.go
@@ -0,0 +1,56 @@
+package execution
+
+import "github.com/tomwright/dasel/v3/model"
+
+// ExecuteOptionFn is a function that can be used to set options on the execution of the selector.
+type ExecuteOptionFn func(*Options)
+
+// Options contains the options for the execution of the selector.
+type Options struct {
+ Funcs FuncCollection
+ Vars map[string]*model.Value
+ Unstable bool
+}
+
+// NewOptions creates a new Options struct with the given options.
+func NewOptions(opts ...ExecuteOptionFn) *Options {
+ o := &Options{
+ Funcs: DefaultFuncCollection,
+ Vars: map[string]*model.Value{},
+ }
+ for _, opt := range opts {
+ if opt == nil {
+ continue
+ }
+ opt(o)
+ }
+ return o
+}
+
+// WithFuncs sets the functions that can be used in the selector.
+func WithFuncs(fc FuncCollection) ExecuteOptionFn {
+ return func(o *Options) {
+ o.Funcs = fc
+ }
+}
+
+// WithVariable sets a variable for use in the selector.
+func WithVariable(key string, val *model.Value) ExecuteOptionFn {
+ return func(o *Options) {
+ o.Vars[key] = val
+ }
+}
+
+// WithUnstable allows access to potentially unstable features.
+func WithUnstable() ExecuteOptionFn {
+ return func(o *Options) {
+ o.Unstable = true
+ }
+}
+
+// WithoutUnstable disallows access to potentially unstable features.
+func WithoutUnstable() ExecuteOptionFn {
+ return func(o *Options) {
+ o.Unstable = false
+ }
+}
diff --git a/func.go b/func.go
deleted file mode 100644
index 7f01e2a8..00000000
--- a/func.go
+++ /dev/null
@@ -1,208 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
- "strings"
-)
-
-type ErrUnknownFunction struct {
- Function string
-}
-
-func (e ErrUnknownFunction) Error() string {
- return fmt.Sprintf("unknown function: %s", e.Function)
-}
-
-func (e ErrUnknownFunction) Is(other error) bool {
- _, ok := other.(*ErrUnknownFunction)
- return ok
-}
-
-type ErrUnexpectedFunctionArgs struct {
- Function string
- Args []string
- Message string
-}
-
-func (e ErrUnexpectedFunctionArgs) Error() string {
- return fmt.Sprintf("unexpected function args: %s(%s): %s", e.Function, strings.Join(e.Args, ", "), e.Message)
-}
-
-func (e ErrUnexpectedFunctionArgs) Is(other error) bool {
- o, ok := other.(*ErrUnexpectedFunctionArgs)
- if !ok {
- return false
- }
- if o.Function != "" && o.Function != e.Function {
- return false
- }
- if o.Message != "" && o.Message != e.Message {
- return false
- }
- if o.Args != nil && !reflect.DeepEqual(o.Args, e.Args) {
- return false
- }
- return true
-}
-
-func standardFunctions() *FunctionCollection {
- collection := &FunctionCollection{}
- collection.Add(
- // Generic
- ThisFunc,
- LenFunc,
- KeyFunc,
- KeysFunc,
- MergeFunc,
- CountFunc,
- MapOfFunc,
- TypeFunc,
- JoinFunc,
- StringFunc,
- NullFunc,
-
- // Selectors
- IndexFunc,
- AllFunc,
- FirstFunc,
- LastFunc,
- PropertyFunc,
- AppendFunc,
- OrDefaultFunc,
-
- // Filters
- FilterFunc,
- FilterOrFunc,
-
- // Comparisons
- EqualFunc,
- MoreThanFunc,
- LessThanFunc,
- AndFunc,
- OrFunc,
- NotFunc,
-
- // Metadata
- MetadataFunc,
- ParentFunc,
- )
- return collection
-}
-
-// SelectorFunc is a function that can be executed in a selector.
-type SelectorFunc func(c *Context, step *Step, args []string) (Values, error)
-
-type FunctionCollection struct {
- functions []Function
-}
-
-func (fc *FunctionCollection) ParseSelector(part string) *Selector {
- for _, f := range fc.functions {
- if s := f.AlternativeSelector(part); s != nil {
- return s
- }
- }
- return nil
-}
-
-func (fc *FunctionCollection) Add(fs ...Function) {
- fc.functions = append(fc.functions, fs...)
-}
-
-func (fc *FunctionCollection) GetAll() map[string]SelectorFunc {
- res := make(map[string]SelectorFunc)
- for _, f := range fc.functions {
- res[f.Name()] = f.Run
- }
- return res
-}
-
-func (fc *FunctionCollection) Get(name string) (SelectorFunc, error) {
- if f, ok := fc.GetAll()[name]; ok {
- return f, nil
- }
- return nil, &ErrUnknownFunction{Function: name}
-}
-
-type Function interface {
- Name() string
- Run(c *Context, s *Step, args []string) (Values, error)
- AlternativeSelector(part string) *Selector
-}
-
-type BasicFunction struct {
- name string
- runFn func(c *Context, s *Step, args []string) (Values, error)
- alternativeSelectorFn func(part string) *Selector
-}
-
-func (bf BasicFunction) Name() string {
- return bf.name
-}
-
-func (bf BasicFunction) Run(c *Context, s *Step, args []string) (Values, error) {
- return bf.runFn(c, s, args)
-}
-
-func (bf BasicFunction) AlternativeSelector(part string) *Selector {
- if bf.alternativeSelectorFn == nil {
- return nil
- }
- return bf.alternativeSelectorFn(part)
-}
-
-func requireNoArgs(name string, args []string) error {
- if len(args) > 0 {
- return &ErrUnexpectedFunctionArgs{
- Function: name,
- Args: args,
- Message: "0 arguments expected",
- }
- }
- return nil
-}
-
-func requireExactlyXArgs(name string, args []string, x int) error {
- if len(args) != x {
- return &ErrUnexpectedFunctionArgs{
- Function: name,
- Args: args,
- Message: fmt.Sprintf("exactly %d arguments expected", x),
- }
- }
- return nil
-}
-
-func requireXOrMoreArgs(name string, args []string, x int) error {
- if len(args) < x {
- return &ErrUnexpectedFunctionArgs{
- Function: name,
- Args: args,
- Message: fmt.Sprintf("expected %d or more arguments", x),
- }
- }
- return nil
-}
-
-func requireXOrLessArgs(name string, args []string, x int) error {
- if len(args) > x {
- return &ErrUnexpectedFunctionArgs{
- Function: name,
- Args: args,
- Message: fmt.Sprintf("expected %d or less arguments", x),
- }
- }
- return nil
-}
-
-func requireModulusXArgs(name string, args []string, x int) error {
- if len(args)%x != 0 {
- return &ErrUnexpectedFunctionArgs{
- Function: name,
- Args: args,
- Message: fmt.Sprintf("expected arguments in groups of %d", x),
- }
- }
- return nil
-}
diff --git a/func_all.go b/func_all.go
deleted file mode 100644
index 7220a3cf..00000000
--- a/func_all.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/dencoding"
- "reflect"
-)
-
-var AllFunc = BasicFunction{
- name: "all",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("all", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- switch val.Kind() {
- case reflect.String:
- for _, r := range val.String() {
- res = append(res, ValueOf(string(r)))
- }
- case reflect.Slice, reflect.Array:
- for i := 0; i < val.Len(); i++ {
- res = append(res, val.Index(i))
- }
- case reflect.Map:
- for _, key := range val.MapKeys() {
- res = append(res, val.MapIndex(key))
- }
- default:
- if val.IsDencodingMap() {
- for _, k := range val.Interface().(*dencoding.Map).Keys() {
- res = append(res, val.dencodingMapIndex(ValueOf(k)))
- }
- } else {
- return nil, fmt.Errorf("cannot use all selector on non slice/array/map types")
- }
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_all_test.go b/func_all_test.go
deleted file mode 100644
index aca27874..00000000
--- a/func_all_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import "testing"
-
-func TestAllFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "all(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "all",
- Args: []string{"x"},
- }),
- )
-
- t.Run(
- "RootAllSlice",
- selectTest(
- "all()",
- []interface{}{"red", "green", "blue"},
- []interface{}{"red", "green", "blue"},
- ),
- )
- t.Run(
- "NestedAllSlice",
- selectTest(
- "colours.all()",
- map[string]interface{}{
- "colours": []interface{}{"red", "green", "blue"},
- },
- []interface{}{"red", "green", "blue"},
- ),
- )
- t.Run(
- "AllString",
- selectTest(
- "all()",
- "asd",
- []interface{}{"a", "s", "d"},
- ),
- )
-}
diff --git a/func_and.go b/func_and.go
deleted file mode 100644
index 899ce011..00000000
--- a/func_and.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var AndFunc = BasicFunction{
- name: "and",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("and", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- runComparison := func(value Value, selector string) (bool, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("and expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- return IsTruthy(gotValues[0]), nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := true
- for _, cmp := range args {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if !pass {
- valPassed = false
- break
- }
- }
- res = append(res, Value{Value: reflect.ValueOf(valPassed)})
- }
-
- return res, nil
- },
-}
diff --git a/func_and_test.go b/func_and_test.go
deleted file mode 100644
index 8a03eb93..00000000
--- a/func_and_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestAndFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "and()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "and",
- Args: []string{},
- }),
- )
-
- t.Run(
- "NoneEqualMoreThan",
- selectTest(
- "numbers.all().and(equal(.,2),moreThan(.,2))",
- map[string]interface{}{
- "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- },
- []interface{}{
- false, false, false, false, false, false, false, false, false, false,
- },
- ),
- )
- t.Run(
- "SomeEqualMoreThan",
- selectTest(
- "numbers.all().and(equal(.,4),moreThan(.,2))",
- map[string]interface{}{
- "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- },
- []interface{}{
- false, false, false, false, true, false, false, false, false, false,
- },
- ),
- )
-}
diff --git a/func_append.go b/func_append.go
deleted file mode 100644
index 258d2728..00000000
--- a/func_append.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var AppendFunc = BasicFunction{
- name: "append",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("append", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- if c.CreateWhenMissing() {
- input = input.initEmptySlices()
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- switch val.Kind() {
- case reflect.Slice, reflect.Array:
- val = val.Append()
- value := val.Index(val.Len() - 1)
- res = append(res, value)
- default:
- return nil, fmt.Errorf("cannot use append selector on non slice/array types")
- }
- }
-
- return res, nil
- },
- alternativeSelectorFn: func(part string) *Selector {
- if part == "[]" {
- return &Selector{
- funcName: "append",
- funcArgs: []string{},
- }
- }
- return nil
- },
-}
diff --git a/func_count.go b/func_count.go
deleted file mode 100644
index 0da7d2b2..00000000
--- a/func_count.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package dasel
-
-var CountFunc = BasicFunction{
- name: "count",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- input := s.inputs()
-
- return Values{
- ValueOf(len(input)),
- }, nil
- },
-}
diff --git a/func_count_test.go b/func_count_test.go
deleted file mode 100644
index d10af778..00000000
--- a/func_count_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestCountFunc(t *testing.T) {
- data := map[string]interface{}{
- "string": "hello",
- "slice": []interface{}{
- 1, 2, 3,
- },
- "falseBool": false,
- "trueBool": true,
- }
-
- t.Run(
- "RootObject",
- selectTest(
- "count()",
- data,
- []interface{}{1},
- ),
- )
- t.Run(
- "All",
- selectTest(
- "all().count()",
- data,
- []interface{}{4},
- ),
- )
- t.Run(
- "NestedAll",
- selectTest(
- "slice.all().count()",
- data,
- []interface{}{3},
- ),
- )
-}
diff --git a/func_equal.go b/func_equal.go
deleted file mode 100644
index beec504f..00000000
--- a/func_equal.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/util"
- "reflect"
-)
-
-var EqualFunc = BasicFunction{
- name: "equal",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("equal", args, 2); err != nil {
- return nil, err
- }
- if err := requireModulusXArgs("equal", args, 2); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- type comparison struct {
- selector string
- value string
- }
-
- comparisons := make([]comparison, 0)
-
- currentComparison := comparison{}
-
- for i, v := range args {
- switch i % 2 {
- case 0:
- currentComparison.selector = v
- case 1:
- currentComparison.value = v
- comparisons = append(comparisons, currentComparison)
- currentComparison = comparison{}
- }
- }
-
- runComparison := func(value Value, cmp comparison) (bool, error) {
- gotValues, err := c.subSelect(value, cmp.selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("equal expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- gotValue := util.ToString(gotValues[0].Interface())
- return gotValue == cmp.value, nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := true
- for _, cmp := range comparisons {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if !pass {
- valPassed = false
- break
- }
- }
- res = append(res, Value{Value: reflect.ValueOf(valPassed)})
- }
-
- return res, nil
- },
-}
diff --git a/func_equal_test.go b/func_equal_test.go
deleted file mode 100644
index 0e2a7220..00000000
--- a/func_equal_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestEqualFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "equal()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "equal",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Single Equal",
- selectTest(
- "name.all().equal(key(),first)",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- true,
- false,
- },
- ),
- )
-
- t.Run(
- "Multi Equal",
- selectTest(
- "name.all().equal(key(),first,key(),first)",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- true,
- false,
- },
- ),
- )
-
- t.Run(
- "Single Equal Optional Field",
- selectTest(
- "all().equal(primary,true)",
- []interface{}{
- map[string]interface{}{
- "name": "red",
- "hex": "ff0000",
- "primary": true,
- },
- map[string]interface{}{
- "name": "green",
- "hex": "00ff00",
- "primary": true,
- },
- map[string]interface{}{
- "name": "blue",
- "hex": "0000ff",
- "primary": true,
- },
- map[string]interface{}{
- "name": "orange",
- "hex": "ffa500",
- "primary": false,
- },
- },
- []interface{}{
- true, true, true, false,
- },
- ),
- )
-}
diff --git a/func_filter.go b/func_filter.go
deleted file mode 100644
index 4888d7df..00000000
--- a/func_filter.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package dasel
-
-import (
- "fmt"
-)
-
-var FilterFunc = BasicFunction{
- name: "filter",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("filter", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- runComparison := func(value Value, selector string) (bool, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("filter expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- return IsTruthy(gotValues[0]), nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := true
- for _, cmp := range args {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if !pass {
- valPassed = false
- break
- }
- }
- if valPassed {
- res = append(res, val)
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_filter_or.go b/func_filter_or.go
deleted file mode 100644
index 6549df21..00000000
--- a/func_filter_or.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package dasel
-
-import (
- "fmt"
-)
-
-var FilterOrFunc = BasicFunction{
- name: "filterOr",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("filterOr", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- runComparison := func(value Value, selector string) (bool, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("filter expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- return IsTruthy(gotValues[0]), nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := false
- for _, cmp := range args {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if pass {
- valPassed = true
- break
- }
- }
- if valPassed {
- res = append(res, val)
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_filter_or_test.go b/func_filter_or_test.go
deleted file mode 100644
index 5f4a74bd..00000000
--- a/func_filter_or_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestFilterOrFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "filterOr()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "filterOr",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Filter Equal Key",
- selectTest(
- "name.all().filterOr(equal(key(),first))",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
-
- t.Run(
- "Multiple Filter Or Equal Key",
- selectTest(
- "name.all().filterOr(equal(key(),first),equal(key(),last))",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- "Wright",
- },
- ),
- )
-
- t.Run(
- "MoreThanEqual",
- selectTest(
- "nums.all().filterOr(moreThan(.,3),equal(.,3))",
- map[string]interface{}{
- "nums": []interface{}{0, 1, 2, 3, 4, 5},
- },
- []interface{}{3, 4, 5},
- ),
- )
-
- t.Run(
- "LessThanEqual",
- selectTest(
- "nums.all().filterOr(lessThan(.,3),equal(.,3))",
- map[string]interface{}{
- "nums": []interface{}{0, 1, 2, 3, 4, 5},
- },
- []interface{}{0, 1, 2, 3},
- ),
- )
-}
diff --git a/func_filter_test.go b/func_filter_test.go
deleted file mode 100644
index 2eb33d27..00000000
--- a/func_filter_test.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestFilterFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "filter()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "filter",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Filter Equal Key",
- selectTest(
- "name.all().filter(equal(key(),first))",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
-
- t.Run(
- "Multiple Filter Equal Key",
- selectTest(
- "name.all().filter(equal(key(),first),equal(key(),last))",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{},
- ),
- )
-
- t.Run(
- "Filter Equal Prop",
- selectTest(
- "all().filter(equal(primary,true)).name",
- []interface{}{
- map[string]interface{}{
- "name": "red",
- "hex": "ff0000",
- "primary": true,
- },
- map[string]interface{}{
- "name": "green",
- "hex": "00ff00",
- "primary": true,
- },
- map[string]interface{}{
- "name": "blue",
- "hex": "0000ff",
- "primary": true,
- },
- map[string]interface{}{
- "name": "orange",
- "hex": "ffa500",
- "primary": false,
- },
- },
- []interface{}{
- "red", "green", "blue",
- },
- ),
- )
-
- t.Run(
- "FilterNestedProp",
- selectTest(
- "all().filter(equal(flags.banned,false)).name",
- []map[string]interface{}{
- {
- "flags": map[string]interface{}{
- "banned": false,
- },
- "name": "Tom",
- },
- {
- "flags": map[string]interface{}{
- "banned": true,
- },
- "name": "Jim",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
-
- t.Run(
- "Filter And",
- selectTest(
- "all().filter(and(equal(primary,true),equal(name,red))).name",
- []interface{}{
- map[string]interface{}{
- "name": "red",
- "hex": "ff0000",
- "primary": true,
- },
- map[string]interface{}{
- "name": "green",
- "hex": "00ff00",
- "primary": true,
- },
- map[string]interface{}{
- "name": "blue",
- "hex": "0000ff",
- "primary": true,
- },
- map[string]interface{}{
- "name": "orange",
- "hex": "ffa500",
- "primary": false,
- },
- },
- []interface{}{
- "red",
- },
- ),
- )
-
- t.Run(
- "Filter And",
- selectTest(
- "all().filter(and(equal(primary,true),equal(name,orange))).name",
- []interface{}{
- map[string]interface{}{
- "name": "red",
- "hex": "ff0000",
- "primary": true,
- },
- map[string]interface{}{
- "name": "green",
- "hex": "00ff00",
- "primary": true,
- },
- map[string]interface{}{
- "name": "blue",
- "hex": "0000ff",
- "primary": true,
- },
- map[string]interface{}{
- "name": "orange",
- "hex": "ffa500",
- "primary": false,
- },
- },
- []interface{}{},
- ),
- )
-
- t.Run(
- "Filter Or",
- selectTest(
- "all().filter(or(equal(primary,true),equal(name,orange))).name",
- []interface{}{
- map[string]interface{}{
- "name": "red",
- "hex": "ff0000",
- "primary": true,
- },
- map[string]interface{}{
- "name": "green",
- "hex": "00ff00",
- "primary": true,
- },
- map[string]interface{}{
- "name": "blue",
- "hex": "0000ff",
- "primary": true,
- },
- map[string]interface{}{
- "name": "orange",
- "hex": "ffa500",
- "primary": false,
- },
- },
- []interface{}{
- "red", "green", "blue", "orange",
- },
- ),
- )
-}
diff --git a/func_first.go b/func_first.go
deleted file mode 100644
index d2210001..00000000
--- a/func_first.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var FirstFunc = BasicFunction{
- name: "first",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("first", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- switch val.Kind() {
- case reflect.Slice, reflect.Array:
- if val.Len() == 0 {
- return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: 0})
- }
- value := val.Index(0)
- res = append(res, value)
- default:
- return nil, fmt.Errorf("cannot use first selector on non slice/array types: %w", &ErrIndexNotFound{Index: 0})
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_first_test.go b/func_first_test.go
deleted file mode 100644
index 4924f3de..00000000
--- a/func_first_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestFirstFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "first(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "first",
- Args: []string{"x"},
- }),
- )
-
- t.Run("NotFound", selectTestErr(
- "first()",
- []interface{}{},
- &ErrIndexNotFound{
- Index: 0,
- }),
- )
-
- t.Run("NotFoundOnInvalidType", selectTestErr(
- "x.first()",
- map[string]interface{}{"x": "y"},
- &ErrIndexNotFound{
- Index: 0,
- }),
- )
-
- original := map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "colours": []interface{}{
- "red", "green", "blue",
- },
- }
-
- t.Run(
- "First",
- selectTest(
- "colours.first()",
- original,
- []interface{}{
- "red",
- },
- ),
- )
-}
diff --git a/func_index.go b/func_index.go
deleted file mode 100644
index f1bc4a7e..00000000
--- a/func_index.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
- "strconv"
- "strings"
-)
-
-type ErrIndexNotFound struct {
- Index int
-}
-
-func (e ErrIndexNotFound) Error() string {
- return fmt.Sprintf("index not found: %d", e.Index)
-}
-
-func (e ErrIndexNotFound) Is(other error) bool {
- o, ok := other.(*ErrIndexNotFound)
- if !ok {
- return false
- }
- if o.Index >= 0 && o.Index != e.Index {
- return false
- }
- return true
-}
-
-var IndexFunc = BasicFunction{
- name: "index",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("index", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- for _, indexStr := range args {
- isOptional := strings.HasSuffix(indexStr, "?")
- if isOptional {
- indexStr = strings.TrimSuffix(indexStr, "?")
- }
-
- index, err := strconv.Atoi(indexStr)
- if err != nil {
- if isOptional {
- continue
- }
- return nil, fmt.Errorf("invalid index: %w", err)
- }
-
- switch val.Kind() {
- case reflect.String:
- runes := []rune(val.String())
- if index < 0 || index > len(runes)-1 {
- if isOptional {
- continue
- }
- return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index})
- }
- res = append(res, ValueOf(string(runes[index])))
- case reflect.Slice, reflect.Array:
- if index < 0 || index > val.Len()-1 {
- if isOptional {
- continue
- }
- return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index})
- }
- value := val.Index(index)
- res = append(res, value)
- default:
- return nil, fmt.Errorf("cannot use index selector on non slice/array types: %w", &ErrIndexNotFound{Index: index})
- }
- }
- }
-
- return res, nil
- },
- alternativeSelectorFn: func(part string) *Selector {
- if part != "[]" && strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") {
- strings.Split(strings.TrimPrefix(strings.TrimSuffix(part, "]"), "["), ",")
- return &Selector{
- funcName: "index",
- funcArgs: strings.Split(strings.TrimPrefix(strings.TrimSuffix(part, "]"), "["), ","),
- }
- }
- return nil
- },
-}
diff --git a/func_index_test.go b/func_index_test.go
deleted file mode 100644
index 6f25054b..00000000
--- a/func_index_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestIndexFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "index()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "index",
- Args: []string{},
- }),
- )
-
- t.Run("NotFound", selectTestErr(
- "[0]",
- []interface{}{},
- &ErrIndexNotFound{
- Index: 0,
- }),
- )
-
- t.Run("NotFoundOnInvalidType", selectTestErr(
- "[0]",
- map[string]interface{}{},
- &ErrIndexNotFound{
- Index: 0,
- }),
- )
-
- original := map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "colours": []interface{}{
- "red", "green", "blue",
- },
- }
-
- t.Run(
- "Index",
- selectTest(
- "colours.index(1)",
- original,
- []interface{}{
- "green",
- },
- ),
- )
-
- t.Run(
- "IndexString",
- selectTest(
- "colours.index(1).index(1)",
- original,
- []interface{}{
- "r",
- },
- ),
- )
-
- t.Run(
- "IndexMulti",
- selectTest(
- "colours.index(0,1,2)",
- original,
- []interface{}{
- "red",
- "green",
- "blue",
- },
- ),
- )
-
- t.Run(
- "IndexShorthand",
- selectTest(
- "colours.[1]",
- original,
- []interface{}{
- "green",
- },
- ),
- )
-
- t.Run(
- "IndexShorthandMulti",
- selectTest(
- "colours.[0,1,2]",
- original,
- []interface{}{
- "red",
- "green",
- "blue",
- },
- ),
- )
-}
diff --git a/func_join.go b/func_join.go
deleted file mode 100644
index cdc56e19..00000000
--- a/func_join.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package dasel
-
-import (
- "github.com/tomwright/dasel/v2/util"
- "strings"
-)
-
-var JoinFunc = BasicFunction{
- name: "join",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("join", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- getValues := func(value Value, selector string) ([]string, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return []string{}, err
- }
-
- res := make([]string, len(gotValues))
- for k, v := range gotValues {
- res[k] = util.ToString(v.Interface())
- }
- return res, nil
- }
-
- res := make(Values, 0)
-
- separator := args[0]
- args = args[1:]
-
- // No args - join all input values
- if len(args) == 0 {
- values := make([]string, len(input))
- for k, v := range input {
- values[k] = util.ToString(v.Interface())
- }
- res = append(res, ValueOf(strings.Join(values, separator)))
- return res, nil
- }
-
- // There are args - use each as a selector and join any resulting values.
- values := make([]string, 0)
- for _, val := range input {
- for _, cmp := range args {
- vals, err := getValues(val, cmp)
- if err != nil {
- return nil, err
- }
- values = append(values, vals...)
- }
- }
- res = append(res, ValueOf(strings.Join(values, separator)))
-
- return res, nil
- },
-}
diff --git a/func_join_test.go b/func_join_test.go
deleted file mode 100644
index f2b3d3c9..00000000
--- a/func_join_test.go
+++ /dev/null
@@ -1,212 +0,0 @@
-package dasel
-
-import (
- "github.com/tomwright/dasel/v2/dencoding"
- "strings"
- "testing"
-)
-
-func TestJoinFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "join()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "join",
- Args: []string{},
- }),
- )
-
- original := dencoding.NewMap().
- Set("name", dencoding.NewMap().
- Set("first", "Tom").
- Set("last", "Wright")).
- Set("colours", []interface{}{
- "red", "green", "blue",
- })
-
- t.Run(
- "JoinCommaSeparator",
- selectTestAssert(
- "name.all().join(\\,)",
- original,
- func(t *testing.T, got []any) {
- required := []string{"Tom", "Wright"}
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- str, ok := got[0].(string)
- if !ok {
- t.Errorf("expected 1st result to be a string, got %T", got[0])
- return
- }
-
- gotStrs := strings.Split(str, ",")
- for _, req := range required {
- found := false
- for _, got := range gotStrs {
- if got == req {
- found = true
- continue
- }
- }
- if !found {
- t.Errorf("expected %v, got %v", required, got)
- }
- }
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- },
- ),
- )
-
- t.Run(
- "JoinNewlineSeparator",
- selectTestAssert(
- "name.all().join(\\\n)",
- original,
- func(t *testing.T, got []any) {
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- str, ok := got[0].(string)
- if !ok {
- t.Errorf("expected 1st result to be a string, got %T", got[0])
- return
- }
-
- exp := "Tom\nWright"
- if exp != str {
- t.Errorf("expected %v, got %v", exp, str)
- return
- }
-
- //gotStrs := strings.Split(str, ",")
- //for _, req := range required {
- // found := false
- // for _, got := range gotStrs {
- // if got == req {
- // found = true
- // continue
- // }
- // }
- // if !found {
- // t.Errorf("expected %v, got %v", required, got)
- // }
- //}
- //if len(got) != 1 {
- // t.Errorf("expected 1 result, got %v", got)
- // return
- //}
- },
- ),
- )
-
- t.Run(
- "JoinSpaceSeparator",
- selectTestAssert(
- "name.all().join( )",
- original,
- func(t *testing.T, got []any) {
- required := []string{"Tom", "Wright"}
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- str, ok := got[0].(string)
- if !ok {
- t.Errorf("expected 1st result to be a string, got %T", got[0])
- return
- }
-
- gotStrs := strings.Split(str, " ")
- for _, req := range required {
- found := false
- for _, got := range gotStrs {
- if got == req {
- found = true
- continue
- }
- }
- if !found {
- t.Errorf("expected %v, got %v", required, got)
- }
- }
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- },
- ),
- )
-
- t.Run(
- "JoinWithSeparatorsAndSelectors",
- selectTest(
- "name.join( ,last,first)",
- original,
- []interface{}{
- "Wright Tom",
- },
- ),
- )
-
- t.Run(
- "JoinInMap",
- selectTest(
- "mapOf(first,name.first,last,name.last,full,name.join( ,string(Mr),first,last))",
- original,
- []interface{}{
- map[string]interface{}{
- "first": "Tom",
- "full": "Mr Tom Wright",
- "last": "Wright",
- },
- },
- ),
- )
-
- t.Run(
- "JoinManyLists",
- selectTestAssert(
- "all().join(\\,,all())",
- dencoding.NewMap().
- Set("x", []interface{}{1, 2, 3}).
- Set("y", []interface{}{4, 5, 6}).
- Set("z", []interface{}{7, 8, 9}),
- func(t *testing.T, got []any) {
- required := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- str, ok := got[0].(string)
- if !ok {
- t.Errorf("expected 1st result to be a string, got %T", got[0])
- return
- }
-
- gotStrs := strings.Split(str, ",")
- for _, req := range required {
- found := false
- for _, got := range gotStrs {
- if got == req {
- found = true
- continue
- }
- }
- if !found {
- t.Errorf("expected %v, got %v", required, got)
- }
- }
- if len(got) != 1 {
- t.Errorf("expected 1 result, got %v", got)
- return
- }
- },
- ),
- )
-}
diff --git a/func_key.go b/func_key.go
deleted file mode 100644
index 96cd5062..00000000
--- a/func_key.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package dasel
-
-var KeyFunc = BasicFunction{
- name: "key",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("key", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, i := range input {
- p := i.Metadata("key")
- if p == nil {
- continue
- }
- res = append(res, ValueOf(p))
- }
-
- return res, nil
- },
-}
diff --git a/func_keys.go b/func_keys.go
deleted file mode 100644
index 6579b77e..00000000
--- a/func_keys.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/dencoding"
- "reflect"
- "sort"
- "strings"
-)
-
-type ErrInvalidType struct {
- ExpectedTypes []string
- CurrentType string
-}
-
-func (e *ErrInvalidType) Error() string {
- return fmt.Sprintf("unexpected types: expect %s, get %s", strings.Join(e.ExpectedTypes, " "), e.CurrentType)
-}
-
-func (e *ErrInvalidType) Is(other error) bool {
- o, ok := other.(*ErrInvalidType)
- if !ok {
- return false
- }
- if len(e.ExpectedTypes) != len(o.ExpectedTypes) {
- return false
- }
- if e.CurrentType != o.CurrentType {
- return false
- }
- for i, t := range e.ExpectedTypes {
- if t != o.ExpectedTypes[i] {
- return false
- }
- }
- return true
-}
-
-var KeysFunc = BasicFunction{
- name: "keys",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("keys", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, len(input))
-
- for i, val := range input {
- switch val.Kind() {
- case reflect.Slice, reflect.Array:
- list := make([]any, 0, val.Len())
-
- for i := 0; i < val.Len(); i++ {
- list = append(list, i)
- }
-
- res[i] = ValueOf(list)
- case reflect.Map:
- keys := val.MapKeys()
-
- // we expect map keys to be string first so that we can sort them
- list, ok := getStringList(keys)
- if !ok {
- list = getAnyList(keys)
- }
-
- res[i] = ValueOf(list)
- default:
- if val.IsDencodingMap() {
- dencodingMap := val.Interface().(*dencoding.Map)
- mapKeys := dencodingMap.Keys()
- list := make([]any, 0, len(mapKeys))
- for _, k := range mapKeys {
- list = append(list, k)
- }
- res[i] = ValueOf(list)
- } else {
- return nil, &ErrInvalidType{
- ExpectedTypes: []string{"slice", "array", "map"},
- CurrentType: val.Kind().String(),
- }
- }
- }
- }
-
- return res, nil
- },
-}
-
-func getStringList(values []Value) ([]any, bool) {
- stringList := make([]string, len(values))
- for i, v := range values {
- if v.Kind() != reflect.String {
- return nil, false
- }
- stringList[i] = v.String()
- }
-
- sort.Strings(stringList)
-
- anyList := make([]any, len(stringList))
- for i, v := range stringList {
- anyList[i] = v
- }
-
- return anyList, true
-}
-
-func getAnyList(values []Value) []any {
- anyList := make([]any, len(values))
- for i, v := range values {
- anyList[i] = v.Interface()
- }
- return anyList
-}
diff --git a/func_keys_test.go b/func_keys_test.go
deleted file mode 100644
index 43cf2c1d..00000000
--- a/func_keys_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package dasel
-
-import (
- "github.com/tomwright/dasel/v2/dencoding"
- "testing"
-)
-
-func TestKeysFunc(t *testing.T) {
- testdata := map[string]any{
- "object": map[string]any{
- "c": 3, "a": 1, "b": 2,
- },
- "list": []any{111, 222, 333},
- "string": "something",
- "dencodingMap": dencoding.NewMap().
- Set("a", 1).
- Set("b", 2).
- Set("c", 3),
- }
-
- t.Run(
- "root",
- selectTest(
- "keys()",
- testdata,
- []any{[]any{"dencodingMap", "list", "object", "string"}},
- ),
- )
-
- t.Run(
- "List",
- selectTest(
- "list.keys()",
- testdata,
- []any{[]any{0, 1, 2}},
- ),
- )
-
- t.Run(
- "Object",
- selectTest(
- "object.keys()",
- testdata,
- []any{[]any{"a", "b", "c"}}, // sorted
- ),
- )
-
- t.Run(
- "Dencoding Map",
- selectTest(
- "dencodingMap.keys()",
- testdata,
- []any{[]any{"a", "b", "c"}}, // sorted
- ),
- )
-
- t.Run("InvalidType",
- selectTestErr(
- "string.keys()",
- testdata,
- &ErrInvalidType{
- ExpectedTypes: []string{"slice", "array", "map"},
- CurrentType: "string",
- },
- ),
- )
-}
diff --git a/func_last.go b/func_last.go
deleted file mode 100644
index cee41181..00000000
--- a/func_last.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var LastFunc = BasicFunction{
- name: "last",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("last", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- switch val.Kind() {
- case reflect.Slice, reflect.Array:
- index := val.Len() - 1
- if val.Len() == 0 {
- return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index})
- }
- value := val.Index(index)
- res = append(res, value)
- default:
- return nil, fmt.Errorf("cannot use last selector on non slice/array types: %w", &ErrIndexNotFound{Index: 0})
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_last_test.go b/func_last_test.go
deleted file mode 100644
index bc529a85..00000000
--- a/func_last_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestLastFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "last(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "last",
- Args: []string{"x"},
- }),
- )
-
- t.Run("NotFound", selectTestErr(
- "last()",
- []interface{}{},
- &ErrIndexNotFound{
- Index: -1,
- }),
- )
-
- t.Run("NotFoundOnInvalidType", selectTestErr(
- "x.last()",
- map[string]interface{}{"x": "y"},
- &ErrIndexNotFound{
- Index: 0,
- }),
- )
-
- original := map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "colours": []interface{}{
- "red", "green", "blue",
- },
- }
-
- t.Run(
- "Last",
- selectTest(
- "colours.last()",
- original,
- []interface{}{
- "blue",
- },
- ),
- )
-}
diff --git a/func_len.go b/func_len.go
deleted file mode 100644
index c3b61390..00000000
--- a/func_len.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package dasel
-
-var LenFunc = BasicFunction{
- name: "len",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("len", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- res = append(res, ValueOf(val.Len()))
- }
-
- return res, nil
- },
-}
diff --git a/func_len_test.go b/func_len_test.go
deleted file mode 100644
index da722afd..00000000
--- a/func_len_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestLenFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "len(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "len",
- Args: []string{"x"},
- }),
- )
-
- data := map[string]interface{}{
- "string": "hello",
- "slice": []interface{}{
- 1, 2, 3,
- },
- "falseBool": false,
- "trueBool": true,
- }
-
- t.Run(
- "String",
- selectTest(
- "string.len()",
- data,
- []interface{}{5},
- ),
- )
- t.Run(
- "Slice",
- selectTest(
- "slice.len()",
- data,
- []interface{}{3},
- ),
- )
- t.Run(
- "False Bool",
- selectTest(
- "falseBool.len()",
- data,
- []interface{}{0},
- ),
- )
- t.Run(
- "True Bool",
- selectTest(
- "trueBool.len()",
- data,
- []interface{}{1},
- ),
- )
-}
diff --git a/func_less_than.go b/func_less_than.go
deleted file mode 100644
index 7000dcf5..00000000
--- a/func_less_than.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/util"
- "reflect"
- "sort"
-)
-
-var LessThanFunc = BasicFunction{
- name: "lessThan",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireExactlyXArgs("lessThan", args, 2); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- type comparison struct {
- selector string
- value string
- }
-
- comparisons := make([]comparison, 0)
-
- currentComparison := comparison{}
-
- for i, v := range args {
- switch i % 2 {
- case 0:
- currentComparison.selector = v
- case 1:
- currentComparison.value = v
- comparisons = append(comparisons, currentComparison)
- currentComparison = comparison{}
- }
- }
-
- runComparison := func(value Value, cmp comparison) (bool, error) {
- gotValues, err := c.subSelect(value, cmp.selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("equal expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- gotValue := util.ToString(gotValues[0].Interface())
-
- // The values are equal
- if gotValue == cmp.value {
- return false, nil
- }
-
- sortedVals := []string{gotValue, cmp.value}
- sort.Strings(sortedVals)
-
- if sortedVals[0] == gotValue {
- return true, nil
- }
- return false, nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := true
- for _, cmp := range comparisons {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if !pass {
- valPassed = false
- break
- }
- }
- res = append(res, Value{Value: reflect.ValueOf(valPassed)})
- }
-
- return res, nil
- },
-}
diff --git a/func_less_than_test.go b/func_less_than_test.go
deleted file mode 100644
index bf8f99f2..00000000
--- a/func_less_than_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestLessThanFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "lessThan()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "lessThan",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Less Than",
- selectTest(
- "nums.all().lessThan(.,5)",
- map[string]interface{}{
- "nums": []any{
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
- },
- },
- []interface{}{
- true,
- true,
- true,
- true,
- true,
- false,
- false,
- false,
- false,
- false,
- },
- ),
- )
-}
diff --git a/func_map_of.go b/func_map_of.go
deleted file mode 100644
index 6b096ed0..00000000
--- a/func_map_of.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var MapOfFunc = BasicFunction{
- name: "mapOf",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("mapOf", args, 2); err != nil {
- return nil, err
- }
- if err := requireModulusXArgs("mapOf", args, 2); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- type pair struct {
- key string
- selector string
- }
-
- pairs := make([]pair, 0)
-
- currentPair := pair{}
-
- for i, v := range args {
- switch i % 2 {
- case 0:
- currentPair.key = v
- case 1:
- currentPair.selector = v
- pairs = append(pairs, currentPair)
- currentPair = pair{}
- }
- }
-
- getValue := func(value Value, p pair) (Value, error) {
- gotValues, err := c.subSelect(value, p.selector)
- if err != nil {
- return Value{}, err
- }
-
- if len(gotValues) != 1 {
- return Value{}, fmt.Errorf("mapOf expects selector to return exactly 1 value")
- }
-
- return gotValues[0], nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- result := reflect.MakeMap(mapStringInterfaceType)
-
- for _, p := range pairs {
- gotValue, err := getValue(val, p)
- if err != nil {
- return nil, err
- }
-
- result.SetMapIndex(reflect.ValueOf(p.key), gotValue.Value)
- }
-
- res = append(res, ValueOf(result))
- }
-
- return res, nil
- },
-}
diff --git a/func_map_of_test.go b/func_map_of_test.go
deleted file mode 100644
index 538a827b..00000000
--- a/func_map_of_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestMapOfFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "mapOf()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "mapOf",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Single Equal",
- selectTest(
- "mapOf(firstName,name.first,lastName,name.last)",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- map[string]interface{}{
- "firstName": "Tom",
- "lastName": "Wright",
- },
- },
- ),
- )
-}
diff --git a/func_merge.go b/func_merge.go
deleted file mode 100644
index 3224626d..00000000
--- a/func_merge.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package dasel
-
-import "reflect"
-
-var MergeFunc = BasicFunction{
- name: "merge",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- input := s.inputs()
-
- res := make(Values, 0)
-
- if len(args) == 0 {
- // Merge all inputs into a slice.
- resSlice := reflect.MakeSlice(sliceInterfaceType, len(input), len(input))
- for i, val := range input {
- resSlice.Index(i).Set(val.Value)
- }
- resPointer := reflect.New(resSlice.Type())
- resPointer.Elem().Set(resSlice)
-
- res = append(res, ValueOf(resPointer))
- return res, nil
- }
-
- // Merge all inputs into a slice.
- resSlice := reflect.MakeSlice(sliceInterfaceType, 0, 0)
- for _, val := range input {
- for _, a := range args {
- gotValues, err := c.subSelect(val, a)
- if err != nil {
- return nil, err
- }
-
- for _, gotVal := range gotValues {
- resSlice = reflect.Append(resSlice, gotVal.Value)
- }
- }
- }
- resPointer := reflect.New(resSlice.Type())
- resPointer.Elem().Set(resSlice)
-
- res = append(res, ValueOf(resPointer))
- return res, nil
- },
-}
diff --git a/func_merge_test.go b/func_merge_test.go
deleted file mode 100644
index c8896b0b..00000000
--- a/func_merge_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestMergeFunc(t *testing.T) {
-
- t.Run(
- "MergeWithArgs",
- selectTest(
- "merge(name.first,firstNames.all())",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "firstNames": []interface{}{
- "Jim",
- "Bob",
- },
- },
- []interface{}{
- []interface{}{
- "Tom",
- "Jim",
- "Bob",
- },
- },
- ),
- )
-
- t.Run(
- "MergeWithArgsAll",
- selectTest(
- "merge(name.first,firstNames.all()).all()",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "firstNames": []interface{}{
- "Jim",
- "Bob",
- },
- },
- []interface{}{
- "Tom",
- "Jim",
- "Bob",
- },
- ),
- )
-
- // Flaky test due to ordering.
- // t.Run(
- // "MergeNoArgs",
- // selectTest(
- // "name.all().merge()",
- // map[string]interface{}{
- // "name": map[string]interface{}{
- // "first": "Tom",
- // "last": "Wright",
- // },
- // },
- // []interface{}{
- // []interface{}{
- // "Tom",
- // "Wright",
- // },
- // },
- // ),
- // )
-
- t.Run(
- "MergeNoArgsAll",
- selectTest(
- "name.all().merge().all()",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- "Wright",
- },
- ),
- )
-}
diff --git a/func_metadata.go b/func_metadata.go
deleted file mode 100644
index 9c5d9d1d..00000000
--- a/func_metadata.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package dasel
-
-var MetadataFunc = BasicFunction{
- name: "metadata",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("metadata", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- for _, a := range args {
- res = append(res, ValueOf(val.Metadata(a)))
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_metadata_test.go b/func_metadata_test.go
deleted file mode 100644
index 4d66dedb..00000000
--- a/func_metadata_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestMetadataFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "metadata()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "metadata",
- Args: []string{},
- }),
- )
-}
diff --git a/func_more_than.go b/func_more_than.go
deleted file mode 100644
index 73ff0172..00000000
--- a/func_more_than.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "github.com/tomwright/dasel/v2/util"
- "reflect"
- "sort"
-)
-
-var MoreThanFunc = BasicFunction{
- name: "moreThan",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireExactlyXArgs("moreThan", args, 2); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- type comparison struct {
- selector string
- value string
- }
-
- comparisons := make([]comparison, 0)
-
- currentComparison := comparison{}
-
- for i, v := range args {
- switch i % 2 {
- case 0:
- currentComparison.selector = v
- case 1:
- currentComparison.value = v
- comparisons = append(comparisons, currentComparison)
- currentComparison = comparison{}
- }
- }
-
- runComparison := func(value Value, cmp comparison) (bool, error) {
- gotValues, err := c.subSelect(value, cmp.selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("equal expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- gotValue := util.ToString(gotValues[0].Interface())
-
- // The values are equal
- if gotValue == cmp.value {
- return false, nil
- }
-
- sortedVals := []string{gotValue, cmp.value}
- sort.Strings(sortedVals)
-
- if sortedVals[0] == cmp.value {
- return true, nil
- }
- return false, nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := true
- for _, cmp := range comparisons {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if !pass {
- valPassed = false
- break
- }
- }
- res = append(res, Value{Value: reflect.ValueOf(valPassed)})
- }
-
- return res, nil
- },
-}
diff --git a/func_more_than_test.go b/func_more_than_test.go
deleted file mode 100644
index 51493b5e..00000000
--- a/func_more_than_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestMoreThanFunc(t *testing.T) {
-
- t.Run("Args", selectTestErr(
- "moreThan()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "moreThan",
- Args: []string{},
- }),
- )
-
- t.Run(
- "More Than",
- selectTest(
- "nums.all().moreThan(.,5)",
- map[string]interface{}{
- "nums": []any{
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
- },
- },
- []interface{}{
- false,
- false,
- false,
- false,
- false,
- false,
- true,
- true,
- true,
- true,
- },
- ),
- )
-}
diff --git a/func_not.go b/func_not.go
deleted file mode 100644
index 1122596a..00000000
--- a/func_not.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var NotFunc = BasicFunction{
- name: "not",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("not", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- runComparison := func(value Value, selector string) (bool, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("not expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- return IsTruthy(gotValues[0].Interface()), nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- for _, selector := range args {
- truthy, err := runComparison(val, selector)
- if err != nil {
- return nil, err
- }
- res = append(res, Value{Value: reflect.ValueOf(!truthy)})
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_not_test.go b/func_not_test.go
deleted file mode 100644
index 4c2621ee..00000000
--- a/func_not_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestNotFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "not()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "not",
- Args: []string{},
- }),
- )
-
- t.Run(
- "Single Equal",
- selectTest(
- "name.all().not(equal(key(),first))",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- false,
- true,
- },
- ),
- )
-
- t.Run(
- "Not Banned",
- selectTest(
- "all().filter(not(equal(banned,true))).name",
- []map[string]interface{}{
- {
- "name": "Tom",
- "banned": true,
- },
- {
- "name": "Jess",
- "banned": false,
- },
- },
- []interface{}{
- "Jess",
- },
- ),
- )
-}
diff --git a/func_null.go b/func_null.go
deleted file mode 100644
index aacec2ca..00000000
--- a/func_null.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package dasel
-
-import (
- "reflect"
-)
-
-var NullFunc = BasicFunction{
- name: "null",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("null", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, len(input))
-
- for k, _ := range args {
- res[k] = ValueOf(reflect.ValueOf(new(any)).Elem())
- }
-
- return res, nil
- },
-}
diff --git a/func_null_test.go b/func_null_test.go
deleted file mode 100644
index e68a65b4..00000000
--- a/func_null_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestNullFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "null(1)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "null",
- Args: []string{"1"},
- }),
- )
-
- original := map[string]interface{}{}
-
- t.Run(
- "Null",
- selectTest(
- "null()",
- original,
- []interface{}{
- nil,
- },
- ),
- )
-}
diff --git a/func_or.go b/func_or.go
deleted file mode 100644
index 87bc152c..00000000
--- a/func_or.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
-)
-
-var OrFunc = BasicFunction{
- name: "or",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("or", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- runComparison := func(value Value, selector string) (bool, error) {
- gotValues, err := c.subSelect(value, selector)
- if err != nil {
- return false, err
- }
-
- if len(gotValues) > 1 {
- return false, fmt.Errorf("or expects selector to return a single value")
- }
-
- if len(gotValues) == 0 {
- return false, nil
- }
-
- return IsTruthy(gotValues[0]), nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- valPassed := false
- for _, cmp := range args {
- pass, err := runComparison(val, cmp)
- if err != nil {
- return nil, err
- }
- if pass {
- valPassed = true
- break
- }
- }
- res = append(res, Value{Value: reflect.ValueOf(valPassed)})
- }
-
- return res, nil
- },
-}
diff --git a/func_or_default.go b/func_or_default.go
deleted file mode 100644
index 0b26f07b..00000000
--- a/func_or_default.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package dasel
-
-import (
- "errors"
- "fmt"
-)
-
-var OrDefaultFunc = BasicFunction{
- name: "orDefault",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireExactlyXArgs("orDefault", args, 2); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- if c.CreateWhenMissing() {
- input = input.initEmptydencodingMaps()
- }
-
- runSubselect := func(value Value, selector string, defaultSelector string) (Value, error) {
- gotValues, err := c.subSelect(value, selector)
- notFound := false
- if err != nil {
- if errors.Is(err, &ErrPropertyNotFound{}) {
- notFound = true
- } else if errors.Is(err, &ErrIndexNotFound{Index: -1}) {
- notFound = true
- } else {
- return Value{}, err
- }
- }
-
- if !notFound {
- // Check result of first query
- if len(gotValues) != 1 {
- return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value")
- }
-
- // Consider nil values as not found
- if gotValues[0].IsNil() {
- notFound = true
- }
- }
-
- if notFound {
- gotValues, err = c.subSelect(value, defaultSelector)
- if err != nil {
- return Value{}, err
- }
- if len(gotValues) != 1 {
- return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value")
- }
- }
-
- return gotValues[0], nil
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- resolvedValue, err := runSubselect(val, args[0], args[1])
- if err != nil {
- return nil, err
- }
-
- res = append(res, resolvedValue)
- }
-
- return res, nil
- },
-}
diff --git a/func_or_default_test.go b/func_or_default_test.go
deleted file mode 100644
index 1f8e2af8..00000000
--- a/func_or_default_test.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestOrDefaultFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "orDefault()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "orDefault",
- Args: []string{},
- }),
- )
-
- t.Run("OriginalAndDefaultNotFoundProperty", selectTestErr(
- "orDefault(a,b)",
- map[string]interface{}{"x": "y"},
- &ErrPropertyNotFound{
- Property: "b",
- }),
- )
-
- t.Run("OriginalAndDefaultNotFoundIndex", selectTestErr(
- "orDefault(x.[1],x.[2])",
- map[string]interface{}{"x": []int{1}},
- &ErrIndexNotFound{
- Index: 2,
- }),
- )
-
- original := map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "colours": []interface{}{
- "red", "green", "blue",
- },
- }
-
- t.Run(
- "FirstNameOrLastName",
- selectTest(
- "orDefault(name.first,name.last)",
- original,
- []interface{}{
- "Tom",
- },
- ),
- )
-
- t.Run(
- "MiddleNameOrDefault",
- selectTest(
- "orDefault(name.middle,string(default))",
- original,
- []interface{}{
- "default",
- },
- ),
- )
-
- t.Run(
- "FirstColourOrSecondColour",
- selectTest(
- "orDefault(colours.[0],colours.[2])",
- original,
- []interface{}{
- "red",
- },
- ),
- )
-
- t.Run(
- "FourthColourOrDefault",
- selectTest(
- "orDefault(colours.[3],string(default))",
- original,
- []interface{}{
- "default",
- },
- ),
- )
-}
diff --git a/func_or_test.go b/func_or_test.go
deleted file mode 100644
index e87c5e18..00000000
--- a/func_or_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestOrFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "or()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "or",
- Args: []string{},
- }),
- )
-
- t.Run(
- "NoneEqualMoreThan",
- selectTest(
- "numbers.all().or(equal(.,2),moreThan(.,2))",
- map[string]interface{}{
- "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- },
- []interface{}{
- false, false, true, true, true, true, true, true, true, true,
- },
- ),
- )
- t.Run(
- "SomeEqualMoreThan",
- selectTest(
- "numbers.all().or(equal(.,0),moreThan(.,2))",
- map[string]interface{}{
- "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- },
- []interface{}{
- true, false, false, true, true, true, true, true, true, true,
- },
- ),
- )
-}
diff --git a/func_parent.go b/func_parent.go
deleted file mode 100644
index 9b553e39..00000000
--- a/func_parent.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package dasel
-
-import (
- "strconv"
-)
-
-var ParentFunc = BasicFunction{
- name: "parent",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrLessArgs("parent", args, 1); err != nil {
- return nil, err
- }
-
- levels := 1
- if len(args) > 0 {
- arg, err := strconv.Atoi(args[0])
- if err != nil {
- return nil, err
- }
- levels = arg
- }
- if levels < 1 {
- levels = 1
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- getParent := func(v Value, levels int) (Value, bool) {
- res := v
- for i := 0; i < levels; i++ {
- p := res.Metadata("parent")
- if p == nil {
- return res, false
- }
- if pv, ok := p.(Value); ok {
- res = pv
- } else {
- return res, false
- }
- }
- return res, true
- }
-
- for _, i := range input {
- if pv, ok := getParent(i, levels); ok {
- res = append(res, pv)
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_parent_test.go b/func_parent_test.go
deleted file mode 100644
index d33f96ee..00000000
--- a/func_parent_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestParentFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "parent(x,x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "parent",
- Args: []string{"x", "x"},
- }),
- )
-
- t.Run(
- "SimpleParent",
- selectTest(
- "name.first.parent()",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- ),
- )
-
- t.Run(
- "SimpleParent2Levels",
- selectTest(
- "user.name.first.parent(2).deleted",
- map[string]interface{}{
- "user": map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "deleted": false,
- },
- },
- []interface{}{
- false,
- },
- ),
- )
-
- t.Run(
- "MultiParent",
- selectTest(
- "name.all().parent()",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- ),
- )
-
- t.Run(
- "FilteredParent",
- selectTest(
- "all().flags.filter(equal(banned,false)).parent().name",
- []map[string]interface{}{
- {
- "flags": map[string]interface{}{
- "banned": false,
- },
- "name": "Tom",
- },
- {
- "flags": map[string]interface{}{
- "banned": true,
- },
- "name": "Jim",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
-}
diff --git a/func_property.go b/func_property.go
deleted file mode 100644
index 9d9be877..00000000
--- a/func_property.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
- "strings"
-)
-
-type ErrPropertyNotFound struct {
- Property string
-}
-
-func (e ErrPropertyNotFound) Error() string {
- return fmt.Sprintf("property not found: %s", e.Property)
-}
-
-func (e ErrPropertyNotFound) Is(other error) bool {
- o, ok := other.(*ErrPropertyNotFound)
- if !ok {
- return false
- }
- if o.Property != "" && o.Property != e.Property {
- return false
- }
- return true
-}
-
-var PropertyFunc = BasicFunction{
- name: "property",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireXOrMoreArgs("property", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- if c.CreateWhenMissing() {
- input = input.initEmptydencodingMaps()
- }
-
- res := make(Values, 0)
-
- for _, val := range input {
- for _, property := range args {
- isOptional := strings.HasSuffix(property, "?")
- if isOptional {
- property = strings.TrimSuffix(property, "?")
- }
-
- switch val.Kind() {
- case reflect.Map:
- index := val.MapIndex(ValueOf(property))
- if index.IsEmpty() {
- if isOptional {
- continue
- }
- if !c.CreateWhenMissing() {
- return nil, fmt.Errorf("could not access map index: %w", &ErrPropertyNotFound{Property: property})
- }
- index = index.asUninitialised()
- }
- res = append(res, index)
- case reflect.Struct:
- value := val.FieldByName(property)
- if value.IsEmpty() {
- if isOptional {
- continue
- }
- return nil, fmt.Errorf("could not access struct field: %w", &ErrPropertyNotFound{Property: property})
- }
- res = append(res, value)
- default:
- if val.IsDencodingMap() {
- index := val.dencodingMapIndex(ValueOf(property))
- if index.IsEmpty() {
- if isOptional {
- continue
- }
- if !c.CreateWhenMissing() {
- return nil, fmt.Errorf("could not access map index: %w", &ErrPropertyNotFound{Property: property})
- }
- index = index.asUninitialised()
- }
- res = append(res, index)
- } else {
- return nil, fmt.Errorf("cannot use property selector on non map/struct types: %s: %w", val.Kind().String(), &ErrPropertyNotFound{Property: property})
- }
- }
- }
- }
-
- return res, nil
- },
-}
diff --git a/func_property_test.go b/func_property_test.go
deleted file mode 100644
index 5fe6143f..00000000
--- a/func_property_test.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestPropertyFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "property()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "property",
- Args: []string{},
- }),
- )
-
- t.Run("NotFound", selectTestErr(
- "asd",
- map[string]interface{}{"x": "y"},
- &ErrPropertyNotFound{
- Property: "asd",
- }),
- )
-
- t.Run("NotFoundOnString", selectTestErr(
- "x.asd",
- map[string]interface{}{"x": "y"},
- &ErrPropertyNotFound{
- Property: "asd",
- }),
- )
-
- original := map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- "colours": []interface{}{
- "red", "green", "blue",
- },
- }
-
- t.Run(
- "SingleLevelProperty",
- selectTest(
- "name",
- original,
- []interface{}{
- map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- ),
- )
-
- t.Run(
- "SingleLevelPropertyFunc",
- selectTest(
- "property(name)",
- original,
- []interface{}{
- map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- ),
- )
-
- t.Run(
- "NestedPropertyFunc",
- selectTest(
- "property(name).property(first)",
- original,
- []interface{}{
- "Tom",
- },
- ),
- )
-
- t.Run(
- "NestedMultiPropertyFunc",
- selectTest(
- "property(name).property(first,last)",
- original,
- []interface{}{
- "Tom",
- "Wright",
- },
- ),
- )
-
- t.Run(
- "NestedMultiMissingPropertyFunc",
- selectTest(
- "property(name).property(first,last,middle?)",
- original,
- []interface{}{
- "Tom",
- "Wright",
- },
- ),
- )
-}
diff --git a/func_string.go b/func_string.go
deleted file mode 100644
index 295071bd..00000000
--- a/func_string.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package dasel
-
-import "github.com/tomwright/dasel/v2/util"
-
-var StringFunc = BasicFunction{
- name: "string",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireExactlyXArgs("string", args, 1); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, len(input))
-
- for k, v := range args {
- res[k] = ValueOf(util.ToString(v))
- }
-
- return res, nil
- },
-}
diff --git a/func_string_test.go b/func_string_test.go
deleted file mode 100644
index f7bc43c5..00000000
--- a/func_string_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestStringFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "string()",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "string",
- Args: []string{},
- }),
- )
-
- original := map[string]interface{}{}
-
- t.Run(
- "String",
- selectTest(
- "string(x)",
- original,
- []interface{}{
- "x",
- },
- ),
- )
-
- t.Run(
- "Comma",
- selectTest(
- "string(\\,)",
- original,
- []interface{}{
- ",",
- },
- ),
- )
-}
diff --git a/func_this.go b/func_this.go
deleted file mode 100644
index 13a6d7f7..00000000
--- a/func_this.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package dasel
-
-var ThisFunc = BasicFunction{
- name: "this",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("this", args); err != nil {
- return nil, err
- }
- return s.inputs(), nil
- },
-}
diff --git a/func_this_test.go b/func_this_test.go
deleted file mode 100644
index d1aacb92..00000000
--- a/func_this_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestThisFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "this(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "this",
- Args: []string{"x"},
- }),
- )
- t.Run(
- "SimpleThis",
- selectTest(
- "name.this().first",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
- t.Run(
- "BlankSelectorThis",
- selectTest(
- ".name.first",
- map[string]interface{}{
- "name": map[string]interface{}{
- "first": "Tom",
- "last": "Wright",
- },
- },
- []interface{}{
- "Tom",
- },
- ),
- )
-}
diff --git a/func_type.go b/func_type.go
deleted file mode 100644
index 07c0f01f..00000000
--- a/func_type.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package dasel
-
-import "reflect"
-
-var TypeFunc = BasicFunction{
- name: "type",
- runFn: func(c *Context, s *Step, args []string) (Values, error) {
- if err := requireNoArgs("type", args); err != nil {
- return nil, err
- }
-
- input := s.inputs()
-
- res := make(Values, 0)
-
- for _, val := range input {
- resStr := "unknown"
-
- if val.IsNil() {
- resStr = "null"
- } else if val.IsDencodingMap() {
- resStr = "object"
- } else {
- switch val.Kind() {
- case reflect.Slice, reflect.Array:
- resStr = "array"
- case reflect.Map, reflect.Struct:
- resStr = "object"
- case reflect.String:
- resStr = "string"
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
- reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
- reflect.Float32, reflect.Float64:
- resStr = "number"
- case reflect.Bool:
- resStr = "bool"
- }
- }
- res = append(res, ValueOf(resStr))
- }
-
- return res, nil
- },
-}
diff --git a/func_type_test.go b/func_type_test.go
deleted file mode 100644
index a042d92b..00000000
--- a/func_type_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package dasel
-
-import (
- "testing"
-)
-
-func TestTypeFunc(t *testing.T) {
- t.Run("Args", selectTestErr(
- "type(x)",
- map[string]interface{}{},
- &ErrUnexpectedFunctionArgs{
- Function: "type",
- Args: []string{"x"},
- }),
- )
-
- data := map[string]interface{}{
- "string": "hello",
- "slice": []interface{}{
- 1, 2, 3,
- },
- "map": map[string]interface{}{
- "x": 1,
- },
- "int": int(1),
- "float": float32(1),
- "bool": true,
- "null": nil,
- }
-
- t.Run(
- "String",
- selectTest(
- "string.type()",
- data,
- []interface{}{
- "string",
- },
- ),
- )
- t.Run(
- "Slice",
- selectTest(
- "slice.type()",
- data,
- []interface{}{
- "array",
- },
- ),
- )
- t.Run(
- "map",
- selectTest(
- "map.type()",
- data,
- []interface{}{
- "object",
- },
- ),
- )
- t.Run(
- "int",
- selectTest(
- "int.type()",
- data,
- []interface{}{
- "number",
- },
- ),
- )
- t.Run(
- "float",
- selectTest(
- "float.type()",
- data,
- []interface{}{
- "number",
- },
- ),
- )
- t.Run(
- "bool",
- selectTest(
- "bool.type()",
- data,
- []interface{}{
- "bool",
- },
- ),
- )
- t.Run(
- "null",
- selectTest(
- "null.type()",
- data,
- []interface{}{
- "null",
- },
- ),
- )
-}
diff --git a/go.mod b/go.mod
index f26f92ed..4cd760d2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,22 +1,41 @@
-module github.com/tomwright/dasel/v2
+module github.com/tomwright/dasel/v3
-go 1.21
+go 1.23
require (
- github.com/alecthomas/chroma/v2 v2.14.0
- github.com/clbanning/mxj/v2 v2.7.0
+ github.com/alecthomas/kong v1.2.1
+ github.com/google/go-cmp v0.6.0
github.com/pelletier/go-toml/v2 v2.2.2
- github.com/spf13/cobra v1.8.1
- golang.org/x/net v0.30.0
- golang.org/x/text v0.19.0
gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
- github.com/dlclark/regexp2 v1.11.0 // indirect
- github.com/google/go-cmp v0.5.9 // indirect
- github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
+ github.com/agext/levenshtein v1.2.1 // indirect
+ github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/bubbles v0.20.0 // indirect
+ github.com/charmbracelet/bubbletea v1.1.2 // indirect
+ github.com/charmbracelet/lipgloss v0.13.1 // indirect
+ github.com/charmbracelet/x/ansi v0.4.0 // indirect
+ github.com/charmbracelet/x/term v0.2.0 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+ github.com/hashicorp/hcl/v2 v2.22.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/zclconf/go-cty v1.13.0 // indirect
+ golang.org/x/mod v0.8.0 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/text v0.11.0 // indirect
+ golang.org/x/tools v0.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index f1d728c7..7c5fbb09 100644
--- a/go.sum
+++ b/go.sum
@@ -1,34 +1,71 @@
-github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
-github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
-github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
-github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
+github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
+github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q=
+github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
-github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
-github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
+github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc=
+github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E=
+github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
+github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
+github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A=
+github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
+github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU=
+github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
+github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
-github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
+github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -38,10 +75,22 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0=
+github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/cli/command.go b/internal/cli/command.go
new file mode 100644
index 00000000..b7389ebf
--- /dev/null
+++ b/internal/cli/command.go
@@ -0,0 +1,59 @@
+package cli
+
+import (
+ "io"
+ "reflect"
+
+ "github.com/alecthomas/kong"
+ "github.com/tomwright/dasel/v3/internal"
+)
+
+type Globals struct {
+ Stdin io.Reader `kong:"-"`
+ Stdout io.Writer `kong:"-"`
+ Stderr io.Writer `kong:"-"`
+}
+
+type CLI struct {
+ Globals
+
+ Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"`
+ Version VersionCmd `cmd:"" help:"Print the version"`
+ Interactive InteractiveCmd `cmd:"" help:"Start an interactive session"`
+}
+
+func MustRun(stdin io.Reader, stdout, stderr io.Writer) {
+ ctx, err := Run(stdin, stdout, stderr)
+ ctx.FatalIfErrorf(err)
+}
+
+func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) {
+ cli := &CLI{
+ Globals: Globals{
+ Stdin: stdin,
+ Stdout: stdout,
+ Stderr: stderr,
+ },
+ }
+
+ ctx := kong.Parse(
+ cli,
+ kong.Name("dasel"),
+ kong.Description("Query and modify data structures from the command line."),
+ kong.UsageOnError(),
+ kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
+ kong.Vars{
+ "version": internal.Version,
+ },
+ kong.Bind(&cli.Globals),
+ kong.TypeMapper(reflect.TypeFor[variables](), &variableMapper{}),
+ kong.TypeMapper(reflect.TypeFor[extReadWriteFlags](), &extReadWriteFlagMapper{}),
+ kong.OptionFunc(func(k *kong.Kong) error {
+ k.Stdout = cli.Stdout
+ k.Stderr = cli.Stderr
+ return nil
+ }),
+ )
+ err := ctx.Run()
+ return ctx, err
+}
diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go
new file mode 100644
index 00000000..9cc9c708
--- /dev/null
+++ b/internal/cli/command_test.go
@@ -0,0 +1,111 @@
+package cli_test
+
+import (
+ "bytes"
+ "errors"
+ "os"
+ "reflect"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/internal/cli"
+)
+
+func runDasel(args []string, in []byte) ([]byte, []byte, error) {
+ stdOut := bytes.NewBuffer([]byte{})
+ stdErr := bytes.NewBuffer([]byte{})
+ stdIn := bytes.NewReader(in)
+
+ originalArgs := os.Args
+ defer func() {
+ os.Args = originalArgs
+ }()
+
+ os.Args = append([]string{"dasel", "query"}, args...)
+
+ _, err := cli.Run(stdIn, stdOut, stdErr)
+
+ return stdOut.Bytes(), stdErr.Bytes(), err
+}
+
+type testCase struct {
+ args []string
+ in []byte
+ stdout []byte
+ stderr []byte
+ err error
+}
+
+func runTest(tc testCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ if tc.stdout == nil {
+ tc.stdout = []byte{}
+ }
+ if tc.stderr == nil {
+ tc.stderr = []byte{}
+ }
+
+ gotStdOut, gotStdErr, gotErr := runDasel(tc.args, tc.in)
+ if !errors.Is(gotErr, tc.err) && !errors.Is(tc.err, gotErr) {
+ t.Errorf("expected error %v, got %v", tc.err, gotErr)
+ return
+ }
+
+ if !reflect.DeepEqual(tc.stderr, gotStdErr) {
+ t.Errorf("expected stderr %s, got %s", string(tc.stderr), string(gotStdErr))
+ }
+
+ if !reflect.DeepEqual(tc.stdout, gotStdOut) {
+ t.Errorf("expected stdout %s, got %s", string(tc.stdout), string(gotStdOut))
+ }
+ }
+}
+
+func TestRun(t *testing.T) {
+ t.Run("complex set", func(t *testing.T) {
+ t.Run("set nested with spread", runTest(testCase{
+ args: []string{"-i", "json", "-o", "json", "--root", `user = {user..., name: {"first": $this.user.name, "last": "Doe"}}`},
+ in: []byte(`{"user": {"name": "John"}}`),
+ stdout: []byte(`{
+ "user": {
+ "name": {
+ "first": "John",
+ "last": "Doe"
+ }
+ }
+}
+`),
+ stderr: nil,
+ err: nil,
+ }))
+ t.Run("set nested", runTest(testCase{
+ args: []string{"-i", "json", "-o", "json", "--root", `user.name = {"first": user.name, "last": "Doe"}`},
+ in: []byte(`{"user": {"name": "John"}}`),
+ stdout: []byte(`{
+ "user": {
+ "name": {
+ "first": "John",
+ "last": "Doe"
+ }
+ }
+}
+`),
+ stderr: nil,
+ err: nil,
+ }))
+ t.Run("set nested with localised group", runTest(testCase{
+ args: []string{"-i", "json", "-o", "json", "--root", `user.(name = {"first": name, "last": "Doe"})`},
+ in: []byte(`{"user": {"name": "John"}}`),
+ stdout: []byte(`{
+ "user": {
+ "name": {
+ "first": "John",
+ "last": "Doe"
+ }
+ }
+}
+`),
+ stderr: nil,
+ err: nil,
+ }))
+ })
+}
diff --git a/internal/cli/generic_test.go b/internal/cli/generic_test.go
new file mode 100644
index 00000000..9ab63fe4
--- /dev/null
+++ b/internal/cli/generic_test.go
@@ -0,0 +1,270 @@
+package cli_test
+
+import (
+ "fmt"
+ "slices"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/json"
+ "github.com/tomwright/dasel/v3/parsing/toml"
+ "github.com/tomwright/dasel/v3/parsing/yaml"
+)
+
+func newStringWithFormat(format parsing.Format, data string) bytesWithFormat {
+ return bytesWithFormat{
+ format: format,
+ data: append([]byte(data), []byte("\n")...),
+ }
+}
+
+type bytesWithFormat struct {
+ format parsing.Format
+ data []byte
+}
+
+type testCases struct {
+ selector string
+ in []bytesWithFormat
+ out []bytesWithFormat
+ args []string
+ skip []string
+}
+
+func (tcs testCases) run(t *testing.T) {
+ for _, i := range tcs.in {
+ for _, o := range tcs.out {
+ tcName := fmt.Sprintf("%s to %s", i.format.String(), o.format.String())
+
+ if slices.Contains(tcs.skip, tcName) {
+ // Run a test and skip for visibility.
+ t.Run(tcName, func(t *testing.T) {
+ t.Skip()
+ })
+ continue
+ }
+
+ args := slices.Clone(tcs.args)
+ args = append(args, "-i", i.format.String(), "-o", o.format.String())
+ if tcs.selector != "" {
+ args = append(args, tcs.selector)
+ }
+ tc := testCase{
+ args: args,
+ in: i.data,
+ stdout: o.data,
+ }
+ t.Run(tcName, runTest(tc))
+ }
+ }
+}
+
+func TestCrossFormatHappyPath(t *testing.T) {
+ jsonInputData := newStringWithFormat(json.JSON, `{
+ "oneTwoThree": 123,
+ "oneTwoDotThree": 12.3,
+ "hello": "world",
+ "boolFalse": false,
+ "boolTrue": true,
+ "stringFalse": "false",
+ "stringTrue": "true",
+ "sliceOfNumbers": [1, 2, 3, 4, 5],
+ "mapData": {
+ "oneTwoThree": 123,
+ "oneTwoDotThree": 12.3,
+ "hello": "world",
+ "boolFalse": false,
+ "boolTrue": true,
+ "stringFalse": "false",
+ "stringTrue": "true",
+ "sliceOfNumbers": [1, 2, 3, 4, 5],
+ "mapData": {
+ "oneTwoThree": 123,
+ "oneTwoDotThree": 12.3,
+ "hello": "world",
+ "boolFalse": false,
+ "boolTrue": true,
+ "stringFalse": "false",
+ "stringTrue": "true",
+ "sliceOfNumbers": [1, 2, 3, 4, 5]
+ }
+ }
+}`)
+ yamlInputData := newStringWithFormat(yaml.YAML, `oneTwoThree: 123
+oneTwoDotThree: 12.3
+hello: world
+boolFalse: false
+boolTrue: true
+stringFalse: "false"
+stringTrue: "true"
+sliceOfNumbers:
+- 1
+- 2
+- 3
+- 4
+- 5
+mapData:
+ oneTwoThree: 123
+ oneTwoDotThree: 12.3
+ hello: world
+ boolFalse: false
+ boolTrue: true
+ stringFalse: "false"
+ stringTrue: "true"
+ sliceOfNumbers:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ mapData:
+ oneTwoThree: 123
+ oneTwoDotThree: 12.3
+ hello: world
+ boolFalse: false
+ boolTrue: true
+ stringFalse: "false"
+ stringTrue: "true"
+ sliceOfNumbers:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+`)
+
+ tomlInputData := newStringWithFormat(toml.TOML, `
+oneTwoThree = 123
+oneTwoDotThree = 12.3
+hello = 'world'
+boolFalse = false
+boolTrue = true
+stringFalse = 'false'
+stringTrue = 'true'
+sliceOfNumbers = [1, 2, 3, 4, 5]
+
+[mapData]
+oneTwoThree = 123
+oneTwoDotThree = 12.3
+hello = "world"
+boolFalse = false
+boolTrue = true
+stringFalse = "false"
+stringTrue = "true"
+sliceOfNumbers = [1, 2, 3, 4, 5]
+
+[mapData.mapData]
+oneTwoThree = 123
+oneTwoDotThree = 12.3
+hello = "world"
+boolFalse = false
+boolTrue = true
+stringFalse = "false"
+stringTrue = "true"
+sliceOfNumbers = [1, 2, 3, 4, 5]
+`)
+
+ t.Run("select", func(t *testing.T) {
+ newTestsWithPrefix := func(prefix string) func(*testing.T) {
+ return func(t *testing.T) {
+ t.Run("string", testCases{
+ selector: prefix + "hello",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ tomlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `"world"`),
+ newStringWithFormat(yaml.YAML, `world`),
+ newStringWithFormat(toml.TOML, `'world'`),
+ },
+ }.run)
+ t.Run("int", testCases{
+ selector: prefix + "oneTwoThree",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ tomlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `123`),
+ newStringWithFormat(yaml.YAML, `123`),
+ newStringWithFormat(toml.TOML, `123`),
+ },
+ skip: []string{
+ // Skipped because the parser outputs as a float.
+ "json to toml",
+ },
+ }.run)
+ t.Run("float", testCases{
+ selector: prefix + "oneTwoDotThree",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ tomlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `12.3`),
+ newStringWithFormat(yaml.YAML, `12.3`),
+ newStringWithFormat(toml.TOML, `12.3`),
+ },
+ }.run)
+ t.Run("bool", func(t *testing.T) {
+ t.Run("true", testCases{
+ selector: prefix + "boolTrue",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `true`),
+ newStringWithFormat(yaml.YAML, `true`),
+ newStringWithFormat(toml.TOML, `true`),
+ },
+ }.run)
+ t.Run("false", testCases{
+ selector: prefix + "boolFalse",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `false`),
+ newStringWithFormat(yaml.YAML, `false`),
+ newStringWithFormat(toml.TOML, `false`),
+ },
+ }.run)
+ t.Run("true string", testCases{
+ selector: prefix + "stringTrue",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `"true"`),
+ newStringWithFormat(yaml.YAML, `"true"`),
+ newStringWithFormat(toml.TOML, `'true'`),
+ },
+ }.run)
+ t.Run("false string", testCases{
+ selector: prefix + "stringFalse",
+ in: []bytesWithFormat{
+ jsonInputData,
+ yamlInputData,
+ },
+ out: []bytesWithFormat{
+ newStringWithFormat(json.JSON, `"false"`),
+ newStringWithFormat(yaml.YAML, `"false"`),
+ newStringWithFormat(toml.TOML, `'false'`),
+ },
+ }.run)
+ })
+ }
+ }
+
+ t.Run("root", newTestsWithPrefix(""))
+ t.Run("nested once", newTestsWithPrefix("mapData."))
+ t.Run("nested twice", newTestsWithPrefix("mapData.mapData."))
+ })
+}
diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go
new file mode 100644
index 00000000..5543e449
--- /dev/null
+++ b/internal/cli/interactive.go
@@ -0,0 +1,100 @@
+package cli
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd {
+ return &InteractiveCmd{
+ Vars: queryCmd.Vars,
+ ExtReadFlags: queryCmd.ExtReadFlags,
+ ExtWriteFlags: queryCmd.ExtWriteFlags,
+ InFormat: queryCmd.InFormat,
+ OutFormat: queryCmd.OutFormat,
+
+ Query: queryCmd.Query,
+ }
+}
+
+type InteractiveCmd struct {
+ Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"`
+ ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"`
+ ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"`
+ InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."`
+ OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."`
+
+ Query string `arg:"" help:"The query to execute." optional:"" default:""`
+}
+
+func (c *InteractiveCmd) Run(ctx *Globals) error {
+ var stdInBytes []byte = nil
+
+ if ctx.Stdin != nil {
+ var err error
+ stdInBytes, err = io.ReadAll(ctx.Stdin)
+ if err != nil {
+ return err
+ }
+ }
+
+ if c.InFormat == "" && c.OutFormat == "" {
+ c.InFormat = "json"
+ c.OutFormat = "json"
+ } else if c.InFormat == "" {
+ c.InFormat = c.OutFormat
+ } else if c.OutFormat == "" {
+ c.OutFormat = c.InFormat
+ }
+
+ var runDasel interactiveDaselExecutor = func(selector string, root bool, formatIn parsing.Format, formatOut parsing.Format, in string) (res string, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("panic: %v", r)
+ }
+ }()
+ var stdIn *bytes.Reader = nil
+ if in != "" {
+ stdIn = bytes.NewReader([]byte(in))
+ } else {
+ stdIn = bytes.NewReader([]byte{})
+ }
+
+ o := runOpts{
+ Vars: c.Vars,
+ ExtReadFlags: c.ExtReadFlags,
+ ExtWriteFlags: c.ExtWriteFlags,
+ InFormat: formatIn.String(),
+ OutFormat: formatOut.String(),
+ ReturnRoot: root,
+ Unstable: true,
+ Query: selector,
+
+ Stdin: stdIn,
+ }
+
+ outBytes, err := run(o)
+ return string(outBytes), err
+ }
+
+ p, selectorFn := newInteractiveTeaProgram(string(stdInBytes), c.Query, parsing.Format(c.InFormat), parsing.Format(c.OutFormat), runDasel)
+
+ _, err := p.Run()
+ if err != nil {
+ return err
+ }
+
+ if selectorFn != nil {
+ s := selectorFn()
+ if s != "" {
+ if _, err := fmt.Fprintf(ctx.Stdout, "%s\n", s); err != nil {
+ return fmt.Errorf("error writing output: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/cli/interactive_tea.go b/internal/cli/interactive_tea.go
new file mode 100644
index 00000000..95154f7f
--- /dev/null
+++ b/internal/cli/interactive_tea.go
@@ -0,0 +1,209 @@
+package cli
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/tomwright/dasel/v3/internal"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+var (
+ interactiveKeyQuit = tea.KeyCtrlC
+ interactiveKeyCycleRead = tea.KeyCtrlE
+ interactiveKeyCycleWrite = tea.KeyCtrlD
+
+ headingStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 1, 1, 1)
+ }()
+ shortcutStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 1).Align(lipgloss.Left)
+ }()
+ headerStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(1).Align(lipgloss.Center)
+ }()
+ inputStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Margin(0, 0, 1, 0)
+ }()
+ inputContentStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 1).Border(lipgloss.RoundedBorder())
+ }()
+ inputHeaderStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 2).Margin(0, 0, 1, 0).Underline(true)
+ }()
+ outputContentStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 1).Border(lipgloss.RoundedBorder())
+ }()
+ outputHeaderStyle = func() lipgloss.Style {
+ return lipgloss.NewStyle().Padding(0, 2).Margin(0, 0, 1, 0).Underline(true)
+ }()
+)
+
+type interactiveDaselExecutor func(selector string, root bool, formatIn parsing.Format, formatOut parsing.Format, in string) (res string, err error)
+
+func newInteractiveTeaProgram(initialInput string, initialSelector string, formatIn parsing.Format, formatOut parsing.Format, run interactiveDaselExecutor) (*tea.Program, func() string) {
+ m := newInteractiveRootModel(initialInput, initialSelector, formatIn, formatOut, run)
+ return tea.NewProgram(m, tea.WithAltScreen()), func() string {
+ return m.sharedData.selector
+ }
+}
+
+type interactiveSharedData struct {
+ formatIn parsing.Format
+ formatOut parsing.Format
+ selector string
+ input string
+}
+
+type interactiveRootModel struct {
+ sharedData *interactiveSharedData
+ inputModel *interactiveInputModel
+ outputModels []*interactiveOutputModel
+}
+
+func newInteractiveRootModel(initialInput string, initialSelector string, formatIn parsing.Format, formatOut parsing.Format, run interactiveDaselExecutor) *interactiveRootModel {
+ res := &interactiveRootModel{
+ sharedData: &interactiveSharedData{
+ formatIn: formatIn,
+ formatOut: formatOut,
+ selector: initialSelector,
+ input: initialInput,
+ },
+ outputModels: make([]*interactiveOutputModel, 0),
+ }
+
+ res.inputModel = newInteractiveInputModel(res.sharedData)
+
+ outputRootModel := newInteractiveOutputModel(res.sharedData, true, run)
+ outputResultModel := newInteractiveOutputModel(res.sharedData, false, run)
+
+ res.outputModels = append(res.outputModels, outputRootModel, outputResultModel)
+
+ return res
+}
+
+func (m *interactiveRootModel) Init() tea.Cmd {
+ return nil
+}
+
+func cycleFormats(all []parsing.Format, current parsing.Format) parsing.Format {
+ slices.SortFunc(all, func(i, j parsing.Format) int {
+ return strings.Compare(string(i), string(j))
+ })
+ cur := -1
+ for i, format := range all {
+ if format == current {
+ cur = i
+ break
+ }
+ }
+ next := cur + 1
+ if next > len(all)-1 {
+ next = 0
+ }
+ return all[next]
+}
+
+func (m *interactiveRootModel) cycleReader() {
+ m.sharedData.formatIn = cycleFormats(parsing.RegisteredReaders(), m.sharedData.formatIn)
+}
+
+func (m *interactiveRootModel) cycleWriter() {
+ m.sharedData.formatOut = cycleFormats(parsing.RegisteredWriters(), m.sharedData.formatOut)
+}
+
+func (m *interactiveRootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.Type {
+ case interactiveKeyQuit:
+ return m, tea.Quit
+ case interactiveKeyCycleRead:
+ m.cycleReader()
+ case interactiveKeyCycleWrite:
+ m.cycleWriter()
+ default:
+ }
+
+ case tea.WindowSizeMsg:
+ headerStyle = headerStyle.Width(msg.Width).MaxWidth(msg.Width)
+
+ var headerHeight int
+ {
+ headerHeight += lipgloss.Height(m.headerView())
+ headerHeight += lipgloss.Height(m.inputView())
+ }
+ verticalMarginHeight := headerHeight
+
+ numCols := len(m.outputModels)
+
+ viewportHeight := msg.Height - verticalMarginHeight - (2 * numCols)
+ viewportWidth := (msg.Width / numCols) - (2 * numCols)
+
+ for _, outputModel := range m.outputModels {
+ outputModel.setSize(viewportWidth, viewportHeight)
+ outputModel.setVerticalPosition(verticalMarginHeight)
+ }
+ }
+
+ {
+ var model tea.Model
+ model, cmd = m.inputModel.Update(msg)
+ m.inputModel = model.(*interactiveInputModel)
+ cmds = append(cmds, cmd)
+ }
+
+ for i, outputModel := range m.outputModels {
+ var model tea.Model
+ model, cmd = outputModel.Update(msg)
+ m.outputModels[i] = model.(*interactiveOutputModel)
+ cmds = append(cmds, cmd)
+ }
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *interactiveRootModel) headerView() string {
+ header := headingStyle.Render("Dasel interactive mode - " + internal.Version)
+
+ shortcuts := "\n"
+ shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyQuit, "Quit")
+ shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyCycleRead, "Cycle reader")
+ shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyCycleWrite, "Cycle writer")
+
+ out := append([]string{header}, shortcutStyle.Render(shortcuts))
+
+ out = append(out, fmt.Sprintf("\nReader: %s | Writer: %s", m.sharedData.formatIn, m.sharedData.formatOut))
+
+ return headerStyle.Render(out...)
+}
+
+func (m *interactiveRootModel) inputView() string {
+ return inputStyle.Render(m.inputModel.View())
+}
+
+func (m *interactiveRootModel) View() string {
+ rows := make([]string, 0)
+
+ rows = append(rows, m.headerView())
+
+ rows = append(rows, m.inputView())
+
+ {
+ cols := make([]string, 0)
+ for _, outputModel := range m.outputModels {
+ cols = append(cols, outputModel.View())
+ }
+ if len(cols) > 0 {
+ rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, cols...))
+ }
+ }
+
+ return lipgloss.JoinVertical(lipgloss.Left, rows...)
+}
diff --git a/internal/cli/interactive_tea_input.go b/internal/cli/interactive_tea_input.go
new file mode 100644
index 00000000..10865332
--- /dev/null
+++ b/internal/cli/interactive_tea_input.go
@@ -0,0 +1,50 @@
+package cli
+
+import (
+ "github.com/charmbracelet/bubbles/textarea"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type interactiveInputModel struct {
+ sharedData *interactiveSharedData
+ inputModel textarea.Model
+}
+
+func newInteractiveInputModel(sharedData *interactiveSharedData) *interactiveInputModel {
+ ti := textarea.New()
+ ti.Placeholder = "Enter a query..."
+ ti.SetValue(sharedData.selector)
+ ti.Focus()
+ ti.SetHeight(5)
+ ti.ShowLineNumbers = false
+
+ return &interactiveInputModel{
+ sharedData: sharedData,
+ inputModel: ti,
+ }
+}
+
+func (m *interactiveInputModel) Init() tea.Cmd {
+ return textarea.Blink
+}
+
+func (m *interactiveInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ m.sharedData.selector = m.inputModel.Value()
+
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.inputModel.SetWidth(msg.Width)
+ }
+
+ m.inputModel, cmd = m.inputModel.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *interactiveInputModel) View() string {
+ return m.inputModel.View()
+}
diff --git a/internal/cli/interactive_tea_output.go b/internal/cli/interactive_tea_output.go
new file mode 100644
index 00000000..1ab0ddda
--- /dev/null
+++ b/internal/cli/interactive_tea_output.go
@@ -0,0 +1,106 @@
+package cli
+
+import (
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+type interactiveOutputModel struct {
+ sharedData *interactiveSharedData
+ hasUpdatedBefore bool
+ lastSeenSelector string
+ lastSeenFormatIn parsing.Format
+ lastSeenFormatOut parsing.Format
+ lastSeenInput string
+ root bool
+ run interactiveDaselExecutor
+ output string
+ outputViewport viewport.Model
+ outputViewportReady bool
+}
+
+func newInteractiveOutputModel(sharedData *interactiveSharedData, root bool, run interactiveDaselExecutor) *interactiveOutputModel {
+ m := &interactiveOutputModel{
+ sharedData: sharedData,
+ root: root,
+ run: run,
+ }
+ m.outputViewport = viewport.New(10, 10)
+ return m
+}
+
+func (m *interactiveOutputModel) Init() tea.Cmd {
+ return nil
+}
+
+func (m *interactiveOutputModel) setOutput(output string) {
+ m.output = output
+ if m.outputViewportReady {
+ m.outputViewport.SetContent(m.output)
+ }
+}
+
+func (m *interactiveOutputModel) setSize(width int, height int) {
+ if !m.outputViewportReady {
+ m.outputViewportReady = true
+ }
+
+ m.outputViewport.Width = width
+ m.outputViewport.Height = height
+ m.outputViewport.SetContent(m.output)
+}
+
+func (m *interactiveOutputModel) setVerticalPosition(pos int) {
+ m.outputViewport.YPosition = pos
+}
+
+func (m *interactiveOutputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ defer func() {
+ m.lastSeenSelector = m.sharedData.selector
+ m.lastSeenFormatIn = m.sharedData.formatIn
+ m.lastSeenFormatOut = m.sharedData.formatOut
+ m.lastSeenInput = m.sharedData.input
+ }()
+ firstUpdate := !m.hasUpdatedBefore
+ m.hasUpdatedBefore = true
+
+ queryChanged := m.lastSeenSelector != m.sharedData.selector ||
+ m.lastSeenFormatIn != m.sharedData.formatIn ||
+ m.lastSeenFormatOut != m.sharedData.formatOut ||
+ m.lastSeenInput != m.sharedData.input
+
+ // Take care of dasel execution + output.
+ if firstUpdate || queryChanged {
+ m.setOutput("Executing...")
+ out, err := m.run(m.sharedData.selector, m.root, m.sharedData.formatIn, m.sharedData.formatOut, m.sharedData.input)
+ if err != nil {
+ m.setOutput(err.Error())
+ } else {
+ m.setOutput(out)
+ }
+ }
+
+ m.outputViewport, cmd = m.outputViewport.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *interactiveOutputModel) View() string {
+ title := "Result"
+ if m.root {
+ title = "Root"
+ }
+
+ content := "Initializing..."
+ if m.outputViewportReady {
+ content = m.outputViewport.View()
+ }
+
+ return lipgloss.JoinVertical(lipgloss.Left, outputHeaderStyle.Render(title), outputContentStyle.Render(content))
+}
diff --git a/internal/cli/query.go b/internal/cli/query.go
new file mode 100644
index 00000000..f6e458aa
--- /dev/null
+++ b/internal/cli/query.go
@@ -0,0 +1,46 @@
+package cli
+
+import "fmt"
+
+type QueryCmd struct {
+ Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"`
+ ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"`
+ ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"`
+ InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."`
+ OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."`
+ ReturnRoot bool `flag:"" name:"root" help:"Return the root value."`
+ Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."`
+ Interactive bool `flag:"" name:"it" help:"Run in interactive mode."`
+
+ Query string `arg:"" help:"The query to execute." optional:"" default:""`
+}
+
+func (c *QueryCmd) Run(ctx *Globals) error {
+ if c.Interactive {
+ return NewInteractiveCmd(c).Run(ctx)
+ }
+
+ o := runOpts{
+ Vars: c.Vars,
+ ExtReadFlags: c.ExtReadFlags,
+ ExtWriteFlags: c.ExtWriteFlags,
+ InFormat: c.InFormat,
+ OutFormat: c.OutFormat,
+ ReturnRoot: c.ReturnRoot,
+ Unstable: c.Unstable,
+ Query: c.Query,
+
+ Stdin: ctx.Stdin,
+ }
+ outBytes, err := run(o)
+ if err != nil {
+ return err
+ }
+
+ _, err = ctx.Stdout.Write(outBytes)
+ if err != nil {
+ return fmt.Errorf("error writing output: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/cli/read_write_flag.go b/internal/cli/read_write_flag.go
new file mode 100644
index 00000000..d38e7c8e
--- /dev/null
+++ b/internal/cli/read_write_flag.go
@@ -0,0 +1,59 @@
+package cli
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/alecthomas/kong"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+type extReadWriteFlag struct {
+ Name string
+ Value string
+}
+
+type extReadWriteFlags *[]extReadWriteFlag
+
+func applyReaderFlags(readerOptions *parsing.ReaderOptions, f extReadWriteFlags) {
+ if f != nil {
+ for _, flag := range *f {
+ readerOptions.Ext[flag.Name] = flag.Value
+ }
+ }
+}
+
+func applyWriterFlags(writerOptions *parsing.WriterOptions, f extReadWriteFlags) {
+ if f != nil {
+ for _, flag := range *f {
+ writerOptions.Ext[flag.Name] = flag.Value
+ }
+ }
+}
+
+type extReadWriteFlagMapper struct {
+}
+
+func (vm *extReadWriteFlagMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
+ t := ctx.Scan.Pop()
+
+ strVal, ok := t.Value.(string)
+ if !ok {
+ return fmt.Errorf("expected string value for variable")
+ }
+
+ nameValueSplit := strings.SplitN(strVal, "=", 2)
+ if len(nameValueSplit) != 2 {
+ return fmt.Errorf("invalid read/write flag format, expect foo=bar")
+ }
+
+ res := extReadWriteFlag{
+ Name: nameValueSplit[0],
+ Value: nameValueSplit[1],
+ }
+
+ target.Elem().Set(reflect.Append(target.Elem(), reflect.ValueOf(res)))
+
+ return nil
+}
diff --git a/internal/cli/run.go b/internal/cli/run.go
new file mode 100644
index 00000000..baffccc0
--- /dev/null
+++ b/internal/cli/run.go
@@ -0,0 +1,99 @@
+package cli
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+type runOpts struct {
+ Vars variables
+ ExtReadFlags extReadWriteFlags
+ ExtWriteFlags extReadWriteFlags
+ InFormat string
+ OutFormat string
+ ReturnRoot bool
+ Unstable bool
+ Query string
+
+ Stdin io.Reader
+}
+
+func run(o runOpts) ([]byte, error) {
+ var opts []execution.ExecuteOptionFn
+
+ if o.OutFormat == "" && o.InFormat != "" {
+ o.OutFormat = o.InFormat
+ } else if o.OutFormat != "" && o.InFormat == "" {
+ o.InFormat = o.OutFormat
+ }
+
+ readerOptions := parsing.DefaultReaderOptions()
+ applyReaderFlags(&readerOptions, o.ExtReadFlags)
+
+ var reader parsing.Reader
+ var err error
+ if len(o.InFormat) > 0 {
+ reader, err = parsing.Format(o.InFormat).NewReader(readerOptions)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get input reader: %w", err)
+ }
+ }
+
+ writerOptions := parsing.DefaultWriterOptions()
+ applyWriterFlags(&writerOptions, o.ExtWriteFlags)
+
+ writer, err := parsing.Format(o.OutFormat).NewWriter(writerOptions)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get output writer: %w", err)
+ }
+
+ opts = append(opts, variableOptions(o.Vars)...)
+
+ // Default to null. If stdin is being read then this will be overwritten.
+ inputData := model.NewNullValue()
+
+ var inputBytes []byte
+ if o.Stdin != nil {
+ inputBytes, err = io.ReadAll(o.Stdin)
+ if err != nil {
+ return nil, fmt.Errorf("error reading stdin: %w", err)
+ }
+ }
+
+ if len(inputBytes) > 0 {
+ if reader == nil {
+ return nil, fmt.Errorf("input format is required when reading stdin")
+ }
+ inputData, err = reader.Read(inputBytes)
+ if err != nil {
+ return nil, fmt.Errorf("error reading input: %w", err)
+ }
+ }
+
+ opts = append(opts, execution.WithVariable("root", inputData))
+
+ if o.Unstable {
+ opts = append(opts, execution.WithUnstable())
+ }
+
+ options := execution.NewOptions(opts...)
+ out, err := execution.ExecuteSelector(o.Query, inputData, options)
+ if err != nil {
+ return nil, err
+ }
+
+ if o.ReturnRoot {
+ out = inputData
+ }
+
+ outputBytes, err := writer.Write(out)
+ if err != nil {
+ return nil, fmt.Errorf("error writing output: %w", err)
+ }
+
+ return outputBytes, nil
+}
diff --git a/internal/cli/variable.go b/internal/cli/variable.go
new file mode 100644
index 00000000..124ee7c3
--- /dev/null
+++ b/internal/cli/variable.go
@@ -0,0 +1,93 @@
+package cli
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+ "strings"
+
+ "github.com/alecthomas/kong"
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+type variable struct {
+ Name string
+ Value *model.Value
+}
+
+type variables *[]variable
+
+func variableOptions(vars variables) []execution.ExecuteOptionFn {
+ var opts []execution.ExecuteOptionFn
+ if vars != nil {
+ for _, v := range *vars {
+ opts = append(opts, execution.WithVariable(v.Name, v.Value))
+ }
+ }
+ return opts
+}
+
+type variableMapper struct {
+}
+
+// Decode decodes a variable from a flag.
+// E.g. --var foo=bar
+// E.g. --var foo=json:{"bar":"baz"}
+// E.g. --var foo=json:file:/path/to/file.json
+func (vm *variableMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
+ t := ctx.Scan.Pop()
+
+ strVal, ok := t.Value.(string)
+ if !ok {
+ return fmt.Errorf("expected string value for variable")
+ }
+
+ nameValueSplit := strings.SplitN(strVal, "=", 2)
+ if len(nameValueSplit) != 2 {
+ return fmt.Errorf("invalid variable format, expect foo=bar, or foo=format:file:path")
+ }
+
+ res := variable{
+ Name: nameValueSplit[0],
+ }
+
+ format := "dasel"
+ valueRaw := nameValueSplit[1]
+
+ firstSplit := strings.SplitN(valueRaw, ":", 2)
+ if len(firstSplit) == 2 {
+ format = firstSplit[0]
+ valueRaw = firstSplit[1]
+ }
+ if strings.HasPrefix(valueRaw, "file:") {
+ filePath := valueRaw[len("file:"):]
+ valueRaw = valueRaw[:len("file:")]
+
+ f, err := os.Open(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to open file: %w", err)
+ }
+ defer f.Close()
+ contents, err := io.ReadAll(f)
+ if err != nil {
+ return fmt.Errorf("failed to read file contents: %w", err)
+ }
+ valueRaw = string(contents)
+ }
+
+ reader, err := parsing.Format(format).NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ return fmt.Errorf("failed to create reader: %w", err)
+ }
+ res.Value, err = reader.Read([]byte(valueRaw))
+ if err != nil {
+ return fmt.Errorf("failed to read value: %w", err)
+ }
+
+ target.Elem().Set(reflect.Append(target.Elem(), reflect.ValueOf(res)))
+
+ return nil
+}
diff --git a/internal/cli/version.go b/internal/cli/version.go
new file mode 100644
index 00000000..5c306084
--- /dev/null
+++ b/internal/cli/version.go
@@ -0,0 +1,11 @@
+package cli
+
+import "github.com/tomwright/dasel/v3/internal"
+
+type VersionCmd struct {
+}
+
+func (c *VersionCmd) Run(ctx *Globals) error {
+ _, err := ctx.Stdout.Write([]byte(internal.Version + "\n"))
+ return err
+}
diff --git a/internal/command/delete.go b/internal/command/delete.go
deleted file mode 100644
index 06833fbe..00000000
--- a/internal/command/delete.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package command
-
-import (
- "github.com/spf13/cobra"
- "github.com/tomwright/dasel/v2"
-)
-
-func deleteCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "delete -f -r ",
- Short: "Delete properties from the given file.",
- RunE: deleteRunE,
- Args: cobra.MaximumNArgs(1),
- }
-
- deleteFlags(cmd)
-
- return cmd
-}
-
-func deleteFlags(cmd *cobra.Command) {
- cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.")
- cmd.Flags().StringP("read", "r", "", "The parser to use when reading.")
- cmd.Flags().StringP("file", "f", "", "The file to query.")
- cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.")
- cmd.Flags().StringP("out", "o", "", "The file to write output to.")
- cmd.Flags().Bool("pretty", true, "Pretty print the output.")
- cmd.Flags().Bool("colour", false, "Print colourised output.")
- cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.")
- cmd.Flags().Int("indent", 2, "The indention level when writing files.")
- cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
- cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
- cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
- cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
-
- _ = cmd.MarkFlagFilename("file")
-}
-
-func deleteRunE(cmd *cobra.Command, args []string) error {
- selectorFlag, _ := cmd.Flags().GetString("selector")
- readParserFlag, _ := cmd.Flags().GetString("read")
- fileFlag, _ := cmd.Flags().GetString("file")
- writeParserFlag, _ := cmd.Flags().GetString("write")
- prettyPrintFlag, _ := cmd.Flags().GetBool("pretty")
- colourFlag, _ := cmd.Flags().GetBool("colour")
- escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html")
- outFlag, _ := cmd.Flags().GetString("out")
- indent, _ := cmd.Flags().GetInt("indent")
- csvComma, _ := cmd.Flags().GetString("csv-comma")
- csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma")
- csvComment, _ := cmd.Flags().GetString("csv-comment")
- csvCRLF, _ := cmd.Flags().GetBool("csv-crlf")
-
- opts := &deleteOptions{
- Read: &readOptions{
- Reader: nil,
- Parser: readParserFlag,
- FilePath: fileFlag,
- CsvComma: csvComma,
- CsvComment: csvComment,
- },
- Write: &writeOptions{
- Writer: nil,
- Parser: writeParserFlag,
- FilePath: outFlag,
- PrettyPrint: prettyPrintFlag,
- Colourise: colourFlag,
- EscapeHTML: escapeHTMLFlag,
- Indent: indent,
- CsvComma: csvWriteComma,
- CsvUseCRLF: csvCRLF,
- },
- Selector: selectorFlag,
- }
-
- if opts.Selector == "" && len(args) > 0 {
- opts.Selector = args[0]
- args = args[1:]
- }
-
- if opts.Selector == "" {
- opts.Selector = "."
- }
-
- if opts.Write.FilePath == "" {
- opts.Write.FilePath = opts.Read.FilePath
- }
-
- return runDeleteCommand(opts, cmd)
-}
-
-type deleteOptions struct {
- Read *readOptions
- Write *writeOptions
- Selector string
-}
-
-func runDeleteCommand(opts *deleteOptions, cmd *cobra.Command) error {
-
- rootValue, err := opts.Read.rootValue(cmd)
- if err != nil {
- return err
- }
-
- value, err := dasel.Delete(rootValue, opts.Selector)
- if err != nil {
- return err
- }
-
- if err := opts.Write.writeValue(cmd, opts.Read, value); err != nil {
- return err
- }
-
- return nil
-}
diff --git a/internal/command/delete_test.go b/internal/command/delete_test.go
deleted file mode 100644
index 9399d008..00000000
--- a/internal/command/delete_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package command
-
-import (
- "testing"
-)
-
-func TestDeleteCommand(t *testing.T) {
-
- t.Run("DeleteMapField", runTest(
- []string{"delete", "-r", "json", "--pretty=false", "x"},
- []byte(`{"x":1,"y":2}`),
- newline([]byte(`{"y":2}`)),
- nil,
- nil,
- ))
-
- t.Run("DeleteNestedMapField", runTest(
- []string{"delete", "-r", "json", "--pretty=false", "x.y"},
- []byte(`{"x":{"x":1,"y":2},"y":{"x":1,"y":2}}`),
- newline([]byte(`{"x":{"x":1},"y":{"x":1,"y":2}}`)),
- nil,
- nil,
- ))
-
- t.Run("DeleteSliceIndex", runTest(
- []string{"delete", "-r", "json", "--pretty=false", "[1]"},
- []byte(`[0,1,2]`),
- newline([]byte(`[0,2]`)),
- nil,
- nil,
- ))
-
- t.Run("DeletedNestedSliceIndex", runTest(
- []string{"delete", "-r", "json", "--pretty=false", "users.[1]"},
- []byte(`{"users":[0,1,2]}`),
- newline([]byte(`{"users":[0,2]}`)),
- nil,
- nil,
- ))
-
- t.Run("CheckIndentionForJSON", runTest(
- []string{"delete", "-r", "json", "--indent", "6", "--pretty=true", "x.y"},
- []byte(`{"x":{"x":1,"y":2}}`),
- newline([]byte("{\n \"x\": {\n \"x\": 1\n }\n}")),
- nil,
- nil,
- ))
-
- t.Run("CheckIndentionForYAML", runTest(
- []string{"delete", "-r", "json", "-w", "yaml", "--indent", "6", "--pretty=true", "x.y"},
- []byte(`{"x":{"x":1,"y":2}}`),
- newline([]byte("x:\n x: 1")),
- nil,
- nil,
- ))
-
- t.Run("CheckIndentionForTOML", runTest(
- []string{"delete", "-r", "json", "-w", "toml", "--indent", "6", "--pretty=true", "x.y"},
- []byte(`{"x":{"x":1,"y":2}}`),
- newline([]byte("[x]\n x = 1")),
- nil,
- nil,
- ))
-
- t.Run("Issue346", func(t *testing.T) {
- t.Run("DeleteNullValue", runTest(
- []string{"delete", "-r", "json", "foo"},
- []byte(`{"foo":null}`),
- newline([]byte("{}")),
- nil,
- nil,
- ))
- })
-}
diff --git a/internal/command/man.go b/internal/command/man.go
deleted file mode 100644
index 52720329..00000000
--- a/internal/command/man.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package command
-
-import (
- "github.com/spf13/cobra"
- "github.com/spf13/cobra/doc"
-)
-
-func manCommand(root *cobra.Command) *cobra.Command {
- // Do not include timestamp in generated man pages.
- // See https://github.com/spf13/cobra/issues/142
- root.DisableAutoGenTag = true
-
- cmd := &cobra.Command{
- Use: "man -o ",
- Short: "Generate manual pages for all dasel subcommands",
- RunE: func(cmd *cobra.Command, args []string) error {
- return manRunE(cmd, root)
- },
- }
-
- cmd.Flags().StringP("output-directory", "o", ".", "The directory in which man pages will be created")
-
- return cmd
-}
-
-func manRunE(cmd *cobra.Command, root *cobra.Command) error {
- outputDirectory, _ := cmd.Flags().GetString("output-directory")
-
- return doc.GenManTree(root, nil, outputDirectory)
-}
diff --git a/internal/command/man_test.go b/internal/command/man_test.go
deleted file mode 100644
index 9588bf13..00000000
--- a/internal/command/man_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package command
-
-import (
- "os"
- "testing"
-)
-
-func TestManCommand(t *testing.T) {
- tempDir := t.TempDir()
-
- _, _, err := runDasel([]string{"man", "-o", tempDir}, nil)
- if err != nil {
- t.Fatalf("expected no error, got %v", err)
- }
-
- files, err := os.ReadDir(tempDir)
- if err != nil {
- t.Fatalf("expected no error, got %v", err)
- }
-
- expectedFiles := []string{
- "dasel-completion-bash.1",
- "dasel-completion-fish.1",
- "dasel-completion-powershell.1",
- "dasel-completion-zsh.1",
- "dasel-completion.1",
- "dasel-delete.1",
- "dasel-man.1",
- "dasel-put.1",
- "dasel-validate.1",
- "dasel.1",
- }
-
- if len(files) != len(expectedFiles) {
- t.Fatalf("expected %d files, got %d", len(expectedFiles), len(files))
- }
-
- for i, f := range files {
- if f.Name() != expectedFiles[i] {
- t.Fatalf("expected %v, got %v", expectedFiles[i], f.Name())
- }
- }
-}
diff --git a/internal/command/options.go b/internal/command/options.go
deleted file mode 100644
index 910b161a..00000000
--- a/internal/command/options.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package command
-
-import (
- "fmt"
- "io"
- "os"
- "strings"
-
- "github.com/spf13/cobra"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/storage"
-)
-
-type readOptions struct {
- // Reader is an io.Reader that we should read from instead of FilePath.
- Reader io.Reader
- // Parser is the name of the parser we should use when reading.
- Parser string
- // FilePath is the path to the source file.
- FilePath string
- // CsvComma is the comma character used when reading CSV files.
- CsvComma string
- // CsvComment is the comment character used when reading CSV files.
- CsvComment string
-}
-
-func (o *readOptions) readFromStdin() bool {
- return o.FilePath == "" || o.FilePath == "stdin" || o.FilePath == "-"
-}
-
-func (o *readOptions) readParser() (storage.ReadParser, error) {
- useStdin := o.readFromStdin()
-
- if useStdin && o.Parser == "" {
- return nil, fmt.Errorf("read parser required when reading from stdin")
- }
-
- if o.Parser == "" {
- parser, err := storage.NewReadParserFromFilename(o.FilePath)
- if err != nil {
- return nil, fmt.Errorf("could not get read parser from filename: %w", err)
- }
- return parser, nil
- }
- parser, err := storage.NewReadParserFromString(o.Parser)
- if err != nil {
- return nil, fmt.Errorf("could not get read parser: %w", err)
- }
- return parser, nil
-}
-
-func (o *readOptions) rootValue(cmd *cobra.Command) (dasel.Value, error) {
- parser, err := o.readParser()
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not get read parser: %w", err)
- }
-
- options := make([]storage.ReadWriteOption, 0)
- if o.CsvComma != "" {
- options = append(options, storage.CsvCommaOption([]rune(o.CsvComma)[0]))
- }
- if o.CsvComment != "" {
- options = append(options, storage.CsvCommentOption([]rune(o.CsvComment)[0]))
- }
-
- reader := o.Reader
- if reader == nil {
- if o.readFromStdin() {
- reader = cmd.InOrStdin()
- } else {
- f, err := os.Open(o.FilePath)
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not open file for reading: %s: %w", o.FilePath, err)
- }
- defer f.Close()
- reader = f
- }
- }
-
- rootNode, err := storage.Load(parser, reader, options...)
- if err != nil {
- return rootNode, err
- }
-
- if !rootNode.Value.IsValid() {
- var defaultValue any = dencoding.NewMap()
- if _, ok := parser.(*storage.CSVParser); ok {
- defaultValue = []any{}
- }
- rootNode = dasel.ValueOf(defaultValue)
- }
-
- return rootNode, nil
-}
-
-type writeOptions struct {
- // Writer is an io.Writer that we should write to instead of FilePath.
- Writer io.Writer
- // Parser is the name of the parser we should use when reading.
- Parser string
- // FilePath is the path to the source file.
- FilePath string
-
- PrettyPrint bool
- Colourise bool
- EscapeHTML bool
-
- // CsvComma is the comma character used when writing CSV files.
- CsvComma string
- // CsvUseCRLF determines whether CRLF is used when writing CSV files.
- CsvUseCRLF bool
-
- Indent int
-}
-
-func (o *writeOptions) writeToStdout() bool {
- return o.FilePath == "" || o.FilePath == "stdout" || o.FilePath == "-"
-}
-
-func (o *writeOptions) writeParser(readOptions *readOptions) (storage.WriteParser, error) {
- if o.writeToStdout() && o.Parser == "" {
- if readOptions != nil {
- o.Parser = readOptions.Parser
- }
- }
-
- if o.writeToStdout() && o.Parser == "" && readOptions != nil && readOptions.FilePath != "" {
- parser, err := storage.NewWriteParserFromFilename(readOptions.FilePath)
- if err != nil {
- return nil, fmt.Errorf("could not get write parser from read filename: %w", err)
- }
- return parser, nil
- }
- if o.Parser == "" {
- parser, err := storage.NewWriteParserFromFilename(o.FilePath)
- if err != nil {
- return nil, fmt.Errorf("could not get write parser from filename: %w", err)
- }
- return parser, nil
- }
- parser, err := storage.NewWriteParserFromString(o.Parser)
- if err != nil {
- return nil, fmt.Errorf("could not get write parser: %w", err)
- }
- return parser, nil
-}
-
-func (o *writeOptions) writeValues(cmd *cobra.Command, readOptions *readOptions, values dasel.Values) error {
- parser, err := o.writeParser(readOptions)
- if err != nil {
- return err
- }
-
- options := []storage.ReadWriteOption{
- storage.ColouriseOption(o.Colourise),
- storage.EscapeHTMLOption(o.EscapeHTML),
- storage.PrettyPrintOption(o.PrettyPrint),
- storage.CsvUseCRLFOption(o.CsvUseCRLF),
- }
-
- if o.CsvComma == "" && readOptions.CsvComma != "" {
- o.CsvComma = readOptions.CsvComma
- }
-
- if o.CsvComma != "" {
- options = append(options, storage.CsvCommaOption([]rune(o.CsvComma)[0]))
- }
-
- if o.Indent != 0 {
- options = append(options, storage.IndentOption(strings.Repeat(" ", o.Indent)))
- }
-
- writer := o.Writer
- if writer == nil {
- if o.writeToStdout() {
- writer = cmd.OutOrStdout()
- } else {
- f, err := os.Create(o.FilePath)
- if err != nil {
- return fmt.Errorf("could not open file for writing: %s: %w", o.FilePath, err)
- }
- defer f.Close()
- writer = f
- }
- }
-
- for _, value := range values {
- valueBytes, err := parser.ToBytes(value, options...)
- if err != nil {
- return err
- }
-
- if _, err := writer.Write(valueBytes); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (o *writeOptions) writeValue(cmd *cobra.Command, readOptions *readOptions, value dasel.Value) error {
- return o.writeValues(cmd, readOptions, dasel.Values{value})
-}
diff --git a/internal/command/put.go b/internal/command/put.go
deleted file mode 100644
index 6ec1b7a2..00000000
--- a/internal/command/put.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package command
-
-import (
- "fmt"
- "github.com/spf13/cobra"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/storage"
- "strconv"
-)
-
-func putCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "put -t -v -f -r ",
- Short: "Write properties to the given file.",
- RunE: putRunE,
- Args: cobra.MaximumNArgs(1),
- }
-
- putFlags(cmd)
-
- return cmd
-}
-
-func putFlags(cmd *cobra.Command) {
- cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.")
- cmd.Flags().StringP("read", "r", "", "The parser to use when reading.")
- cmd.Flags().StringP("file", "f", "", "The file to query.")
- cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.")
- cmd.Flags().StringP("type", "t", "string", "The type of variable being written.")
- cmd.Flags().StringP("value", "v", "", "The value to write.")
- cmd.Flags().StringP("out", "o", "", "The file to write output to.")
- cmd.Flags().Bool("pretty", true, "Pretty print the output.")
- cmd.Flags().Bool("colour", false, "Print colourised output.")
- cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.")
- cmd.Flags().Int("indent", 2, "The indention level when writing files.")
- cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
- cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
- cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
- cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
-
- _ = cmd.MarkFlagFilename("file")
-}
-
-func putRunE(cmd *cobra.Command, args []string) error {
- selectorFlag, _ := cmd.Flags().GetString("selector")
- readParserFlag, _ := cmd.Flags().GetString("read")
- fileFlag, _ := cmd.Flags().GetString("file")
- writeParserFlag, _ := cmd.Flags().GetString("write")
- typeFlag, _ := cmd.Flags().GetString("type")
- valueFlag, _ := cmd.Flags().GetString("value")
- prettyPrintFlag, _ := cmd.Flags().GetBool("pretty")
- colourFlag, _ := cmd.Flags().GetBool("colour")
- escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html")
- outFlag, _ := cmd.Flags().GetString("out")
- indent, _ := cmd.Flags().GetInt("indent")
- csvComma, _ := cmd.Flags().GetString("csv-comma")
- csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma")
- csvComment, _ := cmd.Flags().GetString("csv-comment")
- csvCRLF, _ := cmd.Flags().GetBool("csv-crlf")
-
- opts := &putOptions{
- Read: &readOptions{
- Reader: nil,
- Parser: readParserFlag,
- FilePath: fileFlag,
- CsvComma: csvComma,
- CsvComment: csvComment,
- },
- Write: &writeOptions{
- Writer: nil,
- Parser: writeParserFlag,
- FilePath: outFlag,
- PrettyPrint: prettyPrintFlag,
- Colourise: colourFlag,
- EscapeHTML: escapeHTMLFlag,
- Indent: indent,
- CsvComma: csvWriteComma,
- CsvUseCRLF: csvCRLF,
- },
- Selector: selectorFlag,
- ValueType: typeFlag,
- Value: valueFlag,
- }
-
- if opts.Selector == "" && len(args) > 0 {
- opts.Selector = args[0]
- args = args[1:]
- }
-
- if opts.Selector == "" {
- opts.Selector = "."
- }
-
- if opts.Write.FilePath == "" {
- opts.Write.FilePath = opts.Read.FilePath
- }
-
- return runPutCommand(opts, cmd)
-}
-
-type putOptions struct {
- Read *readOptions
- Write *writeOptions
- Selector string
- ValueType string
- Value string
-}
-
-func runPutCommand(opts *putOptions, cmd *cobra.Command) error {
-
- rootValue, err := opts.Read.rootValue(cmd)
- if err != nil {
- return err
- }
-
- var toSet interface{}
-
- switch opts.ValueType {
- case "string":
- toSet = opts.Value
- case "int":
- intValue, err := strconv.ParseInt(opts.Value, 0, 64)
- if err != nil {
- return fmt.Errorf("invalid int value: %w", err)
- }
- toSet = intValue
- case "float":
- floatValue, err := strconv.ParseFloat(opts.Value, 64)
- if err != nil {
- return fmt.Errorf("invalid float value: %w", err)
- }
- toSet = floatValue
- case "bool":
- toSet = dasel.ValueOf(dasel.IsTruthy(opts.Value))
- default:
- readParser, err := storage.NewReadParserFromString(opts.ValueType)
- if err != nil {
- return fmt.Errorf("unhandled value type")
- }
- docValue, err := readParser.FromBytes([]byte(opts.Value))
- if err != nil {
- return fmt.Errorf("could not parse document: %w", err)
- }
- toSet = docValue
- }
-
- value, err := dasel.Put(rootValue, opts.Selector, toSet)
- if err != nil {
- return err
- }
-
- if err := opts.Write.writeValue(cmd, opts.Read, value); err != nil {
- return err
- }
-
- return nil
-}
diff --git a/internal/command/put_test.go b/internal/command/put_test.go
deleted file mode 100644
index 13545381..00000000
--- a/internal/command/put_test.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package command
-
-import (
- "fmt"
- "testing"
-)
-
-func TestPutCommand(t *testing.T) {
-
- t.Run("SetTypeOnExistingProperty", func(t *testing.T) {
- tests := []struct {
- name string
- t string
- value string
- exp string
- }{
- {
- t: "string",
- value: "some string",
- exp: `"some string"`,
- },
- {
- t: "int",
- value: "123",
- exp: `123`,
- },
- {
- name: "float round number",
- t: "float",
- value: "123",
- exp: `123`,
- },
- {
- name: "float 1 decimal place",
- t: "float",
- value: "123.4",
- exp: `123.4`,
- },
- {
- name: "float 5 decimal place",
- t: "float",
- value: "123.45678",
- exp: `123.45678`,
- },
- {
- name: "true bool",
- t: "bool",
- value: "true",
- exp: `true`,
- },
- {
- name: "false bool",
- t: "bool",
- value: "false",
- exp: `false`,
- },
- {
- t: "json",
- value: `{"some":"json"}`,
- exp: `{"some":"json"}`,
- },
- }
-
- for _, test := range tests {
- tc := test
- if tc.name == "" {
- tc.name = tc.t
- }
- t.Run(tc.name, runTest(
- []string{"put", "-r", "json", "-t", tc.t, "--pretty=false", "-v", tc.value, "val"},
- []byte(`{"val":"oldVal"}`),
- newline([]byte(fmt.Sprintf(`{"val":%s}`, tc.exp))),
- nil,
- nil,
- ))
- }
- })
-
- t.Run("SetStringOnExistingNestedProperty", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"},
- []byte(`{"user":{"name":"oldName"}}`),
- newline([]byte(`{"user":{"name":"Tom"}}`)),
- nil,
- nil,
- ))
-
- t.Run("CreateStringProperty", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "name"},
- []byte(`{}`),
- newline([]byte(`{"name":"Tom"}`)),
- nil,
- nil,
- ))
-
- t.Run("CreateNestedStringPropertyOnExistingParent", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"},
- []byte(`{"user":{}}`),
- newline([]byte(`{"user":{"name":"Tom"}}`)),
- nil,
- nil,
- ))
-
- t.Run("CreateNestedStringPropertyOnMissingParent", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"},
- []byte(`{}`),
- newline([]byte(`{"user":{"name":"Tom"}}`)),
- nil,
- nil,
- ))
-
- t.Run("SetStringOnExistingIndex", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[1]"},
- []byte(`["a","b","c"]`),
- newline([]byte(`["a","z","c"]`)),
- nil,
- nil,
- ))
-
- t.Run("SetStringOnExistingNestedIndex", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[0].[1]"},
- []byte(`[["a","b","c"],["d","e","f"]]`),
- newline([]byte(`[["a","z","c"],["d","e","f"]]`)),
- nil,
- nil,
- ))
-
- t.Run("AppendStringIndexToRoot", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[]"},
- []byte(`[]`),
- newline([]byte(`["z"]`)),
- nil,
- nil,
- ))
-
- t.Run("AppendStringIndexToNestedSlice", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[0].[]"},
- []byte(`[[]]`),
- newline([]byte(`[["z"]]`)),
- nil,
- nil,
- ))
-
- t.Run("AppendToChainOfMissingSlicesAndProperties", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[].name.first"},
- []byte(`{}`),
- newline([]byte(`{"users":[{"name":{"first":"Tom"}}]}`)),
- nil,
- nil,
- ))
-
- t.Run("AppendToEmptyExistingSlice", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[]"},
- []byte(`{"users":[]}`),
- newline([]byte(`{"users":["Tom"]}`)),
- nil,
- nil,
- ))
-
- t.Run("AppendToEmptyMissingSlice", runTest(
- []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[]"},
- []byte(`{}`),
- newline([]byte(`{"users":["Tom"]}`)),
- nil,
- nil,
- ))
-
- // https://github.com/TomWright/dasel/issues/327
- t.Run("Yaml0xStringQuoted", runTest(
- []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "0x12_11", "t"},
- []byte(`t:`),
- newline([]byte(`t: "0x12_11"`)),
- nil,
- nil,
- ))
-
- t.Run("YamlBoolLikeStringTrue", runTest(
- []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "true", "t"},
- []byte(`t:`),
- newline([]byte(`t: "true"`)),
- nil,
- nil,
- ))
-
- t.Run("YamlBoolLikeStringFalse", runTest(
- []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "false", "t"},
- []byte(`t:`),
- newline([]byte(`t: "false"`)),
- nil,
- nil,
- ))
-
- t.Run("CsvChangeSeparator", runTest(
- []string{"put", "-r", "csv", "-t", "int", "-v", "5", "--csv-write-comma", ".", "[0].a"},
- []byte(`a,b
-1,2
-3,4`),
- newline([]byte(`a.b
-5.2
-3.4`)),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForJSON", runTest(
- []string{"put", "-r", "json", "--indent", "6", "-t", "string", "--pretty=true", "-v", "Tom", "user"},
- []byte(`{}`),
- newline([]byte("{\n \"user\": \"Tom\"\n}")),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForYAML", runTest(
- []string{"put", "-r", "yaml", "--indent", "6", "-t", "string", "-v", "Tom", "user.name"},
- []byte(``),
- newline([]byte("user:\n name: Tom")),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForTOML", runTest(
- []string{"put", "-r", "toml", "--indent", "6", "-t", "string", "-v", "Tom", "user.name"},
- []byte(``),
- newline([]byte("[user]\n name = 'Tom'")),
- nil,
- nil,
- ))
-}
diff --git a/internal/command/root.go b/internal/command/root.go
deleted file mode 100644
index 7e56f749..00000000
--- a/internal/command/root.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package command
-
-import (
- "github.com/spf13/cobra"
- "github.com/tomwright/dasel/v2/internal"
-)
-
-// NewRootCMD returns the root command for use with cobra.
-func NewRootCMD() *cobra.Command {
- selectCmd := selectCommand()
- selectCmd.SilenceErrors = true
- selectCmd.SilenceUsage = true
- selectCmd.Version = internal.Version
-
- selectCmd.AddCommand(
- putCommand(),
- deleteCommand(),
- validateCommand(),
- )
-
- manCmd := manCommand(selectCmd)
- selectCmd.AddCommand(manCmd)
-
- return selectCmd
-}
diff --git a/internal/command/root_test.go b/internal/command/root_test.go
deleted file mode 100644
index 21829a65..00000000
--- a/internal/command/root_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package command
-
-import (
- "bytes"
- "errors"
- "reflect"
- "testing"
-)
-
-// Runs the dasel root command.
-// Returns stdout, stderr and error.
-func runDasel(args []string, in []byte) ([]byte, []byte, error) {
- stdOut := bytes.NewBuffer([]byte{})
- stdErr := bytes.NewBuffer([]byte{})
-
- cmd := NewRootCMD()
- cmd.SetArgs(args)
- cmd.SetOut(stdOut)
- cmd.SetErr(stdErr)
-
- if in != nil {
- cmd.SetIn(bytes.NewReader(in))
- }
-
- err := cmd.Execute()
- return stdOut.Bytes(), stdErr.Bytes(), err
-}
-
-func runTest(args []string, in []byte, expStdOut []byte, expStdErr []byte, expErr error) func(t *testing.T) {
- return func(t *testing.T) {
- if expStdOut == nil {
- expStdOut = []byte{}
- }
- if expStdErr == nil {
- expStdErr = []byte{}
- }
-
- gotStdOut, gotStdErr, gotErr := runDasel(args, in)
- if expErr != gotErr && !errors.Is(expErr, gotErr) {
- t.Errorf("expected error %v, got %v", expErr, gotErr)
- return
- }
-
- if !reflect.DeepEqual(expStdErr, gotStdErr) {
- t.Errorf("expected stderr %s, got %s", string(expStdErr), string(gotStdErr))
- }
-
- if !reflect.DeepEqual(expStdOut, gotStdOut) {
- t.Errorf("expected stdout %s, got %s", string(expStdOut), string(gotStdOut))
- }
- }
-}
-
-var newlineBytes = []byte("\n")
-
-func newline(input []byte) []byte {
- return append(input, newlineBytes...)
-}
diff --git a/internal/command/select.go b/internal/command/select.go
deleted file mode 100644
index 0e6816b9..00000000
--- a/internal/command/select.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package command
-
-import (
- "github.com/spf13/cobra"
- "github.com/tomwright/dasel/v2"
-)
-
-func selectCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "dasel -f -r ",
- Short: "Select properties from the given file.",
- RunE: selectRunE,
- Args: cobra.MaximumNArgs(1),
- }
-
- selectFlags(cmd)
-
- return cmd
-}
-
-func selectFlags(cmd *cobra.Command) {
- cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.")
- cmd.Flags().StringP("read", "r", "", "The parser to use when reading.")
- cmd.Flags().StringP("file", "f", "", "The file to query.")
- cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.")
- cmd.Flags().Bool("pretty", true, "Pretty print the output.")
- cmd.Flags().Bool("colour", false, "Print colourised output.")
- cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.")
- cmd.Flags().Int("indent", 2, "The indention level when writing files.")
- cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
- cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
- cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
- cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
-
- _ = cmd.MarkFlagFilename("file")
-}
-
-func selectRunE(cmd *cobra.Command, args []string) error {
- selectorFlag, _ := cmd.Flags().GetString("selector")
- readParserFlag, _ := cmd.Flags().GetString("read")
- fileFlag, _ := cmd.Flags().GetString("file")
- writeParserFlag, _ := cmd.Flags().GetString("write")
- prettyPrintFlag, _ := cmd.Flags().GetBool("pretty")
- colourFlag, _ := cmd.Flags().GetBool("colour")
- escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html")
- indent, _ := cmd.Flags().GetInt("indent")
- csvComma, _ := cmd.Flags().GetString("csv-comma")
- csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma")
- csvComment, _ := cmd.Flags().GetString("csv-comment")
- csvCRLF, _ := cmd.Flags().GetBool("csv-crlf")
-
- opts := &selectOptions{
- Read: &readOptions{
- Reader: nil,
- Parser: readParserFlag,
- FilePath: fileFlag,
- CsvComma: csvComma,
- CsvComment: csvComment,
- },
- Write: &writeOptions{
- Writer: nil,
- Parser: writeParserFlag,
- FilePath: "stdout",
- PrettyPrint: prettyPrintFlag,
- Colourise: colourFlag,
- EscapeHTML: escapeHTMLFlag,
- Indent: indent,
- CsvComma: csvWriteComma,
- CsvUseCRLF: csvCRLF,
- },
- Selector: selectorFlag,
- }
-
- if opts.Selector == "" && len(args) > 0 {
- opts.Selector = args[0]
- args = args[1:]
- }
-
- if opts.Selector == "" {
- opts.Selector = "."
- }
-
- return runSelectCommand(opts, cmd)
-}
-
-type selectOptions struct {
- Read *readOptions
- Write *writeOptions
- Selector string
-}
-
-func runSelectCommand(opts *selectOptions, cmd *cobra.Command) error {
-
- rootValue, err := opts.Read.rootValue(cmd)
- if err != nil {
- return err
- }
-
- values, err := dasel.Select(rootValue, opts.Selector)
- if err != nil {
- return err
- }
-
- if err := opts.Write.writeValues(cmd, opts.Read, values); err != nil {
- return err
- }
-
- return nil
-}
diff --git a/internal/command/select_test.go b/internal/command/select_test.go
deleted file mode 100644
index 8841df71..00000000
--- a/internal/command/select_test.go
+++ /dev/null
@@ -1,511 +0,0 @@
-package command
-
-import (
- "testing"
-)
-
-func standardJsonSelectTestData() []byte {
- return []byte(`{
- "users": [
- {
- "name": {
- "first": "Tom",
- "last": "Wright"
- },
- "flags": {
- "isBanned": false
- }
- },
- {
- "name": {
- "first": "Jim",
- "last": "Wright"
- },
- "flags": {
- "isBanned": true
- }
- },
- {
- "name": {
- "first": "Joe",
- "last": "Blogs"
- },
- "flags": {
- "isBanned": false
- }
- }
- ]
-}`)
-}
-
-func TestSelectCommand(t *testing.T) {
-
- t.Run("TotalUsersLen", runTest(
- []string{"-r", "json", "--pretty=false", "users.len()"},
- standardJsonSelectTestData(),
- newline([]byte(`3`)),
- nil,
- nil,
- ))
-
- t.Run("TotalUsersCount", runTest(
- []string{"-r", "json", "--pretty=false", "users.all().count()"},
- standardJsonSelectTestData(),
- newline([]byte(`3`)),
- nil,
- nil,
- ))
-
- t.Run("TotalBannedUsers", runTest(
- []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,true)).count()"},
- standardJsonSelectTestData(),
- newline([]byte(`1`)),
- nil,
- nil,
- ))
-
- t.Run("TotalNotBannedUsers", runTest(
- []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,false)).count()"},
- standardJsonSelectTestData(),
- newline([]byte(`2`)),
- nil,
- nil,
- ))
-
- t.Run("NotBannedUsers", runTest(
- []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,false)).name.first"},
- standardJsonSelectTestData(),
- newline([]byte(`"Tom"
-"Joe"`)),
- nil,
- nil,
- ))
-
- t.Run("BannedUsers", runTest(
- []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,true)).name.first"},
- standardJsonSelectTestData(),
- newline([]byte(`"Jim"`)),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForJSON", runTest(
- []string{"-r", "json", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true)).name"},
- standardJsonSelectTestData(),
- newline([]byte("{\n \"first\": \"Jim\",\n \"last\": \"Wright\"\n}")),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForYAML", runTest(
- []string{"-r", "json", "-w", "yaml", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true))"},
- standardJsonSelectTestData(),
- newline([]byte("name:\n first: Jim\n last: Wright\nflags:\n isBanned: true")),
- nil,
- nil,
- ))
-
- t.Run("VerifyCorrectIndentionForTOML", runTest(
- []string{"-r", "json", "-w", "toml", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true))"},
- standardJsonSelectTestData(),
- newline([]byte("[flags]\n isBanned = true\n\n[name]\n first = 'Jim'\n last = 'Wright'")),
- nil,
- nil,
- ))
-
- t.Run("Issue258", runTest(
- []string{"-r", "json", "--pretty=false", "-w", "csv", "phones.all().mapOf(make,make,model,model,first,parent().parent().user.name.first,last,parent().parent().user.name.last).merge()"},
- []byte(`{
- "id": "1234",
- "user": {
- "name": {
- "first": "Tom",
- "last": "Wright"
- }
- },
- "favouriteNumbers": [
- 1, 2, 3, 4
- ],
- "favouriteColours": [
- "red", "green"
- ],
- "phones": [
- {
- "make": "OnePlus",
- "model": "8 Pro"
- },
- {
- "make": "Apple",
- "model": "iPhone 12"
- }
- ]
- }`),
- newline([]byte(`first,last,make,model
-Tom,Wright,OnePlus,8 Pro
-Tom,Wright,Apple,iPhone 12`)),
- nil,
- nil,
- ))
-
- t.Run("Issue181", runTest(
- []string{"-r", "json", "--pretty=false", "all().filter(equal(this(),README.md))"},
- []byte(`[
- "README.md",
- "tbump.toml"
-]`),
- newline([]byte(`"README.md"`)),
- nil,
- nil,
- ))
-
- // Flaky test due to ordering
- // t.Run("Discussion242", runTest(
- // []string{"-r", "json", "--pretty=false", "-w", "plain", "all().filter(equal(type(),array)).key()"},
- // []byte(`{
- // "array1": [
- // {
- // "a": "aaa",
- // "b": "bbb",
- // "c": "ccc"
- // }
- // ],
- // "array2": [
- // {
- // "a": "aaa",
- // "b": "bbb",
- // "c": "ccc"
- // }
- // ]
- // }`),
- // newline([]byte(`array1
- // array2`)),
- // nil,
- // nil,
- // ))
-
- t.Run("YamlMultiDoc/Issue314", runTest(
- []string{"-r", "yaml", ""},
- []byte(`a: x
-b: foo
----
-a: y
-c: bar
-`),
- newline([]byte(`a: x
-b: foo
----
-a: y
-c: bar`)),
- nil,
- nil,
- ))
-
- t.Run("Issue316", runTest(
- []string{"-r", "json"},
- []byte(`{
- "a": "alice",
- "b": null,
- "c": [
- {
- "d": 9,
- "e": null
- },
- null
- ]
-}`),
- newline([]byte(`{
- "a": "alice",
- "b": null,
- "c": [
- {
- "d": 9,
- "e": null
- },
- null
- ]
-}`)),
- nil,
- nil,
- ))
-
- // Hex, binary and octal values in YAML
- t.Run("Issue326", runTest(
- []string{"-r", "yaml"},
- []byte(`hex: 0x1234
-binary: 0b1001
-octal: 0o10
-`),
- newline([]byte(`hex: 4660
-binary: 9
-octal: 8`)),
- nil,
- nil,
- ))
-
- t.Run("Issue331 - YAML to JSON", runTest(
- []string{"-r", "yaml", "-w", "json"},
- []byte(`createdAt: 2023-06-13T20:19:48.531620935Z`),
- newline([]byte(`{
- "createdAt": "2023-06-13T20:19:48.531620935Z"
-}`)),
- nil,
- nil,
- ))
-
- t.Run("Issue285 - YAML alias on read", runTest(
- []string{"-r", "yaml", "-w", "yaml"},
- []byte(`foo: &foofoo
- bar: 1
- baz: &baz "baz"
-spam:
- ham: "eggs"
- bar: 0
- <<: *foofoo
- baz: "bazbaz"
-
-baz: *baz
-`),
- []byte(`foo:
- bar: 1
- baz: baz
-spam:
- ham: eggs
- bar: 1
- baz: bazbaz
-baz: baz
-`),
- nil,
- nil,
- ))
-
- t.Run("OrDefaultString", runTest(
- []string{"-r", "json", "all().orDefault(locale,string(nope))"},
- []byte(`{
- "-LCr5pXw_fN32IqNDr4E": {
- "bookCategory": "poetry",
- "locale": "en-us",
- "mediaType": "book",
- "publisher": "Pomelo Books",
- "title": "Sound Waves",
- "type": "poetry"
- },
- "-LDDHjkdY0306fZdvhEQ": {
- "ISBN13": "978-1534402966",
- "bookCategory": "fiction",
- "title": "What Can You Do with a Toolbox?",
- "type": "picturebook"
- }
-}`),
- newline([]byte(`"en-us"
-"nope"`)),
- nil,
- nil,
- ))
-
- t.Run("OrDefaultLookup", runTest(
- []string{"-r", "json", "all().orDefault(locale,bookCategory)"},
- []byte(`{
- "-LCr5pXw_fN32IqNDr4E": {
- "bookCategory": "poetry",
- "locale": "en-us",
- "mediaType": "book",
- "publisher": "Pomelo Books",
- "title": "Sound Waves",
- "type": "poetry"
- },
- "-LDDHjkdY0306fZdvhEQ": {
- "ISBN13": "978-1534402966",
- "bookCategory": "fiction",
- "title": "What Can You Do with a Toolbox?",
- "type": "picturebook"
- }
-}`),
- newline([]byte(`"en-us"
-"fiction"`)),
- nil,
- nil,
- ))
-
- t.Run("Issue364 - CSV root element part 1", runTest(
- []string{"-r", "csv", "-w", "csv", "all().merge()"},
- []byte(`A,B,C
-a,b,c
-d,e,f`),
- newline([]byte(`A,B,C
-a,b,c
-d,e,f`)),
- nil,
- nil,
- ))
-
- t.Run("Issue364 - CSV root element part 2", runTest(
- []string{"-r", "csv", "-w", "csv"},
- []byte(`A,B,C
-a,b,c
-d,e,f`),
- newline([]byte(`A,B,C
-a,b,c
-d,e,f`)),
- nil,
- nil,
- ))
-
- t.Run("CSV custom separator", runTest(
- []string{"-r", "csv", "-w", "csv", "--csv-comma", "."},
- []byte(`A.B.C
-a.b.c
-d.e.f`),
- newline([]byte(`A.B.C
-a.b.c
-d.e.f`)),
- nil,
- nil,
- ))
-
- t.Run("CSV change separator", runTest(
- []string{"-r", "csv", "-w", "csv", "--csv-comma", ".", "--csv-write-comma", ","},
- []byte(`A.B.C
-a.b.c
-d.e.f`),
- newline([]byte(`A,B,C
-a,b,c
-d,e,f`)),
- nil,
- nil,
- ))
-
- t.Run("CSV change from default separator", runTest(
- []string{"-r", "csv", "-w", "csv", "--csv-write-comma", "."},
- []byte(`A,B,C
-a,b,c
-d,e,f`),
- newline([]byte(`A.B.C
-a.b.c
-d.e.f`)),
- nil,
- nil,
- ))
-
- t.Run("Issue351 incorrectly escaped html, default false", runTest(
- []string{"-r", "json"},
- []byte(`{
- "field1": "A",
- "field2": "A > B && B > C"
-}`),
- newline([]byte(`{
- "field1": "A",
- "field2": "A > B && B > C"
-}`)),
- nil,
- nil,
- ))
-
- t.Run("Issue351 incorrectly escaped html, specific false", runTest(
- []string{"-r", "json", "--escape-html=false"},
- []byte(`{
- "field1": "A",
- "field2": "A > B && B > C"
-}`),
- newline([]byte(`{
- "field1": "A",
- "field2": "A > B && B > C"
-}`)),
- nil,
- nil,
- ))
-
- t.Run("Issue351 correctly escaped html", runTest(
- []string{"-r", "json", "--escape-html=true"},
- []byte(`{
- "field1": "A",
- "field2": "A > B && B > C"
-}`),
- newline([]byte(`{
- "field1": "A",
- "field2": "A \u003e B \u0026\u0026 B \u003e C"
-}`)),
- nil,
- nil,
- ))
-
- t.Run("Issue 374 empty input", func(t *testing.T) {
- tests := []struct {
- format string
- exp []byte
- }{
- {
- format: "json",
- exp: []byte("{}\n"),
- },
- {
- format: "toml",
- exp: []byte("\n"),
- },
- {
- format: "yaml",
- exp: []byte("{}\n"),
- },
- {
- format: "xml",
- exp: []byte("\n"),
- },
- {
- format: "csv",
- exp: []byte(""),
- },
- }
-
- for _, test := range tests {
- tc := test
- t.Run(tc.format, runTest(
- []string{"-r", tc.format},
- []byte(``),
- tc.exp,
- nil,
- nil,
- ))
- }
- })
-
- t.Run("Issue 392 panic", runTest(
- []string{"-r", "csv", "--csv-comma", ";", "-w", "json", "equal([], )"},
- []byte(`Hello;There;
-1;2;`),
- []byte("false\n"),
- nil,
- nil,
- ))
-
- t.Run("Issue346", func(t *testing.T) {
- t.Run("Select null or default string", runTest(
- []string{"-r", "json", "orDefault(foo,string(nope))"},
- []byte(`{
- "foo": null
-}`),
- newline([]byte(`"nope"`)),
- nil,
- nil,
- ))
-
- t.Run("Select null or default null", runTest(
- []string{"-r", "json", "orDefault(foo,null())"},
- []byte(`{
- "foo": null
-}`),
- newline([]byte(`null`)),
- nil,
- nil,
- ))
-
- t.Run("Select null value", runTest(
- []string{"-r", "json", "foo"},
- []byte(`{
- "foo": null
-}`),
- newline([]byte(`null`)),
- nil,
- nil,
- ))
- })
-
-}
diff --git a/internal/command/validate.go b/internal/command/validate.go
deleted file mode 100644
index 472e0885..00000000
--- a/internal/command/validate.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package command
-
-import (
- "fmt"
- "github.com/spf13/cobra"
- "io"
- "path/filepath"
- "sync"
-)
-
-func validateCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "validate ",
- Short: "Validate a list of files.",
- RunE: validateRunE,
- Args: cobra.ArbitraryArgs,
- }
-
- validateFlags(cmd)
-
- return cmd
-}
-
-func validateFlags(cmd *cobra.Command) {
- cmd.Flags().Bool("include-error", true, "Show error/validation information")
-}
-
-func validateRunE(cmd *cobra.Command, args []string) error {
- includeErrorFlag, _ := cmd.Flags().GetBool("include-error")
-
- files := make([]validationFile, 0)
- for _, a := range args {
- matches, err := filepath.Glob(a)
- if err != nil {
- return err
- }
-
- for _, m := range matches {
- files = append(files, validationFile{
- File: m,
- Parser: "",
- })
- }
- }
-
- return runValidateCommand(validateOptions{
- Files: files,
- IncludeError: includeErrorFlag,
- }, cmd)
-}
-
-type validateOptions struct {
- Files []validationFile
- Reader io.Reader
- Writer io.Writer
- IncludeError bool
-}
-
-func runValidateCommand(opts validateOptions, cmd *cobra.Command) error {
- fileCount := len(opts.Files)
-
- wg := &sync.WaitGroup{}
- wg.Add(fileCount)
-
- results := make([]validationFileResult, fileCount)
-
- for i, f := range opts.Files {
- index := i
- file := f
- go func() {
- defer wg.Done()
-
- pass, err := validateFile(cmd, file)
- results[index] = validationFileResult{
- File: file,
- Pass: pass,
- Error: err,
- }
- }()
- }
-
- wg.Wait()
-
- failureCount := 0
- for _, result := range results {
- if !result.Pass {
- failureCount++
- }
- }
-
- // Set up our output writer if one wasn't provided.
- if opts.Writer == nil {
- if failureCount > 0 {
- opts.Writer = cmd.OutOrStderr()
- } else {
- opts.Writer = cmd.OutOrStdout()
- }
- }
-
- for _, result := range results {
- outputString := ""
-
- if result.Pass {
- outputString += "pass"
- } else {
- outputString += "fail"
- }
-
- outputString += " " + result.File.File
-
- if opts.IncludeError && result.Error != nil {
- outputString += " " + result.Error.Error()
- }
-
- if _, err := fmt.Fprintln(opts.Writer, outputString); err != nil {
- return fmt.Errorf("could not write output: %w", err)
- }
- }
-
- if failureCount > 0 {
- return fmt.Errorf("%d files failed validation", failureCount)
- }
- return nil
-}
-
-type validationFile struct {
- File string
- Parser string
-}
-
-type validationFileResult struct {
- File validationFile
- Pass bool
- Error error
-}
-
-func validateFile(cmd *cobra.Command, file validationFile) (bool, error) {
- opts := readOptions{
- Parser: file.Parser,
- FilePath: file.File,
- }
- _, err := opts.rootValue(cmd)
-
- return err == nil, err
-}
diff --git a/internal/ptr/to.go b/internal/ptr/to.go
new file mode 100644
index 00000000..ce6ed25c
--- /dev/null
+++ b/internal/ptr/to.go
@@ -0,0 +1,6 @@
+package ptr
+
+// To returns a pointer to the value passed in.
+func To[T any](v T) *T {
+ return &v
+}
diff --git a/model/README.md b/model/README.md
new file mode 100644
index 00000000..5b7917a0
--- /dev/null
+++ b/model/README.md
@@ -0,0 +1,5 @@
+# Model
+
+The model package contains the Value struct and functionality for the application.
+
+`model.Value` is just a wrapper around `reflect.Value` but provides some additional logic for easier use.
diff --git a/model/error.go b/model/error.go
new file mode 100644
index 00000000..8cadf36a
--- /dev/null
+++ b/model/error.go
@@ -0,0 +1,52 @@
+package model
+
+import "fmt"
+
+// MapKeyNotFound is returned when a key is not found in a map.
+type MapKeyNotFound struct {
+ Key string
+}
+
+// Error returns the error message.
+func (e MapKeyNotFound) Error() string {
+ return fmt.Sprintf("map key not found: %q", e.Key)
+}
+
+// SliceIndexOutOfRange is returned when an index is invalid.
+type SliceIndexOutOfRange struct {
+ Index int
+}
+
+// Error returns the error message.
+func (e SliceIndexOutOfRange) Error() string {
+ return fmt.Sprintf("slice index out of range: %d", e.Index)
+}
+
+// ErrIncompatibleTypes is returned when two values are incompatible.
+type ErrIncompatibleTypes struct {
+ A *Value
+ B *Value
+}
+
+// Error returns the error message.
+func (e ErrIncompatibleTypes) Error() string {
+ return fmt.Sprintf("incompatible types: %s and %s", e.A.Type(), e.B.Type())
+}
+
+type ErrUnexpectedType struct {
+ Expected Type
+ Actual Type
+}
+
+func (e ErrUnexpectedType) Error() string {
+ return fmt.Sprintf("unexpected type: expected %s, got %s", e.Expected, e.Actual)
+}
+
+type ErrUnexpectedTypes struct {
+ Expected []Type
+ Actual Type
+}
+
+func (e ErrUnexpectedTypes) Error() string {
+ return fmt.Sprintf("unexpected type: expected %v, got %s", e.Expected, e.Actual)
+}
diff --git a/dencoding/map.go b/model/orderedmap/map.go
similarity index 81%
rename from dencoding/map.go
rename to model/orderedmap/map.go
index 69aa8fd2..9feef834 100644
--- a/dencoding/map.go
+++ b/model/orderedmap/map.go
@@ -1,4 +1,14 @@
-package dencoding
+package orderedmap
+
+import (
+ "reflect"
+)
+
+// KeyValue is a single key value pair from a *Map.
+type KeyValue struct {
+ Key string
+ Value any
+}
// NewMap returns a new *Map that has its values initialised.
func NewMap() *Map {
@@ -29,6 +39,28 @@ type Map struct {
data map[string]any
}
+func (m *Map) Len() int {
+ return len(m.keys)
+}
+
+func (m *Map) Equal(other *Map) bool {
+ if m.Len() != other.Len() {
+ return false
+ }
+
+ for i, k := range m.keys {
+ if k != other.keys[i] {
+ return false
+ }
+
+ if !reflect.DeepEqual(m.data[k], other.data[k]) {
+ return false
+ }
+ }
+
+ return true
+}
+
// Get returns the value found under the given key.
func (m *Map) Get(key string) (any, bool) {
v, ok := m.data[key]
diff --git a/model/value.go b/model/value.go
new file mode 100644
index 00000000..79402bc2
--- /dev/null
+++ b/model/value.go
@@ -0,0 +1,283 @@
+package model
+
+import (
+ "fmt"
+ "reflect"
+ "slices"
+ "strings"
+)
+
+type Type string
+
+func (t Type) String() string {
+ return string(t)
+}
+
+const (
+ TypeString Type = "string"
+ TypeInt Type = "int"
+ TypeFloat Type = "float"
+ TypeBool Type = "bool"
+ TypeMap Type = "map"
+ TypeSlice Type = "array"
+ TypeUnknown Type = "unknown"
+ TypeNull Type = "null"
+)
+
+// KeyValue represents a key value pair.
+type KeyValue struct {
+ Key string
+ Value *Value
+}
+
+// Values represents a list of values.
+type Values []*Value
+
+func (v Values) ToSliceValue() (*Value, error) {
+ slice := NewSliceValue()
+ for _, val := range v {
+ if err := slice.Append(val); err != nil {
+ return nil, err
+ }
+ }
+ return slice, nil
+}
+
+// Value represents a value.
+type Value struct {
+ Value reflect.Value
+ Metadata map[string]any
+
+ setFn func(*Value) error
+}
+
+func (v *Value) String() string {
+ return v.string(0)
+}
+
+func indentStr(indent int) string {
+ return strings.Repeat(" ", indent)
+}
+
+func (v *Value) string(indent int) string {
+ switch v.Type() {
+ case TypeString:
+ val, err := v.StringValue()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("string{%s}", val)
+ case TypeInt:
+ val, err := v.IntValue()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("int{%d}", val)
+ case TypeFloat:
+ val, err := v.FloatValue()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("float(%g)", val)
+ case TypeBool:
+ val, err := v.BoolValue()
+ if err != nil {
+ panic(err)
+ }
+ return fmt.Sprintf("bool{%t}", val)
+ case TypeMap:
+ res := fmt.Sprintf("{\n")
+ if err := v.RangeMap(func(k string, v *Value) error {
+ res += fmt.Sprintf("%s%s: %s,\n", indentStr(indent+1), k, v.string(indent+1))
+ return nil
+ }); err != nil {
+ panic(err)
+ }
+ return res + indentStr(indent) + "}"
+ case TypeSlice:
+ md := ""
+ if v.IsSpread() {
+ md = "spread, "
+ }
+ if v.IsBranch() {
+ md += "branch, "
+ }
+ res := fmt.Sprintf("array[%s]{\n", strings.TrimSuffix(md, ", "))
+ if err := v.RangeSlice(func(k int, v *Value) error {
+ res += fmt.Sprintf("%s%d: %s,\n", indentStr(indent+1), k, v.string(indent+1))
+ return nil
+ }); err != nil {
+ panic(err)
+ }
+ return res + indentStr(indent) + "}"
+ case TypeNull:
+ return indentStr(indent) + "null"
+ default:
+ return fmt.Sprintf("unknown[%s]", v.Interface())
+ }
+}
+
+// NewValue creates a new value.
+func NewValue(v any) *Value {
+ switch val := v.(type) {
+ case *Value:
+ return val
+ case reflect.Value:
+ return &Value{
+ Value: val,
+ Metadata: make(map[string]any),
+ }
+ case nil:
+ return NewNullValue()
+ default:
+ res := newPtr()
+ if v != nil {
+ res.Elem().Set(reflect.ValueOf(v))
+ }
+ return &Value{
+ Value: res,
+ Metadata: make(map[string]any),
+ }
+ }
+}
+
+// Interface returns the value as an interface.
+func (v *Value) Interface() any {
+ if v.IsNull() {
+ return nil
+ }
+ return v.Value.Interface()
+}
+
+// Kind returns the reflect kind of the value.
+func (v *Value) Kind() reflect.Kind {
+ return v.Value.Kind()
+}
+
+// UnpackKinds unpacks the reflect value until it no longer matches the given kinds.
+func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value {
+ res := v.Value
+ for {
+ if !slices.Contains(kinds, res.Kind()) || res.IsNil() {
+ return NewValue(res)
+ }
+ res = res.Elem()
+ }
+}
+
+// UnpackUntilType unpacks the reflect value until it matches the given type.
+func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) {
+ res := v.Value
+ for {
+ if res.Type() == t {
+ return NewValue(res), nil
+ }
+ if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
+ res = res.Elem()
+ continue
+ }
+ return nil, fmt.Errorf("could not unpack to type: %s", t)
+ }
+}
+
+// UnpackUntilAddressable unpacks the reflect value until it is addressable.
+func (v *Value) UnpackUntilAddressable() (*Value, error) {
+ res := v.Value
+ for {
+ if res.CanAddr() {
+ return NewValue(res), nil
+ }
+ if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
+ res = res.Elem()
+ continue
+ }
+ return nil, fmt.Errorf("could not unpack addressable value")
+ }
+}
+
+// UnpackUntilKind unpacks the reflect value until it matches the given kind.
+func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) {
+ res := v.Value
+ for {
+ if res.Kind() == k {
+ return NewValue(res), nil
+ }
+ if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
+ res = res.Elem()
+ continue
+ }
+ return nil, fmt.Errorf("could not unpack to kind: %s", k)
+ }
+}
+
+// UnpackUntilKinds unpacks the reflect value until it matches the given kind.
+func (v *Value) UnpackUntilKinds(kinds ...reflect.Kind) (*Value, error) {
+ res := v.Value
+ for {
+ if slices.Contains(kinds, res.Kind()) {
+ return NewValue(res), nil
+ }
+ if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
+ res = res.Elem()
+ continue
+ }
+ return nil, fmt.Errorf("could not unpack to kinds: %v", kinds)
+ }
+}
+
+// Type returns the type of the value.
+func (v *Value) Type() Type {
+ switch {
+ case v.IsString():
+ return TypeString
+ case v.IsInt():
+ return TypeInt
+ case v.IsFloat():
+ return TypeFloat
+ case v.IsBool():
+ return TypeBool
+ case v.IsMap():
+ return TypeMap
+ case v.IsSlice():
+ return TypeSlice
+ case v.IsNull():
+ return TypeNull
+ default:
+ return TypeUnknown
+ }
+}
+
+// Len returns the length of the value.
+func (v *Value) Len() (int, error) {
+ var l int
+ var err error
+
+ switch {
+ case v.IsSlice():
+ l, err = v.SliceLen()
+ case v.IsMap():
+ l, err = v.MapLen()
+ case v.IsString():
+ l, err = v.StringLen()
+ default:
+ err = ErrUnexpectedTypes{
+ Expected: []Type{TypeSlice, TypeMap, TypeString},
+ Actual: v.Type(),
+ }
+ }
+
+ if err != nil {
+ return l, err
+ }
+
+ return l, nil
+}
+
+func (v *Value) Copy() (*Value, error) {
+ switch v.Type() {
+ case TypeMap:
+ return v.MapCopy()
+ default:
+ return nil, fmt.Errorf("copy not supported for type: %s", v.Type())
+ }
+}
diff --git a/model/value_comparison.go b/model/value_comparison.go
new file mode 100644
index 00000000..e369c118
--- /dev/null
+++ b/model/value_comparison.go
@@ -0,0 +1,302 @@
+package model
+
+// Compare compares two values.
+func (v *Value) Compare(other *Value) (int, error) {
+ eq, err := v.Equal(other)
+ if err != nil {
+ return 0, err
+ }
+ eqVal, err := eq.BoolValue()
+ if err != nil {
+ return 0, err
+ }
+ if eqVal {
+ return 0, nil
+ }
+
+ lt, err := v.LessThan(other)
+ if err != nil {
+ return 0, err
+ }
+ ltVal, err := lt.BoolValue()
+ if err != nil {
+ return 0, err
+ }
+ if ltVal {
+ return -1, nil
+ }
+
+ return 1, nil
+}
+
+// Equal compares two values.
+func (v *Value) Equal(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) == b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a == float64(b)), nil
+ }
+
+ if v.Type() != other.Type() {
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+ }
+
+ isEqual, err := v.EqualTypeValue(other)
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(isEqual), nil
+}
+
+// NotEqual compares two values.
+func (v *Value) NotEqual(other *Value) (*Value, error) {
+ equals, err := v.Equal(other)
+ if err != nil {
+ return nil, err
+ }
+ boolValue, err := equals.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(!boolValue), nil
+}
+
+// LessThan compares two values.
+func (v *Value) LessThan(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a < b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a < b), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) < b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a < float64(b)), nil
+ }
+
+ if v.IsString() && other.IsString() {
+ a, err := v.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a < b), nil
+ }
+
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
+
+// LessThanOrEqual compares two values.
+func (v *Value) LessThanOrEqual(other *Value) (*Value, error) {
+ lessThan, err := v.LessThan(other)
+ if err != nil {
+ return nil, err
+ }
+ boolValue, err := lessThan.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ equals, err := v.Equal(other)
+ if err != nil {
+ return nil, err
+ }
+ boolEquals, err := equals.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(boolValue || boolEquals), nil
+}
+
+// GreaterThan compares two values.
+func (v *Value) GreaterThan(other *Value) (*Value, error) {
+ lessThanOrEqual, err := v.LessThanOrEqual(other)
+ if err != nil {
+ return nil, err
+ }
+ boolValue, err := lessThanOrEqual.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(!boolValue), nil
+}
+
+// GreaterThanOrEqual compares two values.
+func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) {
+ lessThan, err := v.LessThan(other)
+ if err != nil {
+ return nil, err
+ }
+ boolValue, err := lessThan.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(!boolValue), nil
+}
+
+// EqualTypeValue compares two values of the same type.
+func (v *Value) EqualTypeValue(other *Value) (bool, error) {
+ if v.Type() != other.Type() {
+ return false, nil
+ }
+
+ switch v.Type() {
+ case TypeString:
+ a, err := v.StringValue()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.StringValue()
+ if err != nil {
+ return false, err
+ }
+ return a == b, nil
+ case TypeInt:
+ a, err := v.IntValue()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return false, err
+ }
+ return a == b, nil
+ case TypeFloat:
+ a, err := v.FloatValue()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return false, err
+ }
+ return a == b, nil
+ case TypeBool:
+ a, err := v.BoolValue()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.BoolValue()
+ if err != nil {
+ return false, err
+ }
+ return a == b, nil
+ case TypeMap:
+ a, err := v.MapKeys()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.MapKeys()
+ if err != nil {
+ return false, err
+ }
+ if len(a) != len(b) {
+ return false, nil
+ }
+ for _, key := range a {
+ valA, err := v.GetMapKey(key)
+ if err != nil {
+ return false, err
+ }
+ valB, err := other.GetMapKey(key)
+ if err != nil {
+ return false, err
+ }
+ equal, err := valA.EqualTypeValue(valB)
+ if err != nil {
+ return false, err
+ }
+ if !equal {
+ return false, nil
+ }
+ }
+ return true, nil
+ case TypeSlice:
+ a, err := v.SliceLen()
+ if err != nil {
+ return false, err
+ }
+ b, err := other.SliceLen()
+ if err != nil {
+ return false, err
+ }
+ if a != b {
+ return false, nil
+ }
+ for i := 0; i < a; i++ {
+ valA, err := v.GetSliceIndex(i)
+ if err != nil {
+ return false, err
+ }
+ valB, err := other.GetSliceIndex(i)
+ if err != nil {
+ return false, err
+ }
+ equal, err := valA.EqualTypeValue(valB)
+ if err != nil {
+ return false, err
+ }
+ if !equal {
+ return false, nil
+ }
+ }
+ return true, nil
+ case TypeNull:
+ return other.Type() == TypeNull, nil
+ default:
+ return false, nil
+ }
+}
diff --git a/model/value_comparison_test.go b/model/value_comparison_test.go
new file mode 100644
index 00000000..646c2c88
--- /dev/null
+++ b/model/value_comparison_test.go
@@ -0,0 +1,803 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+type compareTestCase struct {
+ a *model.Value
+ b *model.Value
+ exp bool
+}
+
+func TestValue_Equal(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.Equal(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("string", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("hello"),
+ b: model.NewStringValue("hello"),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewStringValue("hello"),
+ b: model.NewStringValue("world"),
+ exp: false,
+ }))
+ })
+ t.Run("int", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: false,
+ }))
+ t.Run("equal float", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: true,
+ }))
+ t.Run("not equal float", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: false,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: false,
+ }))
+ t.Run("equal int", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("not equal int", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("bool", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewBoolValue(true),
+ b: model.NewBoolValue(true),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewBoolValue(true),
+ b: model.NewBoolValue(false),
+ exp: false,
+ }))
+ })
+ t.Run("map", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ b: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ b: model.NewValue(map[string]interface{}{
+ "hello": "world2",
+ }),
+ exp: false,
+ }))
+ })
+ t.Run("array", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ b: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ exp: true,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ b: model.NewValue([]interface{}{
+ "hello", "world2",
+ }),
+ exp: false,
+ }))
+ })
+ t.Run("null", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue(nil),
+ b: model.NewValue(nil),
+ exp: true,
+ }))
+ })
+}
+
+func TestValue_NotEqual(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.NotEqual(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("string", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("hello"),
+ b: model.NewStringValue("hello"),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewStringValue("hello"),
+ b: model.NewStringValue("world"),
+ exp: true,
+ }))
+ })
+ t.Run("int", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: true,
+ }))
+ t.Run("equal float", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: false,
+ }))
+ t.Run("not equal float", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: true,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: true,
+ }))
+ t.Run("equal int", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("not equal int", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("bool", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewBoolValue(true),
+ b: model.NewBoolValue(true),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewBoolValue(true),
+ b: model.NewBoolValue(false),
+ exp: true,
+ }))
+ })
+ t.Run("map", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ b: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewValue(map[string]interface{}{
+ "hello": "world",
+ }),
+ b: model.NewValue(map[string]interface{}{
+ "hello": "world2",
+ }),
+ exp: true,
+ }))
+ })
+ t.Run("array", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ b: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ exp: false,
+ }))
+ t.Run("not equal", run(compareTestCase{
+ a: model.NewValue([]interface{}{
+ "hello", "world",
+ }),
+ b: model.NewValue([]interface{}{
+ "hello", "world2",
+ }),
+ exp: true,
+ }))
+ })
+ t.Run("null", func(t *testing.T) {
+ t.Run("equal", run(compareTestCase{
+ a: model.NewValue(nil),
+ b: model.NewValue(nil),
+ exp: false,
+ }))
+ })
+}
+
+func TestValue_LessThan(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.LessThan(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(1.2),
+ b: model.NewFloatValue(1.1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: false,
+ }))
+ })
+ t.Run("int float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewFloatValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("float int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(2),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("b"),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewStringValue("b"),
+ b: model.NewStringValue("a"),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("a"),
+ exp: false,
+ }))
+ })
+}
+
+func TestValue_LessThanOrEqual(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.LessThanOrEqual(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(1.2),
+ b: model.NewFloatValue(1.1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: true,
+ }))
+ })
+ t.Run("int float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewFloatValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("float int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(2),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(2),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("b"),
+ exp: true,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewStringValue("b"),
+ b: model.NewStringValue("a"),
+ exp: false,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("a"),
+ exp: true,
+ }))
+ })
+}
+
+func TestValue_GreaterThan(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.GreaterThan(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(1.2),
+ b: model.NewFloatValue(1.1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: false,
+ }))
+ })
+ t.Run("int float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewFloatValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("float int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(2),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: false,
+ }))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("b"),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewStringValue("b"),
+ b: model.NewStringValue("a"),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("a"),
+ exp: false,
+ }))
+ })
+}
+
+func TestValue_GreaterThanOrEqual(t *testing.T) {
+ run := func(tc compareTestCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := tc.a.GreaterThanOrEqual(tc.b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ gotBool, err := got.BoolValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if gotBool != tc.exp {
+ t.Errorf("expected %v, got %v", tc.exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(1.2),
+ b: model.NewFloatValue(1.1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewFloatValue(1.1),
+ exp: true,
+ }))
+ })
+ t.Run("int float", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewIntValue(2),
+ b: model.NewFloatValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewIntValue(1),
+ b: model.NewFloatValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("float int", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewFloatValue(1.1),
+ b: model.NewIntValue(2),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewFloatValue(2),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewFloatValue(1),
+ b: model.NewIntValue(1),
+ exp: true,
+ }))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("less", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("b"),
+ exp: false,
+ }))
+ t.Run("greater", run(compareTestCase{
+ a: model.NewStringValue("b"),
+ b: model.NewStringValue("a"),
+ exp: true,
+ }))
+ t.Run("equal", run(compareTestCase{
+ a: model.NewStringValue("a"),
+ b: model.NewStringValue("a"),
+ exp: true,
+ }))
+ })
+}
+
+func TestValue_Compare(t *testing.T) {
+ run := func(a *model.Value, b *model.Value, exp int) func(t *testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Compare(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != exp {
+ t.Errorf("expected %d, got %d", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("less", run(
+ model.NewIntValue(1),
+ model.NewIntValue(2),
+ -1,
+ ))
+ t.Run("greater", run(
+ model.NewIntValue(2),
+ model.NewIntValue(1),
+ 1,
+ ))
+ t.Run("equal", run(
+ model.NewIntValue(1),
+ model.NewIntValue(1),
+ 0,
+ ))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("less", run(
+ model.NewFloatValue(1.1),
+ model.NewFloatValue(1.2),
+ -1,
+ ))
+ t.Run("greater", run(
+ model.NewFloatValue(1.2),
+ model.NewFloatValue(1.1),
+ 1,
+ ))
+ t.Run("equal", run(
+ model.NewFloatValue(1.1),
+ model.NewFloatValue(1.1),
+ 0,
+ ))
+ })
+ t.Run("int float", func(t *testing.T) {
+ t.Run("less", run(
+ model.NewIntValue(1),
+ model.NewFloatValue(2),
+ -1,
+ ))
+ t.Run("greater", run(
+ model.NewIntValue(2),
+ model.NewFloatValue(1),
+ 1,
+ ))
+ t.Run("equal", run(
+ model.NewIntValue(1),
+ model.NewFloatValue(1),
+ 0,
+ ))
+ })
+ t.Run("float int", func(t *testing.T) {
+ t.Run("less", run(
+ model.NewFloatValue(1.1),
+ model.NewIntValue(2),
+ -1,
+ ))
+ t.Run("greater", run(
+ model.NewFloatValue(1.1),
+ model.NewIntValue(1),
+ 1,
+ ))
+ t.Run("equal", run(
+ model.NewFloatValue(1),
+ model.NewIntValue(1),
+ 0,
+ ))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("less", run(
+ model.NewStringValue("a"),
+ model.NewStringValue("b"),
+ -1,
+ ))
+ t.Run("greater", run(
+ model.NewStringValue("b"),
+ model.NewStringValue("a"),
+ 1,
+ ))
+ t.Run("equal", run(
+ model.NewStringValue("a"),
+ model.NewStringValue("a"),
+ 0,
+ ))
+ })
+}
diff --git a/model/value_literal.go b/model/value_literal.go
new file mode 100644
index 00000000..fd3a1bdb
--- /dev/null
+++ b/model/value_literal.go
@@ -0,0 +1,185 @@
+package model
+
+import (
+ "reflect"
+ "slices"
+)
+
+func newPtr() reflect.Value {
+ return reflect.New(reflect.TypeFor[any]())
+}
+
+// NewNullValue creates a new Value with a nil value.
+func NewNullValue() *Value {
+ return NewValue(newPtr())
+}
+
+// IsNull returns true if the value is null.
+func (v *Value) IsNull() bool {
+ return v.UnpackKinds(reflect.Ptr, reflect.Interface).isNull()
+}
+
+func (v *Value) isNull() bool {
+ // This logic can be cleaned up.
+ unpacked, err := v.UnpackUntilKinds(reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice)
+ if err != nil {
+ return false
+ }
+ return unpacked.Value.IsNil()
+}
+
+// NewStringValue creates a new Value with a string value.
+func NewStringValue(x string) *Value {
+ res := newPtr()
+ res.Elem().Set(reflect.ValueOf(x))
+ return NewValue(res)
+}
+
+// IsString returns true if the value is a string.
+func (v *Value) IsString() bool {
+ return v.UnpackKinds(reflect.Ptr, reflect.Interface).isString()
+}
+
+func (v *Value) isString() bool {
+ return v.Value.Kind() == reflect.String
+}
+
+// StringValue returns the string value of the Value.
+func (v *Value) StringValue() (string, error) {
+ unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface)
+ if !unpacked.isString() {
+ return "", ErrUnexpectedType{
+ Expected: TypeString,
+ Actual: v.Type(),
+ }
+ }
+ return unpacked.Value.String(), nil
+}
+
+// StringLen returns the length of the string.
+func (v *Value) StringLen() (int, error) {
+ val, err := v.StringValue()
+ if err != nil {
+ return 0, err
+ }
+ return len(val), nil
+}
+
+// StringIndexRange returns a new string containing the values between the start and end indexes.
+// Comparable to go's string[start:end].
+func (v *Value) StringIndexRange(start, end int) (*Value, error) {
+ strVal, err := v.StringValue()
+ if err != nil {
+ return nil, err
+ }
+
+ inBytes := []rune(strVal)
+ l := len(inBytes)
+
+ if start < 0 {
+ start = l + start
+ }
+ if end < 0 {
+ end = l + end
+ }
+
+ resBytes := make([]rune, 0)
+
+ if start > end {
+ for i := start; i >= end; i-- {
+ resBytes = append(resBytes, inBytes[i])
+ }
+ } else {
+ for i := start; i <= end; i++ {
+ resBytes = append(resBytes, inBytes[i])
+ }
+ }
+
+ res := string(resBytes)
+
+ return NewStringValue(res), nil
+}
+
+// NewIntValue creates a new Value with an int value.
+func NewIntValue(x int64) *Value {
+ res := newPtr()
+ res.Elem().Set(reflect.ValueOf(x))
+ return NewValue(res)
+}
+
+// IsInt returns true if the value is an int.
+func (v *Value) IsInt() bool {
+ return v.UnpackKinds(reflect.Ptr, reflect.Interface).isInt()
+}
+
+func (v *Value) isInt() bool {
+ return slices.Contains([]reflect.Kind{reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64}, v.Value.Kind())
+}
+
+// IntValue returns the int value of the Value.
+func (v *Value) IntValue() (int64, error) {
+ unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface)
+ if !unpacked.isInt() {
+ return 0, ErrUnexpectedType{
+ Expected: TypeInt,
+ Actual: v.Type(),
+ }
+ }
+ return unpacked.Value.Int(), nil
+}
+
+// NewFloatValue creates a new Value with a float value.
+func NewFloatValue(x float64) *Value {
+ res := newPtr()
+ res.Elem().Set(reflect.ValueOf(x))
+ return NewValue(res)
+}
+
+// IsFloat returns true if the value is a float.
+func (v *Value) IsFloat() bool {
+ return v.UnpackKinds(reflect.Ptr, reflect.Interface).isFloat()
+}
+
+func (v *Value) isFloat() bool {
+ return slices.Contains([]reflect.Kind{reflect.Float32, reflect.Float64}, v.Value.Kind())
+}
+
+// FloatValue returns the float value of the Value.
+func (v *Value) FloatValue() (float64, error) {
+ unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface)
+ if !unpacked.IsFloat() {
+ return 0, ErrUnexpectedType{
+ Expected: TypeFloat,
+ Actual: v.Type(),
+ }
+ }
+ return unpacked.Value.Float(), nil
+}
+
+// NewBoolValue creates a new Value with a bool value.
+func NewBoolValue(x bool) *Value {
+ res := newPtr()
+ res.Elem().Set(reflect.ValueOf(x))
+ return NewValue(res)
+}
+
+// IsBool returns true if the value is a bool.
+func (v *Value) IsBool() bool {
+ return v.UnpackKinds(reflect.Ptr, reflect.Interface).isBool()
+}
+
+func (v *Value) isBool() bool {
+ return v.Value.Kind() == reflect.Bool
+}
+
+// BoolValue returns the bool value of the Value.
+func (v *Value) BoolValue() (bool, error) {
+ unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface)
+ if !unpacked.IsBool() {
+ return false, ErrUnexpectedType{
+ Expected: TypeBool,
+ Actual: v.Type(),
+ }
+ }
+ return unpacked.Value.Bool(), nil
+}
diff --git a/model/value_literal_test.go b/model/value_literal_test.go
new file mode 100644
index 00000000..c34e8fff
--- /dev/null
+++ b/model/value_literal_test.go
@@ -0,0 +1,14 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestValue_IsNull(t *testing.T) {
+ v := model.NewNullValue()
+ if !v.IsNull() {
+ t.Fatalf("expected value to be null")
+ }
+}
diff --git a/model/value_map.go b/model/value_map.go
new file mode 100644
index 00000000..77b92ade
--- /dev/null
+++ b/model/value_map.go
@@ -0,0 +1,232 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+// NewMapValue creates a new map value.
+func NewMapValue() *Value {
+ return NewValue(orderedmap.NewMap())
+}
+
+// IsMap returns true if the value is a map.
+func (v *Value) IsMap() bool {
+ return v.isStandardMap() || v.isDencodingMap()
+}
+
+func (v *Value) isStandardMap() bool {
+ return v.UnpackKinds(reflect.Interface, reflect.Ptr).Kind() == reflect.Map
+}
+
+func (v *Value) isDencodingMap() bool {
+ return v.UnpackKinds(reflect.Interface, reflect.Ptr).Value.Type() == reflect.TypeFor[orderedmap.Map]()
+}
+
+func (v *Value) dencodingMapValue() (*orderedmap.Map, error) {
+ if v.isDencodingMap() {
+ m, err := v.UnpackUntilType(reflect.TypeFor[*orderedmap.Map]())
+ if err != nil {
+ return nil, fmt.Errorf("error getting map: %w", err)
+ }
+ return m.Value.Interface().(*orderedmap.Map), nil
+ }
+ return nil, fmt.Errorf("value is not a dencoding map")
+}
+
+// SetMapKey sets the value at the specified key in the map.
+func (v *Value) SetMapKey(key string, value *Value) error {
+ switch {
+ case v.isDencodingMap():
+ m, err := v.dencodingMapValue()
+ if err != nil {
+ return fmt.Errorf("error getting map: %w", err)
+ }
+ m.Set(key, value.Value.Interface())
+ return nil
+ case v.isStandardMap():
+ unpacked, err := v.UnpackUntilKind(reflect.Map)
+ if err != nil {
+ return fmt.Errorf("error unpacking value: %w", err)
+ }
+ unpacked.Value.SetMapIndex(reflect.ValueOf(key), value.Value)
+ return nil
+ default:
+ return fmt.Errorf("value is not a map")
+ }
+}
+
+func (v *Value) MapCopy() (*Value, error) {
+ res := NewMapValue()
+ kvs, err := v.MapKeyValues()
+ if err != nil {
+ return nil, fmt.Errorf("error getting map key values: %w", err)
+ }
+ for _, kv := range kvs {
+ if err := res.SetMapKey(kv.Key, kv.Value); err != nil {
+ return nil, fmt.Errorf("error setting map key: %w", err)
+ }
+ }
+ return res, nil
+}
+
+func (v *Value) MapKeyExists(key string) (bool, error) {
+ _, err := v.GetMapKey(key)
+ if err != nil && !errors.As(err, &MapKeyNotFound{}) {
+ return false, err
+ }
+ return err == nil, nil
+}
+
+// GetMapKey returns the value at the specified key in the map.
+func (v *Value) GetMapKey(key string) (*Value, error) {
+ switch {
+ case v.isDencodingMap():
+ m, err := v.dencodingMapValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting map: %w", err)
+ }
+ val, ok := m.Get(key)
+ if !ok {
+ return nil, MapKeyNotFound{Key: key}
+ }
+ res := NewValue(val)
+ res.setFn = func(newValue *Value) error {
+ m.Set(key, newValue.Value.Interface())
+ return nil
+ }
+ return res, nil
+ case v.isStandardMap():
+ unpacked, err := v.UnpackUntilKind(reflect.Map)
+ if err != nil {
+ return nil, fmt.Errorf("error unpacking value: %w", err)
+ }
+ i := unpacked.Value.MapIndex(reflect.ValueOf(key))
+ if !i.IsValid() {
+ return nil, MapKeyNotFound{Key: key}
+ }
+ res := NewValue(i)
+ res.setFn = func(newValue *Value) error {
+ mapRv, err := v.UnpackUntilKind(reflect.Map)
+ if err != nil {
+ return fmt.Errorf("error unpacking value: %w", err)
+ }
+ mapRv.Value.SetMapIndex(reflect.ValueOf(key), newValue.Value)
+ return nil
+ }
+ return res, nil
+ default:
+ return nil, ErrUnexpectedType{
+ Expected: TypeMap,
+ Actual: v.Type(),
+ }
+ }
+}
+
+// DeleteMapKey deletes the key from the map.
+func (v *Value) DeleteMapKey(key string) error {
+ switch {
+ case v.isDencodingMap():
+ m, err := v.dencodingMapValue()
+ if err != nil {
+ return fmt.Errorf("error getting map: %w", err)
+ }
+ m.Delete(key)
+ return nil
+ case v.isStandardMap():
+ unpacked, err := v.UnpackUntilKind(reflect.Map)
+ if err != nil {
+ return fmt.Errorf("error unpacking value: %w", err)
+ }
+ unpacked.Value.SetMapIndex(reflect.ValueOf(key), reflect.Value{})
+ return nil
+ default:
+ return ErrUnexpectedType{
+ Expected: TypeMap,
+ Actual: v.Type(),
+ }
+ }
+}
+
+// MapKeys returns a list of keys in the map.
+func (v *Value) MapKeys() ([]string, error) {
+ switch {
+ case v.isDencodingMap():
+ m, err := v.dencodingMapValue()
+ if err != nil {
+ return nil, fmt.Errorf("error getting map: %w", err)
+ }
+ return m.Keys(), nil
+ case v.isStandardMap():
+ unpacked, err := v.UnpackUntilKind(reflect.Map)
+ if err != nil {
+ return nil, fmt.Errorf("error unpacking value: %w", err)
+ }
+ keys := unpacked.Value.MapKeys()
+ strKeys := make([]string, len(keys))
+ for i, k := range keys {
+ strKeys[i] = k.String()
+ }
+ return strKeys, nil
+ default:
+ return nil, ErrUnexpectedType{
+ Expected: TypeMap,
+ Actual: v.Type(),
+ }
+ }
+}
+
+// RangeMap iterates over each key in the map and calls the provided function with the key and value.
+func (v *Value) RangeMap(f func(string, *Value) error) error {
+ keys, err := v.MapKeys()
+ if err != nil {
+ return fmt.Errorf("error getting map keys: %w", err)
+ }
+
+ for _, k := range keys {
+ va, err := v.GetMapKey(k)
+ if err != nil {
+ return fmt.Errorf("error getting map key: %w", err)
+ }
+ if err := f(k, va); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// MapKeyValues returns a list of key value pairs in the map.
+func (v *Value) MapKeyValues() ([]KeyValue, error) {
+ keys, err := v.MapKeys()
+ if err != nil {
+ return nil, fmt.Errorf("error getting map keys: %w", err)
+ }
+
+ kvs := make([]KeyValue, len(keys))
+
+ for i, k := range keys {
+ va, err := v.GetMapKey(k)
+ if err != nil {
+ return nil, fmt.Errorf("error getting map key: %w", err)
+ }
+ kvs[i] = KeyValue{
+ Key: k,
+ Value: va,
+ }
+ }
+
+ return kvs, nil
+}
+
+// MapLen returns the length of the slice.
+func (v *Value) MapLen() (int, error) {
+ keys, err := v.MapKeys()
+ if err != nil {
+ return 0, err
+ }
+ return len(keys), nil
+}
diff --git a/model/value_map_test.go b/model/value_map_test.go
new file mode 100644
index 00000000..afb3bc89
--- /dev/null
+++ b/model/value_map_test.go
@@ -0,0 +1,153 @@
+package model_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/model/orderedmap"
+)
+
+func TestMap(t *testing.T) {
+ standardMap := func() *model.Value {
+ return model.NewValue(map[string]interface{}{
+ "foo": "foo1",
+ "bar": "bar1",
+ })
+ }
+
+ dencodingMap := func() *model.Value {
+ return model.NewValue(orderedmap.NewMap().
+ Set("foo", "foo1").
+ Set("bar", "bar1"))
+ }
+
+ modelMap := func() *model.Value {
+ res := model.NewMapValue()
+ if err := res.SetMapKey("foo", model.NewValue("foo1")); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.SetMapKey("bar", model.NewValue("bar1")); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ }
+
+ runTests := func(v func() *model.Value) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Run("IsMap", func(t *testing.T) {
+ v := v()
+ if !v.IsMap() {
+ t.Errorf("expected value to be a map")
+ }
+ })
+ t.Run("GetMapKey", func(t *testing.T) {
+ v := v()
+ foo, err := v.GetMapKey("foo")
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := foo.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "foo1" {
+ t.Errorf("expected foo1, got %s", got)
+ }
+ })
+ t.Run("SetMapKey", func(t *testing.T) {
+ v := v()
+ if err := v.SetMapKey("baz", model.NewValue("baz1")); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ baz, err := v.GetMapKey("baz")
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := baz.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "baz1" {
+ t.Errorf("expected baz1, got %s", got)
+ }
+ })
+ t.Run("MapKeys", func(t *testing.T) {
+ v := v()
+ keys, err := v.MapKeys()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if len(keys) != 2 {
+ t.Errorf("expected 2 keys, got %d", len(keys))
+ }
+ exp := []string{"foo", "bar"}
+ for _, k := range exp {
+ var found bool
+ for _, e := range keys {
+ if e == k {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("expected key %s not found", k)
+ }
+ }
+ })
+ t.Run("RangeMap", func(t *testing.T) {
+ v := v()
+ var keys []string
+ err := v.RangeMap(func(k string, v *model.Value) error {
+ keys = append(keys, k)
+ return nil
+ })
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if len(keys) != 2 {
+ t.Errorf("expected 2 keys, got %d", len(keys))
+ }
+ exp := []string{"foo", "bar"}
+ for _, k := range exp {
+ var found bool
+ for _, e := range keys {
+ if e == k {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("expected key %s not found", k)
+ }
+ }
+ })
+ t.Run("DeleteMapKey", func(t *testing.T) {
+ v := v()
+ if _, err := v.GetMapKey("foo"); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if err := v.DeleteMapKey("foo"); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ _, err := v.GetMapKey("foo")
+ if !errors.As(err, &model.MapKeyNotFound{}) {
+ t.Errorf("expected key not found error, got %s", err)
+ }
+ })
+ }
+ }
+
+ t.Run("standard map", runTests(standardMap))
+ t.Run("dencoding map", runTests(dencodingMap))
+ t.Run("model map", runTests(modelMap))
+}
diff --git a/model/value_math.go b/model/value_math.go
new file mode 100644
index 00000000..bc932f21
--- /dev/null
+++ b/model/value_math.go
@@ -0,0 +1,261 @@
+package model
+
+import (
+ "math"
+)
+
+// Add adds two values together.
+func (v *Value) Add(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a + b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a + b), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) + b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a + float64(b)), nil
+ }
+ if v.IsString() && other.IsString() {
+ a, err := v.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a + b), nil
+ }
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
+
+// Subtract returns the difference between two values.
+func (v *Value) Subtract(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a - b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a - b), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) - b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a - float64(b)), nil
+ }
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
+
+// Multiply returns the product of the two values.
+func (v *Value) Multiply(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a * b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a * b), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) * b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a * float64(b)), nil
+ }
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
+
+// Divide returns the result of dividing the value by another value.
+func (v *Value) Divide(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a / b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a / b), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(float64(a) / b), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a / float64(b)), nil
+ }
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
+
+// Modulo returns the remainder of the division of two values.
+func (v *Value) Modulo(other *Value) (*Value, error) {
+ if v.IsInt() && other.IsInt() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(a % b), nil
+ }
+ if v.IsFloat() && other.IsFloat() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(math.Mod(a, b)), nil
+ }
+ if v.IsInt() && other.IsFloat() {
+ a, err := v.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(math.Mod(float64(a), b)), nil
+ }
+ if v.IsFloat() && other.IsInt() {
+ a, err := v.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ b, err := other.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(math.Mod(a, float64(b))), nil
+ }
+ return nil, ErrIncompatibleTypes{A: v, B: other}
+}
diff --git a/model/value_math_test.go b/model/value_math_test.go
new file mode 100644
index 00000000..f6b80e54
--- /dev/null
+++ b/model/value_math_test.go
@@ -0,0 +1,150 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestValue_Add(t *testing.T) {
+ run := func(a, b *model.Value, exp *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Add(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ eq, err := got.EqualTypeValue(exp)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("int", run(model.NewIntValue(1), model.NewIntValue(2), model.NewIntValue(3)))
+ t.Run("float", run(model.NewIntValue(1), model.NewFloatValue(2), model.NewFloatValue(3)))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("int", run(model.NewFloatValue(1), model.NewIntValue(2), model.NewFloatValue(3)))
+ t.Run("float", run(model.NewFloatValue(1), model.NewFloatValue(2), model.NewFloatValue(3)))
+ })
+ t.Run("string", func(t *testing.T) {
+ t.Run("string", run(model.NewStringValue("hello"), model.NewStringValue(" world"), model.NewStringValue("hello world")))
+ })
+}
+
+func TestValue_Subtract(t *testing.T) {
+ run := func(a, b *model.Value, exp *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Subtract(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ eq, err := got.EqualTypeValue(exp)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("int", run(model.NewIntValue(3), model.NewIntValue(2), model.NewIntValue(1)))
+ t.Run("float", run(model.NewIntValue(3), model.NewFloatValue(2), model.NewFloatValue(1)))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("int", run(model.NewFloatValue(3), model.NewIntValue(2), model.NewFloatValue(1)))
+ t.Run("float", run(model.NewFloatValue(3), model.NewFloatValue(2), model.NewFloatValue(1)))
+ })
+}
+
+func TestValue_Multiply(t *testing.T) {
+ run := func(a, b *model.Value, exp *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Multiply(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ eq, err := got.EqualTypeValue(exp)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("int", run(model.NewIntValue(3), model.NewIntValue(2), model.NewIntValue(6)))
+ t.Run("float", run(model.NewIntValue(3), model.NewFloatValue(2), model.NewFloatValue(6)))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("int", run(model.NewFloatValue(3), model.NewIntValue(2), model.NewFloatValue(6)))
+ t.Run("float", run(model.NewFloatValue(3), model.NewFloatValue(2), model.NewFloatValue(6)))
+ })
+}
+
+func TestValue_Divide(t *testing.T) {
+ run := func(a, b *model.Value, exp *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Divide(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ eq, err := got.EqualTypeValue(exp)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("int", run(model.NewIntValue(6), model.NewIntValue(2), model.NewIntValue(3)))
+ t.Run("float", run(model.NewIntValue(6), model.NewFloatValue(2), model.NewFloatValue(3)))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("int", run(model.NewFloatValue(6), model.NewIntValue(2), model.NewFloatValue(3)))
+ t.Run("float", run(model.NewFloatValue(6), model.NewFloatValue(2), model.NewFloatValue(3)))
+ })
+}
+
+func TestValue_Modulo(t *testing.T) {
+ run := func(a, b *model.Value, exp *model.Value) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := a.Modulo(b)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ eq, err := got.EqualTypeValue(exp)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ }
+ }
+ t.Run("int", func(t *testing.T) {
+ t.Run("int", run(model.NewIntValue(10), model.NewIntValue(3), model.NewIntValue(1)))
+ t.Run("float", run(model.NewIntValue(10), model.NewFloatValue(3), model.NewFloatValue(1)))
+ })
+ t.Run("float", func(t *testing.T) {
+ t.Run("int", run(model.NewFloatValue(10), model.NewIntValue(3), model.NewFloatValue(1)))
+ t.Run("float", run(model.NewFloatValue(10), model.NewFloatValue(3), model.NewFloatValue(1)))
+ })
+}
diff --git a/model/value_metadata.go b/model/value_metadata.go
new file mode 100644
index 00000000..76f3c3e0
--- /dev/null
+++ b/model/value_metadata.go
@@ -0,0 +1,74 @@
+package model
+
+// MetadataValue returns a metadata value.
+func (v *Value) MetadataValue(key string) (any, bool) {
+ if v.Metadata == nil {
+ return nil, false
+ }
+ val, ok := v.Metadata[key]
+ return val, ok
+}
+
+// SetMetadataValue sets a metadata value.
+func (v *Value) SetMetadataValue(key string, val any) {
+ if v.Metadata == nil {
+ v.Metadata = map[string]any{}
+ }
+ v.Metadata[key] = val
+}
+
+// IsSpread returns true if the value is a spread value.
+// Spread values are used to represent the spread operator.
+func (v *Value) IsSpread() bool {
+ if v == nil {
+ return false
+ }
+ val, ok := v.MetadataValue("spread")
+ if !ok {
+ return false
+ }
+ spread, ok := val.(bool)
+ return ok && spread
+}
+
+// MarkAsSpread marks the value as a spread value.
+// Spread values are used to represent the spread operator.
+func (v *Value) MarkAsSpread() {
+ v.SetMetadataValue("spread", true)
+}
+
+// IsBranch returns true if the value is a branched value.
+func (v *Value) IsBranch() bool {
+ if v == nil {
+ return false
+ }
+ val, ok := v.MetadataValue("branch")
+ if !ok {
+ return false
+ }
+ branch, ok := val.(bool)
+ return ok && branch
+}
+
+// MarkAsBranch marks the value as a branch value.
+func (v *Value) MarkAsBranch() {
+ v.SetMetadataValue("branch", true)
+}
+
+// IsIgnore returns true if value should be ignored.
+func (v *Value) IsIgnore() bool {
+ if v == nil {
+ return false
+ }
+ val, ok := v.MetadataValue("ignore")
+ if !ok {
+ return false
+ }
+ ignore, ok := val.(bool)
+ return ok && ignore
+}
+
+// MarkAsIgnore marks the value to be ignored.
+func (v *Value) MarkAsIgnore() {
+ v.SetMetadataValue("ignore", true)
+}
diff --git a/model/value_metadata_test.go b/model/value_metadata_test.go
new file mode 100644
index 00000000..64e0e610
--- /dev/null
+++ b/model/value_metadata_test.go
@@ -0,0 +1,29 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestValue_IsBranch(t *testing.T) {
+ val := model.NewNullValue()
+ if exp, got := false, val.IsBranch(); exp != got {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ val.MarkAsBranch()
+ if exp, got := true, val.IsBranch(); exp != got {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+}
+
+func TestValue_IsSpread(t *testing.T) {
+ val := model.NewNullValue()
+ if exp, got := false, val.IsSpread(); exp != got {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+ val.MarkAsSpread()
+ if exp, got := true, val.IsSpread(); exp != got {
+ t.Errorf("expected %v, got %v", exp, got)
+ }
+}
diff --git a/model/value_set.go b/model/value_set.go
new file mode 100644
index 00000000..57c9b127
--- /dev/null
+++ b/model/value_set.go
@@ -0,0 +1,62 @@
+package model
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Set sets the value of the value.
+func (v *Value) Set(newValue *Value) error {
+ if v.setFn != nil {
+ return v.setFn(newValue)
+ }
+
+ a, err := v.UnpackUntilAddressable()
+ if err != nil {
+ return err
+ }
+
+ b := newValue.UnpackKinds(reflect.Ptr)
+ if a.Kind() == b.Kind() {
+ a.Value.Set(b.Value)
+ return nil
+ }
+
+ b = newValue.UnpackKinds(reflect.Ptr, reflect.Interface)
+ if a.Kind() == b.Kind() {
+ a.Value.Set(b.Value)
+ return nil
+ }
+
+ // These are commented out because I don't think they are needed.
+
+ //if a.Kind() == newValue.Kind() {
+ // a.Value.Set(newValue.Value)
+ // return nil
+ //}
+
+ //b = newValue.UnpackKinds(reflect.Interface)
+ //if a.Kind() == b.Kind() {
+ // a.Value.Set(b.Value)
+ // return nil
+ //}
+
+ //b = newValue.UnpackKinds(reflect.Ptr, reflect.Interface)
+ //if a.Kind() == b.Kind() {
+ // a.Value.Set(b.Value)
+ // return nil
+ //}
+
+ //b, err = newValue.UnpackUntilAddressable()
+ //if err != nil {
+ // return err
+ //}
+ //if a.Kind() == b.Kind() {
+ // a.Value.Set(b.Value)
+ // return nil
+ //}
+
+ // This is a hard limitation at the moment.
+ // If the types are not the same, we cannot set the value.
+ return fmt.Errorf("could not set %s value on %s value", newValue.Type(), v.Type())
+}
diff --git a/model/value_set_test.go b/model/value_set_test.go
new file mode 100644
index 00000000..11f0f03d
--- /dev/null
+++ b/model/value_set_test.go
@@ -0,0 +1,239 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+type setTestCase struct {
+ valueFn func() *model.Value
+ value *model.Value
+ newValueFn func() *model.Value
+ newValue *model.Value
+}
+
+func (tc setTestCase) run(t *testing.T) {
+ val := tc.value
+ if tc.valueFn != nil {
+ val = tc.valueFn()
+ }
+ newVal := tc.newValue
+ if tc.newValueFn != nil {
+ newVal = tc.newValueFn()
+ }
+ if err := val.Set(newVal); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+
+ eq, err := val.EqualTypeValue(newVal)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if !eq {
+ t.Errorf("expected values to be equal")
+ }
+}
+
+func TestValue_Set(t *testing.T) {
+ testCases := []struct {
+ name string
+ stringValue func() *model.Value
+ intValue func() *model.Value
+ floatValue func() *model.Value
+ boolValue func() *model.Value
+ mapValue func() *model.Value
+ sliceValue func() *model.Value
+ nullValue func() *model.Value
+ }{
+ {
+ name: "model constructor",
+ stringValue: func() *model.Value {
+ return model.NewStringValue("hello")
+ },
+ intValue: func() *model.Value {
+ return model.NewIntValue(1)
+ },
+ floatValue: func() *model.Value {
+ return model.NewFloatValue(1)
+ },
+ boolValue: func() *model.Value {
+ return model.NewBoolValue(true)
+ },
+ mapValue: func() *model.Value {
+ res := model.NewMapValue()
+ if err := res.SetMapKey("greeting", model.NewStringValue("hello")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ sliceValue: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("hello")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ nullValue: func() *model.Value {
+ return model.NewNullValue()
+ },
+ },
+ {
+ name: "go types non ptr",
+ stringValue: func() *model.Value {
+ v := "hello"
+ return model.NewValue(v)
+ },
+ intValue: func() *model.Value {
+ v := int64(1)
+ return model.NewValue(v)
+ },
+ floatValue: func() *model.Value {
+ v := 1.0
+ return model.NewValue(v)
+ },
+ boolValue: func() *model.Value {
+ v := true
+ return model.NewValue(v)
+ },
+ mapValue: func() *model.Value {
+ v := map[string]interface{}{
+ "greeting": "hello",
+ }
+ return model.NewValue(v)
+ },
+ sliceValue: func() *model.Value {
+ v := []interface{}{
+ "hello",
+ }
+ return model.NewValue(v)
+ },
+ nullValue: func() *model.Value {
+ return model.NewValue(nil)
+ },
+ },
+ {
+ name: "go types ptr",
+ stringValue: func() *model.Value {
+ v := "hello"
+ return model.NewValue(&v)
+ },
+ intValue: func() *model.Value {
+ v := int64(1)
+ return model.NewValue(&v)
+ },
+ floatValue: func() *model.Value {
+ v := 1.0
+ return model.NewValue(&v)
+ },
+ boolValue: func() *model.Value {
+ v := true
+ return model.NewValue(&v)
+ },
+ mapValue: func() *model.Value {
+ v := map[string]interface{}{
+ "greeting": "hello",
+ }
+ return model.NewValue(&v)
+ },
+ sliceValue: func() *model.Value {
+ v := []interface{}{
+ "hello",
+ }
+ return model.NewValue(&v)
+ },
+ nullValue: func() *model.Value {
+ var x any
+ return model.NewValue(&x)
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+ t.Run(tc.name, func(t *testing.T) {
+ t.Run("string", setTestCase{
+ valueFn: tc.stringValue,
+ newValue: model.NewStringValue("world"),
+ }.run)
+ t.Run("int", setTestCase{
+ valueFn: tc.intValue,
+ newValue: model.NewIntValue(2),
+ }.run)
+ t.Run("float", setTestCase{
+ valueFn: tc.floatValue,
+ newValue: model.NewFloatValue(2),
+ }.run)
+ t.Run("bool", setTestCase{
+ valueFn: tc.boolValue,
+ newValue: model.NewBoolValue(false),
+ }.run)
+ t.Run("map", setTestCase{
+ valueFn: tc.mapValue,
+ newValueFn: func() *model.Value {
+ res := model.NewMapValue()
+ if err := res.SetMapKey("greeting", model.NewStringValue("world")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("slice", setTestCase{
+ valueFn: tc.sliceValue,
+ newValueFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("world")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("string over int", setTestCase{
+ valueFn: tc.intValue,
+ newValue: model.NewStringValue("world"),
+ }.run)
+ t.Run("int over float", setTestCase{
+ valueFn: tc.floatValue,
+ newValue: model.NewIntValue(2),
+ }.run)
+ t.Run("float over bool", setTestCase{
+ valueFn: tc.boolValue,
+ newValue: model.NewFloatValue(2),
+ }.run)
+ t.Run("bool over map", setTestCase{
+ valueFn: tc.mapValue,
+ newValue: model.NewBoolValue(true),
+ }.run)
+ t.Run("map over slice", setTestCase{
+ valueFn: tc.sliceValue,
+ newValueFn: func() *model.Value {
+ res := model.NewMapValue()
+ if err := res.SetMapKey("greeting", model.NewStringValue("world")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("string over slice", setTestCase{
+ valueFn: tc.sliceValue,
+ newValue: model.NewStringValue("world"),
+ }.run)
+ t.Run("slice over map", setTestCase{
+ valueFn: tc.mapValue,
+ newValueFn: func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewStringValue("world")); err != nil {
+ t.Fatal(err)
+ }
+ return res
+ },
+ }.run)
+ t.Run("string over null", setTestCase{
+ valueFn: tc.nullValue,
+ newValue: model.NewStringValue("world"),
+ }.run)
+ })
+ }
+}
diff --git a/model/value_slice.go b/model/value_slice.go
new file mode 100644
index 00000000..52ce98a6
--- /dev/null
+++ b/model/value_slice.go
@@ -0,0 +1,153 @@
+package model
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// NewSliceValue returns a new slice value.
+func NewSliceValue() *Value {
+ res := newPtr()
+ s := reflect.MakeSlice(reflect.SliceOf(reflect.TypeFor[any]()), 0, 0)
+ ptr := reflect.New(reflect.SliceOf(reflect.TypeFor[any]()))
+ ptr.Elem().Set(s)
+ res.Elem().Set(ptr)
+ return NewValue(res)
+}
+
+// IsSlice returns true if the value is a slice.
+func (v *Value) IsSlice() bool {
+ return v.UnpackKinds(reflect.Interface, reflect.Ptr).isSlice()
+}
+
+func (v *Value) isSlice() bool {
+ return v.Value.Kind() == reflect.Slice
+}
+
+// Append appends a value to the slice.
+func (v *Value) Append(val *Value) error {
+ // Branches behave differently when appending to a slice.
+ // We expect each item in a branch to be its own value.
+ if val.IsBranch() {
+ return val.RangeSlice(func(_ int, item *Value) error {
+ return v.Append(item)
+ })
+ }
+
+ unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr)
+ if !unpacked.isSlice() {
+ return ErrUnexpectedType{
+ Expected: TypeSlice,
+ Actual: v.Type(),
+ }
+ }
+ newVal := reflect.Append(unpacked.Value, val.Value)
+ unpacked.Value.Set(newVal)
+ return nil
+}
+
+// SliceLen returns the length of the slice.
+func (v *Value) SliceLen() (int, error) {
+ unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr)
+ if !unpacked.isSlice() {
+ return 0, ErrUnexpectedType{
+ Expected: TypeSlice,
+ Actual: v.Type(),
+ }
+ }
+ return unpacked.Value.Len(), nil
+}
+
+// GetSliceIndex returns the value at the specified index in the slice.
+func (v *Value) GetSliceIndex(i int) (*Value, error) {
+ unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr)
+ if !unpacked.isSlice() {
+ return nil, ErrUnexpectedType{
+ Expected: TypeSlice,
+ Actual: v.Type(),
+ }
+ }
+ if i < 0 || i >= unpacked.Value.Len() {
+ return nil, SliceIndexOutOfRange{Index: i}
+ }
+ res := NewValue(unpacked.Value.Index(i))
+ return res, nil
+}
+
+// SetSliceIndex sets the value at the specified index in the slice.
+func (v *Value) SetSliceIndex(i int, val *Value) error {
+ unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr)
+ if !unpacked.isSlice() {
+ return ErrUnexpectedType{
+ Expected: TypeSlice,
+ Actual: v.Type(),
+ }
+ }
+ if i < 0 || i >= unpacked.Value.Len() {
+ return SliceIndexOutOfRange{Index: i}
+ }
+ unpacked.Value.Index(i).Set(val.Value)
+ return nil
+}
+
+// RangeSlice iterates over each item in the slice and calls the provided function.
+func (v *Value) RangeSlice(f func(int, *Value) error) error {
+ length, err := v.SliceLen()
+ if err != nil {
+ return fmt.Errorf("error getting slice length: %w", err)
+ }
+
+ for i := 0; i < length; i++ {
+ va, err := v.GetSliceIndex(i)
+ if err != nil {
+ return fmt.Errorf("error getting slice index %d: %w", i, err)
+ }
+ if err := f(i, va); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// SliceIndexRange returns a new slice containing the values between the start and end indexes.
+// Comparable to go's slice[start:end].
+func (v *Value) SliceIndexRange(start, end int) (*Value, error) {
+ l, err := v.SliceLen()
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice length: %w", err)
+ }
+
+ if start < 0 {
+ start = l + start
+ }
+ if end < 0 {
+ end = l + end
+ }
+
+ res := NewSliceValue()
+
+ if start > end {
+ for i := start; i >= end; i-- {
+ item, err := v.GetSliceIndex(i)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ if err := res.Append(item); err != nil {
+ return nil, fmt.Errorf("error appending value to slice: %w", err)
+ }
+ }
+ } else {
+ for i := start; i <= end; i++ {
+ item, err := v.GetSliceIndex(i)
+ if err != nil {
+ return nil, fmt.Errorf("error getting slice index: %w", err)
+ }
+ if err := res.Append(item); err != nil {
+ return nil, fmt.Errorf("error appending value to slice: %w", err)
+ }
+ }
+ }
+
+ return res, nil
+}
diff --git a/model/value_slice_test.go b/model/value_slice_test.go
new file mode 100644
index 00000000..91b9b9a1
--- /dev/null
+++ b/model/value_slice_test.go
@@ -0,0 +1,197 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestSlice(t *testing.T) {
+ standardSlice := func() *model.Value {
+ return model.NewValue([]any{"foo", "bar"})
+ }
+
+ modelSlice := func() *model.Value {
+ res := model.NewSliceValue()
+ if err := res.Append(model.NewValue("foo")); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if err := res.Append(model.NewValue("bar")); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ return res
+ }
+
+ runTests := func(v func() *model.Value) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Run("IsSlice", func(t *testing.T) {
+ v := v()
+ if !v.IsSlice() {
+ t.Errorf("expected value to be a slice")
+ }
+ })
+ t.Run("GetSliceIndex", func(t *testing.T) {
+ v := v()
+ foo, err := v.GetSliceIndex(0)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := foo.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "foo" {
+ t.Errorf("expected foo, got %s", got)
+ }
+ })
+ t.Run("SetSliceIndex", func(t *testing.T) {
+ v := v()
+ if err := v.SetSliceIndex(0, model.NewValue("baz")); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ baz, err := v.GetSliceIndex(0)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := baz.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "baz" {
+ t.Errorf("expected baz, got %s", got)
+ }
+ })
+ t.Run("Len", func(t *testing.T) {
+ v := v()
+ got, err := v.SliceLen()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != 2 {
+ t.Errorf("expected len of 2, got %d", got)
+ }
+ })
+ t.Run("RangeSlice", func(t *testing.T) {
+ v := v()
+ var keys []int
+ var vals []string
+ err := v.RangeSlice(func(k int, v *model.Value) error {
+ keys = append(keys, k)
+ s, err := v.StringValue()
+ if err != nil {
+ return err
+ }
+ vals = append(vals, s)
+ return nil
+ })
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if len(keys) != 2 {
+ t.Errorf("expected 2 keys, got %d", len(keys))
+ }
+ if len(vals) != 2 {
+ t.Errorf("expected 2 vals, got %d", len(keys))
+ }
+ exp := []string{"foo", "bar"}
+
+ for k, e := range exp {
+ if keys[k] != k {
+ t.Errorf("expected key %d, got %d", k, keys[k])
+ }
+ if vals[k] != e {
+ t.Errorf("expected val %s, got %s", e, vals[k])
+ }
+ }
+ })
+ //t.Run("DeleteMapKey", func(t *testing.T) {
+ // v := v()
+ // if _, err := v.GetSliceIndex(1); err != nil {
+ // t.Errorf("unexpected error: %s", err)
+ // return
+ // }
+ // if err := v.DeleteSliceIndex(1); err != nil {
+ // t.Errorf("unexpected error: %s", err)
+ // return
+ // }
+ // _, err := v.GetSliceIndex(1)
+ // notFoundErr := &model.SliceIndexOutOfRange{}
+ // if !errors.As(err, ¬FoundErr) {
+ // t.Errorf("expected index not found error, got %s", err)
+ // }
+ //})
+ t.Run("SliceIndexRange", func(t *testing.T) {
+ t.Run("last element", func(t *testing.T) {
+ v := v()
+ s, err := v.SliceIndexRange(-1, -1)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ length, err := s.SliceLen()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if length != 1 {
+ t.Errorf("expected length of 1, got %d", length)
+ }
+
+ val, err := s.GetSliceIndex(0)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := val.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "bar" {
+ t.Errorf("expected bar, got %s", got)
+ }
+ })
+ t.Run("first element", func(t *testing.T) {
+ v := v()
+ s, err := v.SliceIndexRange(0, 0)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ length, err := s.SliceLen()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if length != 1 {
+ t.Errorf("expected length of 1, got %d", length)
+ }
+
+ val, err := s.GetSliceIndex(0)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ got, err := val.StringValue()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != "foo" {
+ t.Errorf("expected foo, got %s", got)
+ }
+ })
+ })
+ }
+ }
+
+ t.Run("standard slice", runTests(standardSlice))
+ t.Run("model slice", runTests(modelSlice))
+}
diff --git a/model/value_test.go b/model/value_test.go
new file mode 100644
index 00000000..322fce56
--- /dev/null
+++ b/model/value_test.go
@@ -0,0 +1,53 @@
+package model_test
+
+import (
+ "testing"
+
+ "github.com/tomwright/dasel/v3/model"
+)
+
+func TestType_String(t *testing.T) {
+ run := func(ty model.Type, exp string) func(*testing.T) {
+ return func(t *testing.T) {
+ got := ty.String()
+ if got != exp {
+ t.Errorf("expected %s, got %s", exp, got)
+ }
+ }
+ }
+ t.Run("string", run(model.TypeString, "string"))
+ t.Run("int", run(model.TypeInt, "int"))
+ t.Run("float", run(model.TypeFloat, "float"))
+ t.Run("bool", run(model.TypeBool, "bool"))
+ t.Run("map", run(model.TypeMap, "map"))
+ t.Run("slice", run(model.TypeSlice, "array"))
+ t.Run("slice", run(model.TypeUnknown, "unknown"))
+ t.Run("slice", run(model.TypeNull, "null"))
+}
+
+func TestValue_Len(t *testing.T) {
+ run := func(v *model.Value, exp int) func(*testing.T) {
+ return func(t *testing.T) {
+ got, err := v.Len()
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ return
+ }
+ if got != exp {
+ t.Errorf("expected %d, got %d", exp, got)
+ }
+ }
+ }
+ t.Run("string", func(t *testing.T) {
+ t.Run("empty", run(model.NewStringValue(""), 0))
+ t.Run("non-empty", run(model.NewStringValue("hello"), 5))
+ })
+ t.Run("slice", func(t *testing.T) {
+ t.Run("empty", run(model.NewSliceValue(), 0))
+ t.Run("non-empty", run(model.NewValue([]any{1, 2, 3}), 3))
+ })
+ t.Run("map", func(t *testing.T) {
+ t.Run("empty", run(model.NewMapValue(), 0))
+ t.Run("non-empty", run(model.NewValue(map[string]any{"one": 1, "two": 2, "three": 3}), 3))
+ })
+}
diff --git a/parsing/d/reader.go b/parsing/d/reader.go
new file mode 100644
index 00000000..0e7b544b
--- /dev/null
+++ b/parsing/d/reader.go
@@ -0,0 +1,38 @@
+package json
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/execution"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+const (
+ // Dasel represents the dasel format.
+ Dasel parsing.Format = "dasel"
+)
+
+var _ parsing.Reader = (*daselReader)(nil)
+
+func init() {
+ parsing.RegisterReader(Dasel, newDaselReader)
+}
+
+func newDaselReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &daselReader{}, nil
+}
+
+type daselReader struct {
+}
+
+func (dr *daselReader) Read(in []byte) (*model.Value, error) {
+ if len(in) == 0 {
+ return model.NewNullValue(), nil
+ }
+ out, err := execution.ExecuteSelector(string(in), model.NewNullValue(), execution.NewOptions())
+ if err != nil {
+ return nil, fmt.Errorf("failed to read value: %w", err)
+ }
+ return out, nil
+}
diff --git a/parsing/format.go b/parsing/format.go
new file mode 100644
index 00000000..096c5fc3
--- /dev/null
+++ b/parsing/format.go
@@ -0,0 +1,49 @@
+package parsing
+
+import (
+ "fmt"
+)
+
+// Format represents a file format.
+type Format string
+
+// NewReader creates a new reader for the format.
+func (f Format) NewReader(options ReaderOptions) (Reader, error) {
+ fn, ok := readers[f]
+ if !ok {
+ return nil, fmt.Errorf("unsupported reader file format: %s", f)
+ }
+ return fn(options)
+}
+
+// NewWriter creates a new writer for the format.
+func (f Format) NewWriter(options WriterOptions) (Writer, error) {
+ fn, ok := writers[f]
+ if !ok {
+ return nil, fmt.Errorf("unsupported writer file format: %s", f)
+ }
+ return fn(options)
+}
+
+// String returns the string representation of the format.
+func (f Format) String() string {
+ return string(f)
+}
+
+// RegisteredReaders returns a list of registered readers.
+func RegisteredReaders() []Format {
+ var formats []Format
+ for format := range readers {
+ formats = append(formats, format)
+ }
+ return formats
+}
+
+// RegisteredWriters returns a list of registered writers.
+func RegisteredWriters() []Format {
+ var formats []Format
+ for format := range writers {
+ formats = append(formats, format)
+ }
+ return formats
+}
diff --git a/parsing/hcl/hcl.go b/parsing/hcl/hcl.go
new file mode 100644
index 00000000..17e9c3c8
--- /dev/null
+++ b/parsing/hcl/hcl.go
@@ -0,0 +1,18 @@
+package hcl
+
+import (
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+const (
+ // HCL represents the hcl2 file format.
+ HCL parsing.Format = "hcl"
+)
+
+var _ parsing.Reader = (*hclReader)(nil)
+var _ parsing.Writer = (*hclWriter)(nil)
+
+func init() {
+ parsing.RegisterReader(HCL, newHCLReader)
+ parsing.RegisterWriter(HCL, newHCLWriter)
+}
diff --git a/parsing/hcl/reader.go b/parsing/hcl/reader.go
new file mode 100644
index 00000000..b6fa445a
--- /dev/null
+++ b/parsing/hcl/reader.go
@@ -0,0 +1,236 @@
+package hcl
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/hcl/v2"
+ "github.com/hashicorp/hcl/v2/gohcl"
+ "github.com/hashicorp/hcl/v2/hclsyntax"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/zclconf/go-cty/cty"
+)
+
+func newHCLReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &hclReader{
+ alwaysReadLabelsToSlice: options.Ext["hcl-block-format"] == "array",
+ }, nil
+}
+
+type hclReader struct {
+ alwaysReadLabelsToSlice bool
+}
+
+// Read reads a value from a byte slice.
+// Reads the HCL data into a model that follows the HCL JSON spec.
+// See https://github.com/hashicorp/hcl/blob/main/json%2Fspec.md
+func (r *hclReader) Read(data []byte) (*model.Value, error) {
+ f, _ := hclsyntax.ParseConfig(data, "input", hcl.InitialPos)
+
+ body, ok := f.Body.(*hclsyntax.Body)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert file body type")
+ }
+
+ return r.decodeHCLBody(body)
+}
+
+func (r *hclReader) decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) {
+ res := model.NewMapValue()
+ var err error
+
+ for _, attr := range body.Attributes {
+ val, err := r.decodeHCLExpr(attr.Expr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode attr %q: %w", attr.Name, err)
+ }
+
+ if err := res.SetMapKey(attr.Name, val); err != nil {
+ return nil, err
+ }
+ }
+
+ res, err = r.decodeHCLBodyBlocks(body, res)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+func (r *hclReader) decodeHCLBodyBlocks(body *hclsyntax.Body, res *model.Value) (*model.Value, error) {
+ for _, block := range body.Blocks {
+ if err := r.decodeHCLBlock(block, res); err != nil {
+ return nil, err
+ }
+ }
+ return res, nil
+}
+
+func (r *hclReader) decodeHCLBlock(block *hclsyntax.Block, res *model.Value) error {
+ key := block.Type
+ v := res
+ for _, label := range block.Labels {
+ exists, err := v.MapKeyExists(key)
+ if err != nil {
+ return err
+ }
+
+ if exists {
+ keyV, err := v.GetMapKey(key)
+ if err != nil {
+ return err
+ }
+ v = keyV
+ } else {
+ keyV := model.NewMapValue()
+ if err := v.SetMapKey(key, keyV); err != nil {
+ return err
+ }
+ v = keyV
+ }
+
+ key = label
+ }
+
+ body, err := r.decodeHCLBody(block.Body)
+ if err != nil {
+ return err
+ }
+
+ exists, err := v.MapKeyExists(key)
+ if err != nil {
+ return err
+ }
+ if exists {
+ keyV, err := v.GetMapKey(key)
+ if err != nil {
+ return err
+ }
+
+ switch keyV.Type() {
+ case model.TypeSlice:
+ if err := keyV.Append(body); err != nil {
+ return err
+ }
+ case model.TypeMap:
+ // Previous value was a map.
+ // Create a new slice containing the previous map and the new map.
+ newKeyV := model.NewSliceValue()
+ previousKeyV, err := keyV.Copy()
+ if err != nil {
+ return err
+ }
+ if err := newKeyV.Append(previousKeyV); err != nil {
+ return err
+ }
+ if err := newKeyV.Append(body); err != nil {
+ return err
+ }
+ if err := keyV.Set(newKeyV); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unexpected type: %s", keyV.Type())
+ }
+ } else {
+ if r.alwaysReadLabelsToSlice {
+ slice := model.NewSliceValue()
+ if err := slice.Append(body); err != nil {
+ return err
+ }
+ if err := v.SetMapKey(key, slice); err != nil {
+ return err
+ }
+ } else {
+ if err := v.SetMapKey(key, body); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func (r *hclReader) decodeHCLExpr(expr hcl.Expression) (*model.Value, error) {
+ source := cty.Value{}
+ _ = gohcl.DecodeExpression(expr, nil, &source)
+
+ return r.decodeCtyValue(source)
+}
+
+func (r *hclReader) decodeCtyValue(source cty.Value) (res *model.Value, err error) {
+ defer func() {
+ r := recover()
+ if r != nil {
+ err = fmt.Errorf("failed to decode: %v", r)
+ return
+ }
+ }()
+ if source.IsNull() {
+ return model.NewNullValue(), nil
+ }
+
+ sourceT := source.Type()
+ switch {
+ case sourceT.IsListType(), sourceT.IsTupleType():
+ res = model.NewSliceValue()
+ it := source.ElementIterator()
+ for it.Next() {
+ k, v := it.Element()
+ // We don't need the index as they should be in order.
+ // Just validates the key is correct.
+ _, _ = k.AsBigFloat().Float64()
+
+ val, err := r.decodeCtyValue(v)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode tuple value: %w", err)
+ }
+
+ if err := res.Append(val); err != nil {
+ return nil, err
+ }
+ }
+ return res, nil
+ case sourceT.IsMapType(), sourceT.IsObjectType(), sourceT.IsSetType():
+ v := model.NewMapValue()
+ it := source.ElementIterator()
+ for it.Next() {
+ k, el := it.Element()
+ if k.Type() != cty.String {
+ return nil, fmt.Errorf("object key must be a string")
+ }
+ kStr := k.AsString()
+
+ elVal, err := r.decodeCtyValue(el)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode object value: %w", err)
+ }
+
+ if err := v.SetMapKey(kStr, elVal); err != nil {
+ return nil, err
+ }
+ }
+ return v, nil
+ case sourceT.IsPrimitiveType():
+ switch sourceT {
+ case cty.String:
+ v := source.AsString()
+ return model.NewStringValue(v), nil
+ case cty.Bool:
+ v := source.True()
+ return model.NewBoolValue(v), nil
+ case cty.Number:
+ v := source.AsBigFloat()
+ f64, _ := v.Float64()
+ if v.IsInt() {
+ return model.NewIntValue(int64(f64)), nil
+ }
+ return model.NewFloatValue(f64), nil
+ default:
+ return nil, fmt.Errorf("unhandled primitive type %q", source.Type())
+ }
+ default:
+ return nil, fmt.Errorf("unsupported type: %s", sourceT.FriendlyName())
+ }
+}
diff --git a/parsing/hcl/reader_test.go b/parsing/hcl/reader_test.go
new file mode 100644
index 00000000..6b60a8f9
--- /dev/null
+++ b/parsing/hcl/reader_test.go
@@ -0,0 +1,87 @@
+package hcl_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/hcl"
+)
+
+type readTestCase struct {
+ in string
+}
+
+func (tc readTestCase) run(t *testing.T) {
+ r, err := hcl.HCL.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ in := []byte(tc.in)
+
+ got, err := r.Read(in)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ return
+ }
+
+ fmt.Println(got)
+}
+
+func TestHclReader_Read(t *testing.T) {
+ t.Run("document a", readTestCase{
+ in: `io_mode = "async"
+
+service "http" "web_proxy" {
+ listen_addr = "127.0.0.1:8080"
+
+ process "main" {
+ command = ["/usr/local/bin/awesome-app", "server"]
+ }
+
+ process "mgmt" {
+ command = ["/usr/local/bin/awesome-app", "mgmt"]
+ }
+}`,
+ }.run)
+ t.Run("document b", readTestCase{
+ in: `resource "aws_instance" "example" {
+ # (resource configuration omitted for brevity)
+
+ provisioner "local-exec" {
+ command = "echo 'Hello World' >example.txt"
+ }
+ provisioner "file" {
+ source = "example.txt"
+ destination = "/tmp/example.txt"
+ }
+ provisioner "remote-exec" {
+ inline = [
+ "sudo install-something -f /tmp/example.txt",
+ ]
+ }
+}`,
+ }.run)
+ t.Run("document c", readTestCase{
+ in: `image_id = "ami-123"
+cluster_min_nodes = 2
+cluster_decimal_nodes = 2.2
+cluster_max_nodes = true
+availability_zone_names = [
+"us-east-1a",
+"us-west-1c",
+]
+docker_ports = [{
+internal = 8300
+external = 8300
+protocol = "tcp"
+},
+{
+internal = 8301
+external = 8301
+protocol = "tcp"
+}
+]`,
+ }.run)
+}
diff --git a/parsing/hcl/writer.go b/parsing/hcl/writer.go
new file mode 100644
index 00000000..7ebb510d
--- /dev/null
+++ b/parsing/hcl/writer.go
@@ -0,0 +1,187 @@
+package hcl
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/hashicorp/hcl/v2/hclwrite"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/zclconf/go-cty/cty"
+)
+
+func newHCLWriter(options parsing.WriterOptions) (parsing.Writer, error) {
+ return &hclWriter{}, nil
+}
+
+type hclWriter struct {
+ options parsing.WriterOptions
+}
+
+// Write writes a value to a byte slice.
+func (j *hclWriter) Write(value *model.Value) ([]byte, error) {
+ f, err := j.valueToFile(value)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := new(bytes.Buffer)
+ if _, err := f.WriteTo(buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func (j *hclWriter) valueToFile(v *model.Value) (*hclwrite.File, error) {
+ f := hclwrite.NewEmptyFile()
+
+ body := f.Body()
+
+ if err := j.addValueToBody(nil, v, body); err != nil {
+ return nil, err
+ }
+
+ return f, nil
+}
+
+func (j *hclWriter) addValueToBody(previousLabels []string, v *model.Value, body *hclwrite.Body) error {
+ if !v.IsMap() {
+ return fmt.Errorf("hcl body is expected to be a map, got %s", v.Type())
+ }
+
+ kvs, err := v.MapKeyValues()
+ if err != nil {
+ return err
+ }
+
+ blocks := make([]*hclwrite.Block, 0)
+ for _, kv := range kvs {
+ switch kv.Value.Type() {
+ case model.TypeMap:
+ block, err := j.valueToBlock(kv.Key, previousLabels, kv.Value)
+ if err != nil {
+ return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err)
+ }
+ blocks = append(blocks, block)
+ case model.TypeSlice:
+ vals := make([]cty.Value, 0)
+
+ allMaps := true
+
+ if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error {
+ ctyVal, err := j.valueToCty(value)
+ if err != nil {
+ return err
+ }
+ vals = append(vals, ctyVal)
+
+ if !value.IsMap() {
+ allMaps = false
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ if allMaps {
+ if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error {
+ block, err := j.valueToBlock(kv.Key, previousLabels, value)
+ if err != nil {
+ return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err)
+ }
+ blocks = append(blocks, block)
+ return nil
+ }); err != nil {
+ return err
+ }
+ } else {
+ body.SetAttributeValue(kv.Key, cty.TupleVal(vals))
+ }
+
+ default:
+ ctyVal, err := j.valueToCty(kv.Value)
+ if err != nil {
+ return fmt.Errorf("failed to encode attribute %q: %w", kv.Key, err)
+ }
+ body.SetAttributeValue(kv.Key, ctyVal)
+ }
+ }
+
+ for _, block := range blocks {
+ body.AppendBlock(block)
+ }
+
+ return nil
+}
+
+func (j *hclWriter) valueToCty(v *model.Value) (cty.Value, error) {
+ switch v.Type() {
+ case model.TypeString:
+ val, err := v.StringValue()
+ if err != nil {
+ return cty.Value{}, err
+ }
+ return cty.StringVal(val), nil
+ case model.TypeBool:
+ val, err := v.BoolValue()
+ if err != nil {
+ return cty.Value{}, err
+ }
+ return cty.BoolVal(val), nil
+ case model.TypeInt:
+ val, err := v.IntValue()
+ if err != nil {
+ return cty.Value{}, err
+ }
+ return cty.NumberIntVal(val), nil
+ case model.TypeFloat:
+ val, err := v.FloatValue()
+ if err != nil {
+ return cty.Value{}, err
+ }
+ return cty.NumberFloatVal(val), nil
+ case model.TypeNull:
+ return cty.NullVal(cty.NilType), nil
+ case model.TypeSlice:
+ var vals []cty.Value
+ if err := v.RangeSlice(func(_ int, value *model.Value) error {
+ ctyVal, err := j.valueToCty(value)
+ if err != nil {
+ return err
+ }
+ vals = append(vals, ctyVal)
+ return nil
+ }); err != nil {
+ return cty.Value{}, err
+ }
+ return cty.TupleVal(vals), nil
+ case model.TypeMap:
+ mapV := map[string]cty.Value{}
+ if err := v.RangeMap(func(s string, value *model.Value) error {
+ ctyVal, err := j.valueToCty(value)
+ if err != nil {
+ return err
+ }
+ mapV[s] = ctyVal
+ return nil
+ }); err != nil {
+ return cty.Value{}, err
+ }
+ return cty.ObjectVal(mapV), nil
+ default:
+ return cty.Value{}, fmt.Errorf("unhandled type when converting to cty value %q", v.Type())
+ }
+}
+
+func (j *hclWriter) valueToBlock(key string, labels []string, v *model.Value) (*hclwrite.Block, error) {
+ if !v.IsMap() {
+ return nil, fmt.Errorf("must be map")
+ }
+
+ b := hclwrite.NewBlock(key, labels)
+
+ if err := j.addValueToBody(labels, v, b.Body()); err != nil {
+ return nil, err
+ }
+
+ return b, nil
+}
diff --git a/parsing/hcl/writer_test.go b/parsing/hcl/writer_test.go
new file mode 100644
index 00000000..c7aa398e
--- /dev/null
+++ b/parsing/hcl/writer_test.go
@@ -0,0 +1,65 @@
+package hcl_test
+
+import (
+ "github.com/google/go-cmp/cmp"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/hcl"
+)
+
+type readWriteTestCase struct {
+ in string
+}
+
+func (tc readWriteTestCase) run(t *testing.T) {
+ r, err := hcl.HCL.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ w, err := hcl.HCL.NewWriter(parsing.DefaultWriterOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ in := []byte(tc.in)
+
+ data, err := r.Read(in)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ return
+ }
+
+ got, err := w.Write(data)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ return
+ }
+ gotStr := string(got)
+
+ if !cmp.Equal(tc.in, gotStr) {
+ t.Errorf("unexpected output: %s", cmp.Diff(tc.in, gotStr))
+ }
+}
+
+func TestHclReader_ReadWrite(t *testing.T) {
+ t.Run("document a", readWriteTestCase{
+ in: `io_mode = "async"
+
+service "http" "web_proxy" {
+ listen_addr = "127.0.0.1:8080"
+
+ process "main" {
+ command = ["/usr/local/bin/awesome-app", "server"]
+ }
+
+ process "mgmt" {
+ command = ["/usr/local/bin/awesome-app", "mgmt"]
+ }
+
+ process "mgmt" {
+ command = ["/usr/local/bin/awesome-app", "mgmt2"]
+ }
+}`,
+ }.run)
+}
diff --git a/parsing/json/json.go b/parsing/json/json.go
new file mode 100644
index 00000000..5486d192
--- /dev/null
+++ b/parsing/json/json.go
@@ -0,0 +1,406 @@
+package json
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+const (
+ // JSON represents the JSON file format.
+ JSON parsing.Format = "json"
+
+ jsonOpenObject = json.Delim('{')
+ jsonCloseObject = json.Delim('}')
+ jsonOpenArray = json.Delim('[')
+ jsonCloseArray = json.Delim(']')
+)
+
+var _ parsing.Reader = (*jsonReader)(nil)
+var _ parsing.Writer = (*jsonWriter)(nil)
+
+func init() {
+ parsing.RegisterReader(JSON, newJSONReader)
+ parsing.RegisterWriter(JSON, newJSONWriter)
+}
+
+func newJSONReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &jsonReader{}, nil
+}
+
+// NewJSONWriter creates a new JSON writer.
+func newJSONWriter(options parsing.WriterOptions) (parsing.Writer, error) {
+ return &jsonWriter{
+ options: options,
+ }, nil
+}
+
+type jsonReader struct{}
+
+// Read reads a value from a byte slice.
+func (j *jsonReader) Read(data []byte) (*model.Value, error) {
+ decoder := json.NewDecoder(bytes.NewReader(data))
+ decoder.UseNumber()
+
+ t, err := decoder.Token()
+ if err != nil {
+ return nil, err
+ }
+
+ var res *model.Value
+
+ switch t {
+ case jsonOpenObject:
+ res, err = j.decodeObject(decoder)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode object: %w", err)
+ }
+ case jsonOpenArray:
+ res, err = j.decodeArray(decoder)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode array: %w", err)
+ }
+ default:
+ res, err = j.decodeToken(decoder, t)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return res, nil
+}
+
+func (j *jsonReader) decodeObject(decoder *json.Decoder) (*model.Value, error) {
+ res := model.NewMapValue()
+
+ var key any = nil
+
+ for {
+ t, err := decoder.Token()
+ if err != nil {
+ // We don't expect an EOF here since we're in the middle of processing an object.
+ return res, err
+ }
+
+ switch t {
+ case jsonOpenArray:
+ if key == nil {
+ return res, fmt.Errorf("unexpected token: %v", t)
+ }
+ value, err := j.decodeArray(decoder)
+ if err != nil {
+ return res, err
+ }
+ if err := res.SetMapKey(key.(string), value); err != nil {
+ return res, err
+ }
+ key = nil
+ case jsonCloseArray:
+ return res, fmt.Errorf("unexpected token: %v", t)
+ case jsonCloseObject:
+ return res, nil
+ case jsonOpenObject:
+ if key == nil {
+ return res, fmt.Errorf("unexpected token: %v", t)
+ }
+ value, err := j.decodeObject(decoder)
+ if err != nil {
+ return res, err
+ }
+ if err := res.SetMapKey(key.(string), value); err != nil {
+ return res, err
+ }
+ key = nil
+ default:
+ if key == nil {
+ if tStr, ok := t.(string); ok {
+ key = tStr
+ } else {
+ return nil, fmt.Errorf("unexpected token: %v", t)
+ }
+ } else {
+ value, err := j.decodeToken(decoder, t)
+ if err != nil {
+ return nil, err
+ }
+ if err := res.SetMapKey(key.(string), value); err != nil {
+ return res, err
+ }
+ key = nil
+ }
+ }
+ }
+}
+
+func (j *jsonReader) decodeArray(decoder *json.Decoder) (*model.Value, error) {
+ res := model.NewSliceValue()
+ for {
+ t, err := decoder.Token()
+ if err != nil {
+ // We don't expect an EOF here since we're in the middle of processing an object.
+ return res, err
+ }
+
+ switch t {
+ case jsonOpenArray:
+ value, err := j.decodeArray(decoder)
+ if err != nil {
+ return res, err
+ }
+ if err := res.Append(value); err != nil {
+ return res, err
+ }
+ case jsonCloseArray:
+ return res, nil
+ case jsonCloseObject:
+ return res, fmt.Errorf("unexpected token: %t", t)
+ case jsonOpenObject:
+ value, err := j.decodeObject(decoder)
+ if err != nil {
+ return res, err
+ }
+ if err := res.Append(value); err != nil {
+ return res, err
+ }
+ default:
+ value, err := j.decodeToken(decoder, t)
+ if err != nil {
+ return nil, err
+ }
+ if err := res.Append(value); err != nil {
+ return res, err
+ }
+ }
+ }
+}
+
+func (j *jsonReader) decodeToken(decoder *json.Decoder, t json.Token) (*model.Value, error) {
+ switch tv := t.(type) {
+ case json.Number:
+ strNum := tv.String()
+ if strings.Contains(strNum, ".") {
+ floatNum, err := tv.Float64()
+ if err == nil {
+ return model.NewFloatValue(floatNum), nil
+ }
+ return nil, err
+ }
+ intNum, err := tv.Int64()
+ if err == nil {
+ return model.NewIntValue(intNum), nil
+ }
+
+ return nil, err
+ default:
+ return model.NewValue(tv), nil
+ }
+}
+
+type jsonWriter struct {
+ options parsing.WriterOptions
+}
+
+// Write writes a value to a byte slice.
+func (j *jsonWriter) Write(value *model.Value) ([]byte, error) {
+ buf := new(bytes.Buffer)
+
+ es := encoderState{indentStr: " "}
+
+ encoderFn := func(v any) error {
+ res, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ _, err = buf.Write(res)
+ return err
+ }
+
+ if value.IsBranch() {
+ if err := value.RangeSlice(func(i int, v *model.Value) error {
+ if err := j.write(buf, encoderFn, es, v); err != nil {
+ return err
+ }
+
+ if _, err := buf.Write([]byte("\n")); err != nil {
+ return err
+ }
+
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ } else {
+ if err := j.write(buf, encoderFn, es, value); err != nil {
+ return nil, err
+ }
+
+ if _, err := buf.Write([]byte("\n")); err != nil {
+ return nil, err
+ }
+ }
+
+ return buf.Bytes(), nil
+}
+
+type encoderState struct {
+ indent int
+ indentStr string
+}
+
+func (es encoderState) inc() encoderState {
+ es.indent++
+ return es
+}
+
+func (es encoderState) writeIndent(w io.Writer) error {
+ if es.indent == 0 || es.indentStr == "" {
+ return nil
+ }
+ i := strings.Repeat(es.indentStr, es.indent)
+ if _, err := w.Write([]byte(i)); err != nil {
+ return err
+ }
+ return nil
+}
+
+type encoderFn func(v any) error
+
+func (j *jsonWriter) write(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error {
+ switch value.Type() {
+ case model.TypeMap:
+ return j.writeMap(w, encoder, es, value)
+ case model.TypeSlice:
+ return j.writeSlice(w, encoder, es, value)
+ case model.TypeString:
+ val, err := value.StringValue()
+ if err != nil {
+ return err
+ }
+ return encoder(val)
+ case model.TypeInt:
+ val, err := value.IntValue()
+ if err != nil {
+ return err
+ }
+ return encoder(val)
+ case model.TypeFloat:
+ val, err := value.FloatValue()
+ if err != nil {
+ return err
+ }
+ return encoder(val)
+ case model.TypeBool:
+ val, err := value.BoolValue()
+ if err != nil {
+ return err
+ }
+ return encoder(val)
+ case model.TypeNull:
+ return encoder(nil)
+ default:
+ return fmt.Errorf("unsupported type: %s", value.Type())
+ }
+}
+
+func (j *jsonWriter) writeMap(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error {
+ kvs, err := value.MapKeyValues()
+ if err != nil {
+ return err
+ }
+
+ if _, err := w.Write([]byte(`{`)); err != nil {
+ return err
+ }
+
+ if len(kvs) > 0 {
+ if _, err := w.Write([]byte("\n")); err != nil {
+ return err
+ }
+
+ incEs := es.inc()
+ for i, kv := range kvs {
+ if err := incEs.writeIndent(w); err != nil {
+ return err
+ }
+
+ if _, err := w.Write([]byte(fmt.Sprintf(`"%s": `, kv.Key))); err != nil {
+ return err
+ }
+
+ if err := j.write(w, encoder, incEs, kv.Value); err != nil {
+ return err
+ }
+
+ if i < len(kvs)-1 {
+ if _, err := w.Write([]byte(`,`)); err != nil {
+ return err
+ }
+ }
+
+ if _, err := w.Write([]byte("\n")); err != nil {
+ return err
+ }
+ }
+ if err := es.writeIndent(w); err != nil {
+ return err
+ }
+ }
+
+ if _, err := w.Write([]byte(`}`)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (j *jsonWriter) writeSlice(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error {
+ if _, err := w.Write([]byte(`[`)); err != nil {
+ return err
+ }
+
+ length, err := value.SliceLen()
+ if err != nil {
+ return fmt.Errorf("error getting slice length: %w", err)
+ }
+
+ if length > 0 {
+ if _, err := w.Write([]byte("\n")); err != nil {
+ return err
+ }
+ incEs := es.inc()
+ for i := 0; i < length; i++ {
+ if err := incEs.writeIndent(w); err != nil {
+ return err
+ }
+ va, err := value.GetSliceIndex(i)
+ if err != nil {
+ return fmt.Errorf("error getting slice index %d: %w", i, err)
+ }
+ if err := j.write(w, encoder, incEs, va); err != nil {
+ return err
+ }
+ if i < length-1 {
+ if _, err := w.Write([]byte(`,`)); err != nil {
+ return err
+ }
+ }
+ if _, err := w.Write([]byte("\n")); err != nil {
+ return err
+ }
+ }
+ if err := es.writeIndent(w); err != nil {
+ return err
+ }
+ }
+
+ if _, err := w.Write([]byte(`]`)); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/parsing/json/json_test.go b/parsing/json/json_test.go
new file mode 100644
index 00000000..8c104248
--- /dev/null
+++ b/parsing/json/json_test.go
@@ -0,0 +1,50 @@
+package json_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/json"
+)
+
+func TestJson(t *testing.T) {
+ doc := []byte(`{
+ "string": "foo",
+ "int": 1,
+ "float": 1.1,
+ "bool": true,
+ "null": null,
+ "array": [
+ 1,
+ 2,
+ 3
+ ],
+ "object": {
+ "key": "value"
+ }
+}
+`)
+ reader, err := json.JSON.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatal(err)
+ }
+ writer, err := json.JSON.NewWriter(parsing.DefaultWriterOptions())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ value, err := reader.Read(doc)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ newDoc, err := writer.Write(value)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(doc) != string(newDoc) {
+ t.Fatalf("expected %s, got %s...\n%s", string(doc), string(newDoc), cmp.Diff(string(doc), string(newDoc)))
+ }
+}
diff --git a/parsing/reader.go b/parsing/reader.go
new file mode 100644
index 00000000..c8e49027
--- /dev/null
+++ b/parsing/reader.go
@@ -0,0 +1,30 @@
+package parsing
+
+import "github.com/tomwright/dasel/v3/model"
+
+var readers = map[Format]NewReaderFn{}
+
+type ReaderOptions struct {
+ Ext map[string]string
+}
+
+// DefaultReaderOptions returns the default reader options.
+func DefaultReaderOptions() ReaderOptions {
+ return ReaderOptions{
+ Ext: make(map[string]string),
+ }
+}
+
+// Reader reads a value from a byte slice.
+type Reader interface {
+ // Read reads a value from a byte slice.
+ Read([]byte) (*model.Value, error)
+}
+
+// NewReaderFn is a function that creates a new reader.
+type NewReaderFn func(options ReaderOptions) (Reader, error)
+
+// RegisterReader registers a new reader for the format.
+func RegisterReader(format Format, fn NewReaderFn) {
+ readers[format] = fn
+}
diff --git a/parsing/toml/toml.go b/parsing/toml/toml.go
new file mode 100644
index 00000000..feef8457
--- /dev/null
+++ b/parsing/toml/toml.go
@@ -0,0 +1,50 @@
+package toml
+
+import (
+ "github.com/pelletier/go-toml/v2"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+// TODO : Implement using https://github.com/pelletier/go-toml/blob/v2/unstable/ast.go
+
+// TOML represents the TOML file format.
+const TOML parsing.Format = "toml"
+
+var _ parsing.Reader = (*tomlReader)(nil)
+var _ parsing.Writer = (*tomlWriter)(nil)
+
+func init() {
+ parsing.RegisterReader(TOML, newTOMLReader)
+ parsing.RegisterWriter(TOML, newTOMLWriter)
+}
+
+func newTOMLReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &tomlReader{}, nil
+}
+
+func newTOMLWriter(options parsing.WriterOptions) (parsing.Writer, error) {
+ return &tomlWriter{}, nil
+}
+
+type tomlReader struct{}
+
+// Read reads a value from a byte slice.
+func (j *tomlReader) Read(data []byte) (*model.Value, error) {
+ var unmarshalled any
+ if err := toml.Unmarshal(data, &unmarshalled); err != nil {
+ return nil, err
+ }
+ return model.NewValue(&unmarshalled), nil
+}
+
+type tomlWriter struct{}
+
+// Write writes a value to a byte slice.
+func (j *tomlWriter) Write(value *model.Value) ([]byte, error) {
+ res, err := toml.Marshal(value.Interface())
+ if err != nil {
+ return nil, err
+ }
+ return append(res, []byte("\n")...), nil
+}
diff --git a/parsing/writer.go b/parsing/writer.go
new file mode 100644
index 00000000..27165b66
--- /dev/null
+++ b/parsing/writer.go
@@ -0,0 +1,34 @@
+package parsing
+
+import "github.com/tomwright/dasel/v3/model"
+
+var writers = map[Format]NewWriterFn{}
+
+type WriterOptions struct {
+ Compact bool
+ Indent string
+ Ext map[string]string
+}
+
+// DefaultWriterOptions returns the default writer options.
+func DefaultWriterOptions() WriterOptions {
+ return WriterOptions{
+ Compact: false,
+ Indent: " ",
+ Ext: make(map[string]string),
+ }
+}
+
+// Writer writes a value to a byte slice.
+type Writer interface {
+ // Write writes a value to a byte slice.
+ Write(*model.Value) ([]byte, error)
+}
+
+// NewWriterFn is a function that creates a new writer.
+type NewWriterFn func(options WriterOptions) (Writer, error)
+
+// RegisterWriter registers a new writer for the format.
+func RegisterWriter(format Format, fn NewWriterFn) {
+ writers[format] = fn
+}
diff --git a/parsing/xml/reader.go b/parsing/xml/reader.go
new file mode 100644
index 00000000..61fa10ff
--- /dev/null
+++ b/parsing/xml/reader.go
@@ -0,0 +1,183 @@
+package xml
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "unicode"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+func newXMLReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &xmlReader{
+ structured: options.Ext["xml-mode"] == "structured",
+ }, nil
+}
+
+type xmlReader struct {
+ structured bool
+}
+
+// Read reads a value from a byte slice.
+func (j *xmlReader) Read(data []byte) (*model.Value, error) {
+ decoder := xml.NewDecoder(bytes.NewReader(data))
+ decoder.Strict = true
+
+ el, err := j.parseElement(decoder, xml.StartElement{
+ Name: xml.Name{
+ Local: "root",
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if j.structured {
+ return el.toStructuredModel()
+ }
+ return el.toFriendlyModel()
+}
+
+func (e *xmlElement) toStructuredModel() (*model.Value, error) {
+ attrs := model.NewMapValue()
+ for _, attr := range e.Attrs {
+ if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil {
+ return nil, err
+ }
+ }
+ res := model.NewMapValue()
+ if err := res.SetMapKey("name", model.NewStringValue(e.Name)); err != nil {
+ return nil, err
+ }
+ if err := res.SetMapKey("attrs", attrs); err != nil {
+ return nil, err
+ }
+
+ if err := res.SetMapKey("content", model.NewStringValue(e.Content)); err != nil {
+ return nil, err
+ }
+ children := model.NewSliceValue()
+ for _, child := range e.Children {
+ childModel, err := child.toStructuredModel()
+ if err != nil {
+ return nil, err
+ }
+ if err := children.Append(childModel); err != nil {
+ return nil, err
+ }
+ }
+ if err := res.SetMapKey("children", children); err != nil {
+ return nil, err
+ }
+ return res, nil
+}
+
+func (e *xmlElement) toFriendlyModel() (*model.Value, error) {
+ if len(e.Attrs) == 0 && len(e.Children) == 0 {
+ return model.NewStringValue(e.Content), nil
+ }
+
+ res := model.NewMapValue()
+ for _, attr := range e.Attrs {
+ if err := res.SetMapKey("-"+attr.Name, model.NewStringValue(attr.Value)); err != nil {
+ return nil, err
+ }
+ }
+
+ if len(e.Content) > 0 {
+ if err := res.SetMapKey("#text", model.NewStringValue(e.Content)); err != nil {
+ return nil, err
+ }
+ }
+
+ if len(e.Children) > 0 {
+ childElementKeys := make([]string, 0)
+ childElements := make(map[string][]*xmlElement)
+
+ for _, child := range e.Children {
+ if _, ok := childElements[child.Name]; !ok {
+ childElementKeys = append(childElementKeys, child.Name)
+ }
+ childElements[child.Name] = append(childElements[child.Name], child)
+ }
+
+ for _, key := range childElementKeys {
+ cs := childElements[key]
+ switch len(cs) {
+ case 0:
+ continue
+ case 1:
+ childModel, err := cs[0].toFriendlyModel()
+ if err != nil {
+ return nil, err
+ }
+ if err := res.SetMapKey(key, childModel); err != nil {
+ return nil, err
+ }
+ default:
+ children := model.NewSliceValue()
+ for _, child := range cs {
+ childModel, err := child.toFriendlyModel()
+ if err != nil {
+ return nil, err
+ }
+ if err := children.Append(childModel); err != nil {
+ return nil, err
+ }
+ }
+ if err := res.SetMapKey(key, children); err != nil {
+ return nil, err
+ }
+ }
+ }
+ }
+
+ return res, nil
+}
+
+func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) {
+ el := &xmlElement{
+ Name: element.Name.Local,
+ Attrs: make([]xmlAttr, 0),
+ Children: make([]*xmlElement, 0),
+ }
+
+ for _, attr := range element.Attr {
+ el.Attrs = append(el.Attrs, xmlAttr{
+ Name: attr.Name.Local,
+ Value: attr.Value,
+ })
+ }
+
+ for {
+ t, err := decoder.Token()
+ if errors.Is(err, io.EOF) {
+ if el.Name == "root" {
+ return el, nil
+ }
+ return nil, fmt.Errorf("unexpected EOF")
+ }
+
+ switch t := t.(type) {
+ case xml.StartElement:
+ child, err := j.parseElement(decoder, t)
+ if err != nil {
+ return nil, err
+ }
+ el.Children = append(el.Children, child)
+ case xml.CharData:
+ if unicode.IsSpace([]rune(string(t))[0]) {
+ continue
+ }
+ el.Content += string(t)
+ case xml.EndElement:
+ return el, nil
+ default:
+ return nil, fmt.Errorf("unexpected token: %v", t)
+ }
+ }
+}
diff --git a/parsing/xml/writer.go b/parsing/xml/writer.go
new file mode 100644
index 00000000..0a84e394
--- /dev/null
+++ b/parsing/xml/writer.go
@@ -0,0 +1,21 @@
+package xml
+
+import (
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) {
+ return &xmlWriter{
+ options: options,
+ }, nil
+}
+
+type xmlWriter struct {
+ options parsing.WriterOptions
+}
+
+// Write writes a value to a byte slice.
+func (j *xmlWriter) Write(value *model.Value) ([]byte, error) {
+ return nil, nil
+}
diff --git a/parsing/xml/xml.go b/parsing/xml/xml.go
new file mode 100644
index 00000000..897200dc
--- /dev/null
+++ b/parsing/xml/xml.go
@@ -0,0 +1,31 @@
+package xml
+
+import (
+ "github.com/tomwright/dasel/v3/parsing"
+)
+
+const (
+ // XML represents the XML file format.
+ XML parsing.Format = "xml"
+)
+
+var _ parsing.Reader = (*xmlReader)(nil)
+var _ parsing.Writer = (*xmlWriter)(nil)
+
+func init() {
+ parsing.RegisterReader(XML, newXMLReader)
+ // XML writer is not implemented yet
+ //parsing.RegisterWriter(XML, newXMLWriter)
+}
+
+type xmlAttr struct {
+ Name string
+ Value string
+}
+
+type xmlElement struct {
+ Name string
+ Attrs []xmlAttr
+ Children []*xmlElement
+ Content string
+}
diff --git a/parsing/xml/xml_test.go b/parsing/xml/xml_test.go
new file mode 100644
index 00000000..a72629dc
--- /dev/null
+++ b/parsing/xml/xml_test.go
@@ -0,0 +1,78 @@
+package xml_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/xml"
+)
+
+type testCase struct {
+ in string
+ assert func(t *testing.T, res *model.Value)
+}
+
+func (tc testCase) run(t *testing.T) {
+ r, err := xml.XML.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ res, err := r.Read([]byte(tc.in))
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ tc.assert(t, res)
+}
+
+type rwTestCase struct {
+ in string
+ out string
+}
+
+func (tc rwTestCase) run(t *testing.T) {
+ if tc.out == "" {
+ tc.out = tc.in
+ }
+ r, err := xml.XML.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ w, err := xml.XML.NewWriter(parsing.WriterOptions{})
+ res, err := r.Read([]byte(tc.in))
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ out, err := w.Write(res)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ if !bytes.Equal([]byte(tc.out), out) {
+ t.Errorf("unexpected output: %s", cmp.Diff(tc.out, string(out)))
+ }
+}
+
+func TestYamlValue_UnmarshalXML(t *testing.T) {
+ //t.Run("generic", rwTestCase{
+ // in: `
+ //
+ // Test
+ //
+ //
+ // Test
+ // Test
+ //
+ //
+ //
Hello
+ //
World
+ //
+ //
+ //`,
+ // }.run)
+}
diff --git a/parsing/yaml/yaml.go b/parsing/yaml/yaml.go
new file mode 100644
index 00000000..cc9aae4b
--- /dev/null
+++ b/parsing/yaml/yaml.go
@@ -0,0 +1,278 @@
+package yaml
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strconv"
+
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+ "gopkg.in/yaml.v3"
+)
+
+// YAML represents the YAML file format.
+const YAML parsing.Format = "yaml"
+
+var _ parsing.Reader = (*yamlReader)(nil)
+var _ parsing.Writer = (*yamlWriter)(nil)
+
+func init() {
+ parsing.RegisterReader(YAML, newYAMLReader)
+ parsing.RegisterWriter(YAML, newYAMLWriter)
+}
+
+func newYAMLReader(options parsing.ReaderOptions) (parsing.Reader, error) {
+ return &yamlReader{}, nil
+}
+
+func newYAMLWriter(options parsing.WriterOptions) (parsing.Writer, error) {
+ return &yamlWriter{}, nil
+}
+
+type yamlReader struct{}
+
+// Read reads a value from a byte slice.
+func (j *yamlReader) Read(data []byte) (*model.Value, error) {
+ d := yaml.NewDecoder(bytes.NewReader(data))
+ res := make([]*yamlValue, 0)
+ for {
+ unmarshalled := &yamlValue{}
+ if err := d.Decode(&unmarshalled); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, err
+ }
+ res = append(res, unmarshalled)
+ }
+
+ switch len(res) {
+ case 0:
+ return model.NewNullValue(), nil
+ case 1:
+ return res[0].value, nil
+ default:
+ slice := model.NewSliceValue()
+ slice.MarkAsBranch()
+ for _, v := range res {
+ if err := slice.Append(v.value); err != nil {
+ return nil, err
+ }
+ }
+ return slice, nil
+ }
+}
+
+type yamlWriter struct{}
+
+// Write writes a value to a byte slice.
+func (j *yamlWriter) Write(value *model.Value) ([]byte, error) {
+ if value.IsBranch() {
+ res := make([]byte, 0)
+ sliceLen, err := value.SliceLen()
+ if err != nil {
+ return nil, err
+ }
+ if err := value.RangeSlice(func(i int, val *model.Value) error {
+ yv := &yamlValue{value: val}
+ marshalled, err := yaml.Marshal(yv)
+ if err != nil {
+ return err
+ }
+ res = append(res, marshalled...)
+ if i < sliceLen-1 {
+ res = append(res, []byte("---\n")...)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return res, nil
+ }
+
+ yv := &yamlValue{value: value}
+ res, err := yv.ToNode()
+ if err != nil {
+ return nil, err
+ }
+ return yaml.Marshal(res)
+}
+
+type yamlValue struct {
+ node *yaml.Node
+ value *model.Value
+}
+
+func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error {
+ yv.node = value
+ switch value.Kind {
+ case yaml.ScalarNode:
+ switch value.Tag {
+ case "!!bool":
+ yv.value = model.NewBoolValue(value.Value == "true")
+ case "!!int":
+ i, err := strconv.Atoi(value.Value)
+ if err != nil {
+ return err
+ }
+ yv.value = model.NewIntValue(int64(i))
+ case "!!float":
+ f, err := strconv.ParseFloat(value.Value, 64)
+ if err != nil {
+ return err
+ }
+ yv.value = model.NewFloatValue(f)
+ default:
+ yv.value = model.NewStringValue(value.Value)
+ }
+ case yaml.DocumentNode:
+ yv.value = model.NewNullValue()
+ case yaml.SequenceNode:
+ res := model.NewSliceValue()
+ for _, item := range value.Content {
+ newItem := &yamlValue{}
+ if err := newItem.UnmarshalYAML(item); err != nil {
+ return err
+ }
+ if err := res.Append(newItem.value); err != nil {
+ return err
+ }
+ }
+ yv.value = res
+ case yaml.MappingNode:
+ res := model.NewMapValue()
+ for i := 0; i < len(value.Content); i += 2 {
+ key := value.Content[i]
+ val := value.Content[i+1]
+
+ newKey := &yamlValue{}
+ if err := newKey.UnmarshalYAML(key); err != nil {
+ return err
+ }
+
+ newVal := &yamlValue{}
+ if err := newVal.UnmarshalYAML(val); err != nil {
+ return err
+ }
+
+ keyStr, err := newKey.value.StringValue()
+ if err != nil {
+ return fmt.Errorf("keys are expected to be strings: %w", err)
+ }
+
+ if err := res.SetMapKey(keyStr, newVal.value); err != nil {
+ return err
+ }
+ }
+ yv.value = res
+ case yaml.AliasNode:
+ newVal := &yamlValue{}
+ if err := newVal.UnmarshalYAML(value.Alias); err != nil {
+ return err
+ }
+ yv.value = newVal.value
+ yv.value.Metadata["yaml-alias"] = value.Value
+ }
+ return nil
+}
+
+func (yv *yamlValue) ToNode() (*yaml.Node, error) {
+ res := &yaml.Node{}
+
+ yamlAlias, ok := yv.value.Metadata["yaml-alias"].(string)
+ if ok {
+ //res.Kind = yaml.ScalarNode
+ res.Kind = yaml.AliasNode
+ res.Value = yamlAlias
+ //res.Alias = &yaml.Node{
+ // Kind: yaml.ScalarNode,
+ // Value: yamlAlias,
+ //}
+ return res, nil
+ }
+
+ switch yv.value.Type() {
+ case model.TypeString:
+ v, err := yv.value.StringValue()
+ if err != nil {
+ return nil, err
+ }
+ res.Kind = yaml.ScalarNode
+ res.Value = v
+ res.Tag = "!!str"
+ case model.TypeBool:
+ v, err := yv.value.BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ res.Kind = yaml.ScalarNode
+ res.Value = fmt.Sprintf("%t", v)
+ res.Tag = "!!bool"
+ case model.TypeInt:
+ v, err := yv.value.IntValue()
+ if err != nil {
+ return nil, err
+ }
+ res.Kind = yaml.ScalarNode
+ res.Value = fmt.Sprintf("%d", v)
+ res.Tag = "!!int"
+ case model.TypeFloat:
+ v, err := yv.value.FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ res.Kind = yaml.ScalarNode
+ res.Value = fmt.Sprintf("%g", v)
+ res.Tag = "!!float"
+ case model.TypeMap:
+ res.Kind = yaml.MappingNode
+ if err := yv.value.RangeMap(func(key string, val *model.Value) error {
+ keyNode := &yamlValue{value: model.NewStringValue(key)}
+ valNode := &yamlValue{value: val}
+
+ marshalledKey, err := keyNode.ToNode()
+ if err != nil {
+ return err
+ }
+ marshalledVal, err := valNode.ToNode()
+ if err != nil {
+ return err
+ }
+
+ res.Content = append(res.Content, marshalledKey)
+ res.Content = append(res.Content, marshalledVal)
+
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ case model.TypeSlice:
+ res.Kind = yaml.SequenceNode
+ if err := yv.value.RangeSlice(func(i int, val *model.Value) error {
+ valNode := &yamlValue{value: val}
+ marshalledVal, err := valNode.ToNode()
+ if err != nil {
+ return err
+ }
+ res.Content = append(res.Content, marshalledVal)
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ case model.TypeNull:
+ res.Kind = yaml.DocumentNode
+ case model.TypeUnknown:
+ return nil, fmt.Errorf("unknown type: %s", yv.value.Type())
+ }
+
+ return res, nil
+}
+
+func (yv *yamlValue) MarshalYAML() (any, error) {
+ res, err := yv.ToNode()
+ if err != nil {
+ return nil, err
+ }
+ return res, nil
+}
diff --git a/parsing/yaml/yaml_test.go b/parsing/yaml/yaml_test.go
new file mode 100644
index 00000000..12c4da45
--- /dev/null
+++ b/parsing/yaml/yaml_test.go
@@ -0,0 +1,186 @@
+package yaml_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3/model"
+ "github.com/tomwright/dasel/v3/parsing"
+ "github.com/tomwright/dasel/v3/parsing/yaml"
+)
+
+type testCase struct {
+ in string
+ assert func(t *testing.T, res *model.Value)
+}
+
+func (tc testCase) run(t *testing.T) {
+ r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ res, err := r.Read([]byte(tc.in))
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ tc.assert(t, res)
+}
+
+type rwTestCase struct {
+ in string
+ out string
+}
+
+func (tc rwTestCase) run(t *testing.T) {
+ if tc.out == "" {
+ tc.out = tc.in
+ }
+ r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions())
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ w, err := yaml.YAML.NewWriter(parsing.WriterOptions{})
+ res, err := r.Read([]byte(tc.in))
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ out, err := w.Write(res)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ if !bytes.Equal([]byte(tc.out), out) {
+ t.Errorf("unexpected output: %s", cmp.Diff(tc.out, string(out)))
+ }
+}
+
+func TestYamlValue_UnmarshalYAML(t *testing.T) {
+ t.Run("simple key value", testCase{
+ in: `name: Tom`,
+ assert: func(t *testing.T, res *model.Value) {
+ got, err := res.GetMapKey("name")
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ gotStr, err := got.StringValue()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ if gotStr != "Tom" {
+ t.Errorf("unexpected value: %s", gotStr)
+ }
+ },
+ }.run)
+
+ t.Run("multi document", testCase{
+ in: `name: Tom
+---
+name: Jerry`,
+ assert: func(t *testing.T, res *model.Value) {
+ a, err := res.GetSliceIndex(0)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ got, err := a.GetMapKey("name")
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ gotStr, err := got.StringValue()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if gotStr != "Tom" {
+ t.Errorf("unexpected value: %s", gotStr)
+ }
+
+ b, err := res.GetSliceIndex(1)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ got, err = b.GetMapKey("name")
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ gotStr, err = got.StringValue()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if gotStr != "Jerry" {
+ t.Errorf("unexpected value: %s", gotStr)
+ }
+ },
+ }.run)
+
+ t.Run("multi document", testCase{
+ in: `name: Tom
+---
+name: Jerry`,
+ assert: func(t *testing.T, res *model.Value) {
+ a, err := res.GetSliceIndex(0)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ got, err := a.GetMapKey("name")
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ gotStr, err := got.StringValue()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if gotStr != "Tom" {
+ t.Errorf("unexpected value: %s", gotStr)
+ }
+
+ b, err := res.GetSliceIndex(1)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ got, err = b.GetMapKey("name")
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ gotStr, err = got.StringValue()
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if gotStr != "Jerry" {
+ t.Errorf("unexpected value: %s", gotStr)
+ }
+ },
+ }.run)
+
+ t.Run("multi document", rwTestCase{
+ in: `name: Tom
+---
+name: Jerry
+`,
+ }.run)
+
+ t.Run("generic", rwTestCase{
+ in: `str: foo
+int: 1
+float: 1.1
+bool: true
+map:
+ key: value
+list:
+ - item1
+ - item2
+`,
+ }.run)
+
+ // This test is technically wrong because we're only supporting the alias on read and not write.
+ t.Run("alias", rwTestCase{
+ in: `name: &name Tom
+name2: *name
+`,
+ out: `name: Tom
+name2: Tom
+`,
+ }.run)
+}
diff --git a/selector.go b/selector.go
deleted file mode 100644
index 95520c5e..00000000
--- a/selector.go
+++ /dev/null
@@ -1,224 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "io"
- "strings"
-)
-
-type ErrBadSelectorSyntax struct {
- Part string
- Message string
-}
-
-func (e ErrBadSelectorSyntax) Error() string {
- return fmt.Sprintf("bad syntax: %s, around %s", e.Message, e.Part)
-}
-
-func (e ErrBadSelectorSyntax) Is(other error) bool {
- o, ok := other.(*ErrBadSelectorSyntax)
- if !ok {
- return false
- }
- if o.Part != "" && o.Part != e.Part {
- return false
- }
- if o.Message != "" && o.Message != e.Message {
- return false
- }
- return true
-}
-
-type Selector struct {
- funcName string
- funcArgs []string
-}
-
-type SelectorResolver interface {
- Original() string
- Next() (*Selector, error)
-}
-
-func NewSelectorResolver(selector string, functions *FunctionCollection) SelectorResolver {
- return &standardSelectorResolver{
- functions: functions,
- original: selector,
- reader: strings.NewReader(selector),
- separator: '.',
- openFunc: '(',
- closeFunc: ')',
- argSeparator: ',',
- escapeChar: '\\',
- }
-}
-
-type standardSelectorResolver struct {
- functions *FunctionCollection
- original string
- reader *strings.Reader
- separator rune
- openFunc rune
- closeFunc rune
- argSeparator rune
- escapeChar rune
-}
-
-func (r *standardSelectorResolver) Original() string {
- return r.original
-}
-
-// nextPart returns the next part.
-// It returns true if there are more parts to the selector, or false if we reached the end.
-func (r *standardSelectorResolver) nextPart() (string, bool) {
- b := &strings.Builder{}
- bracketDepth := 0
- escaped := false
- for {
- readRune, _, err := r.reader.ReadRune()
- if err == io.EOF {
- return b.String(), false
- }
- if escaped {
- b.WriteRune(readRune)
- escaped = false
- continue
- } else if readRune == r.escapeChar {
- b.WriteRune(readRune)
- escaped = true
- continue
- } else if readRune == r.openFunc {
- bracketDepth++
- } else if readRune == r.closeFunc {
- bracketDepth--
- }
- if readRune == r.separator && bracketDepth == 0 {
- return b.String(), true
- }
- b.WriteRune(readRune)
- }
-}
-
-func (r *standardSelectorResolver) Next() (*Selector, error) {
- nextPart, moreParts := r.nextPart()
- if nextPart == "" && !moreParts {
- return nil, nil
- }
- if nextPart == "" && moreParts {
- return &Selector{
- funcName: "this",
- funcArgs: []string{},
- }, nil
- }
-
- if r.functions != nil {
- if s := r.functions.ParseSelector(nextPart); s != nil {
- return s, nil
- }
- }
-
- var hasOpenedFunc, hasClosedFunc = false, false
- bracketDepth := 0
-
- var funcNameBuilder = &strings.Builder{}
- var argBuilder = &strings.Builder{}
-
- nextPartReader := strings.NewReader(nextPart)
-
- funcName := ""
- args := make([]string, 0)
-
- escaped := false
- for {
- nextRune, _, err := nextPartReader.ReadRune()
- if err == io.EOF {
- if funcNameBuilder.Len() > 0 {
- funcName = funcNameBuilder.String()
- }
- break
- }
- if err != nil {
- return nil, fmt.Errorf("could not read selector: %w", err)
- }
-
- switch {
- case nextRune == r.escapeChar && !escaped:
- escaped = true
- continue
-
- case nextRune == r.openFunc && !escaped:
- if !hasOpenedFunc {
- hasOpenedFunc = true
- funcName = funcNameBuilder.String()
- if funcName == "" {
- return nil, &ErrBadSelectorSyntax{
- Part: nextPart,
- Message: "function name required before open bracket",
- }
- }
- } else {
- argBuilder.WriteRune(nextRune)
- }
- bracketDepth++
-
- case nextRune == r.closeFunc && !escaped:
- if bracketDepth > 1 {
- argBuilder.WriteRune(nextRune)
- } else if bracketDepth == 1 {
- hasClosedFunc = true
- arg := argBuilder.String()
- if arg != "" {
- args = append(args, argBuilder.String())
- }
- } else if bracketDepth < 1 {
- return nil, &ErrBadSelectorSyntax{
- Part: nextPart,
- Message: "too many closing brackets",
- }
- }
- bracketDepth--
-
- case hasOpenedFunc && nextRune == r.argSeparator && !escaped:
- if bracketDepth > 1 {
- argBuilder.WriteRune(nextRune)
- } else if bracketDepth == 1 {
- arg := argBuilder.String()
- argBuilder.Reset()
- if arg != "" {
- args = append(args, arg)
- }
- }
-
- case hasOpenedFunc:
- if escaped {
- escaped = false
- }
- argBuilder.WriteRune(nextRune)
-
- case hasClosedFunc:
- // Do not allow anything after the closeFunc
- return nil, &ErrBadSelectorSyntax{
- Part: nextPart,
- Message: "selector function must end after closing bracket",
- }
-
- default:
- if escaped {
- escaped = false
- }
- funcNameBuilder.WriteRune(nextRune)
- }
- }
-
- if !hasOpenedFunc {
- return &Selector{
- funcName: "property",
- funcArgs: []string{funcName},
- }, nil
- }
-
- return &Selector{
- funcName: funcName,
- funcArgs: args,
- }, nil
-
-}
diff --git a/selector/README.md b/selector/README.md
new file mode 100644
index 00000000..4821462e
--- /dev/null
+++ b/selector/README.md
@@ -0,0 +1,3 @@
+# Selector
+
+The selector package contains everything needed to parse a selector string into an AST, which we can then execute.
diff --git a/selector/ast/ast.go b/selector/ast/ast.go
new file mode 100644
index 00000000..b5a97d48
--- /dev/null
+++ b/selector/ast/ast.go
@@ -0,0 +1,36 @@
+package ast
+
+type Expressions []Expr
+
+type Expr interface {
+ expr()
+}
+
+func IsType[T Expr](e Expr) bool {
+ _, ok := AsType[T](e)
+ return ok
+}
+
+func AsType[T Expr](e Expr) (T, bool) {
+ v, ok := e.(T)
+ return v, ok
+}
+
+func LastAsType[T Expr](e Expr) (T, bool) {
+ return AsType[T](Last(e))
+}
+
+func Last(e Expr) Expr {
+ if v, ok := e.(ChainedExpr); ok {
+ return v.Exprs[len(v.Exprs)-1]
+ }
+ return e
+}
+
+func RemoveLast(e Expr) Expr {
+ var res Expressions
+ if v, ok := e.(ChainedExpr); ok {
+ res = v.Exprs[0 : len(v.Exprs)-1]
+ }
+ return ChainExprs(res...)
+}
diff --git a/selector/ast/ast_test.go b/selector/ast/ast_test.go
new file mode 100644
index 00000000..4744a2e9
--- /dev/null
+++ b/selector/ast/ast_test.go
@@ -0,0 +1,28 @@
+package ast
+
+import "testing"
+
+// TestExpr_expr tests the expr method of all the types in the ast package.
+// Note that this doesn't actually do anything and is just forcing test coverage.
+// The expr func only exists for type safety with the Expr interface.
+func TestExpr_expr(t *testing.T) {
+ NumberFloatExpr{}.expr()
+ NumberIntExpr{}.expr()
+ StringExpr{}.expr()
+ BoolExpr{}.expr()
+ BinaryExpr{}.expr()
+ UnaryExpr{}.expr()
+ CallExpr{}.expr()
+ ChainedExpr{}.expr()
+ SpreadExpr{}.expr()
+ RangeExpr{}.expr()
+ IndexExpr{}.expr()
+ ArrayExpr{}.expr()
+ PropertyExpr{}.expr()
+ ObjectExpr{}.expr()
+ MapExpr{}.expr()
+ VariableExpr{}.expr()
+ GroupExpr{}.expr()
+ ConditionalExpr{}.expr()
+ BranchExpr{}.expr()
+}
diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go
new file mode 100644
index 00000000..3715633a
--- /dev/null
+++ b/selector/ast/expression_complex.go
@@ -0,0 +1,137 @@
+package ast
+
+import "github.com/tomwright/dasel/v3/selector/lexer"
+
+type BinaryExpr struct {
+ Left Expr
+ Operator lexer.Token
+ Right Expr
+}
+
+func (BinaryExpr) expr() {}
+
+type UnaryExpr struct {
+ Operator lexer.Token
+ Right Expr
+}
+
+func (UnaryExpr) expr() {}
+
+type CallExpr struct {
+ Function string
+ Args Expressions
+}
+
+func (CallExpr) expr() {}
+
+type ChainedExpr struct {
+ Exprs Expressions
+}
+
+func ChainExprs(exprs ...Expr) Expr {
+ if len(exprs) == 0 {
+ return nil
+ }
+ if len(exprs) == 1 {
+ return exprs[0]
+ }
+ return ChainedExpr{
+ Exprs: exprs,
+ }
+}
+
+func (ChainedExpr) expr() {}
+
+type SpreadExpr struct{}
+
+func (SpreadExpr) expr() {}
+
+type RangeExpr struct {
+ Start Expr
+ End Expr
+}
+
+func (RangeExpr) expr() {}
+
+type IndexExpr struct {
+ Index Expr
+}
+
+func (IndexExpr) expr() {}
+
+type ArrayExpr struct {
+ Exprs Expressions
+}
+
+func (ArrayExpr) expr() {}
+
+type PropertyExpr struct {
+ // Property can resolve to a string or number.
+ // If it resolves to a number, we expect to be reading from an array.
+ // If it resolves to a string, we expect to be reading from a map.
+ Property Expr
+}
+
+func (PropertyExpr) expr() {}
+
+type KeyValue struct {
+ Key Expr
+ Value Expr
+}
+
+type ObjectExpr struct {
+ Pairs []KeyValue
+}
+
+func (ObjectExpr) expr() {}
+
+type MapExpr struct {
+ Expr Expr
+}
+
+func (MapExpr) expr() {}
+
+type FilterExpr struct {
+ Expr Expr
+}
+
+func (FilterExpr) expr() {}
+
+type SortByExpr struct {
+ Expr Expr
+ Descending bool
+}
+
+func (SortByExpr) expr() {}
+
+type VariableExpr struct {
+ Name string
+}
+
+func (VariableExpr) expr() {}
+
+type GroupExpr struct {
+ Expr Expr
+}
+
+func (GroupExpr) expr() {}
+
+type ConditionalExpr struct {
+ Cond Expr
+ Then Expr
+ Else Expr
+}
+
+func (ConditionalExpr) expr() {}
+
+type BranchExpr struct {
+ Exprs []Expr
+}
+
+func (BranchExpr) expr() {}
+
+func BranchExprs(exprs ...Expr) Expr {
+ return BranchExpr{
+ Exprs: exprs,
+ }
+}
diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go
new file mode 100644
index 00000000..128d5d9a
--- /dev/null
+++ b/selector/ast/expression_literal.go
@@ -0,0 +1,37 @@
+package ast
+
+import "regexp"
+
+type NumberFloatExpr struct {
+ Value float64
+}
+
+func (NumberFloatExpr) expr() {}
+
+type NumberIntExpr struct {
+ Value int64
+}
+
+func (NumberIntExpr) expr() {}
+
+type StringExpr struct {
+ Value string
+}
+
+func (StringExpr) expr() {}
+
+type BoolExpr struct {
+ Value bool
+}
+
+func (BoolExpr) expr() {}
+
+type RegexExpr struct {
+ Regex *regexp.Regexp
+}
+
+func (RegexExpr) expr() {}
+
+type NullExpr struct{}
+
+func (NullExpr) expr() {}
diff --git a/selector/lexer/token.go b/selector/lexer/token.go
new file mode 100644
index 00000000..5c7e116c
--- /dev/null
+++ b/selector/lexer/token.go
@@ -0,0 +1,124 @@
+package lexer
+
+import (
+ "fmt"
+ "slices"
+)
+
+type TokenKind int
+
+func TokenKinds(tk ...TokenKind) []TokenKind {
+ return tk
+}
+
+const (
+ EOF TokenKind = iota
+ Symbol
+ Comma
+ Colon
+ OpenBracket // [
+ CloseBracket // ]
+ OpenCurly
+ CloseCurly
+ OpenParen
+ CloseParen
+ Equal // ==
+ Equals // =
+ NotEqual // !=
+ And
+ Or
+ Like
+ NotLike
+ String
+ Number
+ Bool
+ Plus
+ Increment
+ IncrementBy
+ Dash
+ Decrement
+ DecrementBy
+ Star
+ Slash
+ Percent
+ Dot
+ Spread
+ Dollar
+ Variable
+ GreaterThan
+ GreaterThanOrEqual
+ LessThan
+ LessThanOrEqual
+ Exclamation
+ Null
+ If
+ Else
+ ElseIf
+ Branch
+ Map
+ Filter
+ RegexPattern
+ SortBy
+ Asc
+ Desc
+ QuestionMark
+ DoubleQuestionMark
+)
+
+type Tokens []Token
+
+func (tt Tokens) Split(kind TokenKind) []Tokens {
+ var res []Tokens
+ var cur Tokens
+ for _, t := range tt {
+ if t.Kind == kind {
+ if len(cur) > 0 {
+ res = append(res, cur)
+ }
+ cur = nil
+ continue
+ }
+ cur = append(cur, t)
+ }
+ if len(cur) > 0 {
+ res = append(res, cur)
+ }
+ return res
+}
+
+type Token struct {
+ Kind TokenKind
+ Value string
+ Pos int
+ Len int
+}
+
+func NewToken(kind TokenKind, value string, pos int, len int) Token {
+ return Token{
+ Kind: kind,
+ Value: value,
+ Pos: pos,
+ Len: len,
+ }
+}
+
+func (t Token) IsKind(kind ...TokenKind) bool {
+ return slices.Contains(kind, t.Kind)
+}
+
+type UnexpectedTokenError struct {
+ Pos int
+ Token rune
+}
+
+func (e *UnexpectedTokenError) Error() string {
+ return fmt.Sprintf("failed to tokenize: unexpected token: %s at position %d.", string(e.Token), e.Pos)
+}
+
+type UnexpectedEOFError struct {
+ Pos int
+}
+
+func (e *UnexpectedEOFError) Error() string {
+ return fmt.Sprintf("failed to tokenize: unexpected EOF at position %d.", e.Pos)
+}
diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go
new file mode 100644
index 00000000..4aebae15
--- /dev/null
+++ b/selector/lexer/tokenize.go
@@ -0,0 +1,309 @@
+package lexer
+
+import (
+ "strings"
+ "unicode"
+
+ "github.com/tomwright/dasel/v3/internal/ptr"
+)
+
+type Tokenizer struct {
+ i int
+ src string
+ srcLen int
+}
+
+func NewTokenizer(src string) *Tokenizer {
+ return &Tokenizer{
+ i: 0,
+ src: src,
+ srcLen: len([]rune(src)),
+ }
+}
+
+func (p *Tokenizer) Tokenize() (Tokens, error) {
+ var tokens Tokens
+ for {
+ tok, err := p.Next()
+ if err != nil {
+ return nil, err
+ }
+ if tok.Kind == EOF {
+ break
+ }
+ tokens = append(tokens, tok)
+ }
+ return tokens, nil
+}
+
+func (p *Tokenizer) peekRuneEqual(i int, to rune) bool {
+ if i >= p.srcLen {
+ return false
+ }
+ return rune(p.src[i]) == to
+}
+
+func (p *Tokenizer) peekRuneMatches(i int, fn func(rune) bool) bool {
+ if i >= p.srcLen {
+ return false
+ }
+ return fn(rune(p.src[i]))
+}
+
+func (p *Tokenizer) parseCurRune() (Token, error) {
+ // Skip over whitespace
+ for p.i < p.srcLen && unicode.IsSpace(rune(p.src[p.i])) {
+ p.i++
+ }
+
+ switch p.src[p.i] {
+ case '.':
+ if p.peekRuneEqual(p.i+1, '.') && p.peekRuneEqual(p.i+2, '.') {
+ return NewToken(Spread, "...", p.i, 3), nil
+ }
+ return NewToken(Dot, ".", p.i, 1), nil
+ case ',':
+ return NewToken(Comma, ",", p.i, 1), nil
+ case ':':
+ return NewToken(Colon, ":", p.i, 1), nil
+ case '[':
+ return NewToken(OpenBracket, "[", p.i, 1), nil
+ case ']':
+ return NewToken(CloseBracket, "]", p.i, 1), nil
+ case '(':
+ return NewToken(OpenParen, "(", p.i, 1), nil
+ case ')':
+ return NewToken(CloseParen, ")", p.i, 1), nil
+ case '{':
+ return NewToken(OpenCurly, "{", p.i, 1), nil
+ case '}':
+ return NewToken(CloseCurly, "}", p.i, 1), nil
+ case '*':
+ return NewToken(Star, "*", p.i, 1), nil
+ case '/':
+ return NewToken(Slash, "/", p.i, 1), nil
+ case '%':
+ return NewToken(Percent, "%", p.i, 1), nil
+ case '$':
+ if p.peekRuneMatches(p.i+1, unicode.IsLetter) {
+ pos := p.i + 1
+ for pos < p.srcLen && (unicode.IsLetter(rune(p.src[pos])) || unicode.IsDigit(rune(p.src[pos]))) {
+ pos++
+ }
+ return NewToken(Variable, p.src[p.i+1:pos], p.i, pos-p.i), nil
+ }
+ return NewToken(Dollar, "$", p.i, 1), nil
+ case '=':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(Equal, "==", p.i, 2), nil
+ }
+ if p.peekRuneEqual(p.i+1, '~') {
+ return NewToken(Like, "=~", p.i, 2), nil
+ }
+ return NewToken(Equals, "=", p.i, 1), nil
+ case '+':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(IncrementBy, "+=", p.i, 2), nil
+ }
+ if p.peekRuneEqual(p.i+1, '+') {
+ return NewToken(Increment, "++", p.i, 2), nil
+ }
+ return NewToken(Plus, "+", p.i, 1), nil
+ case '-':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(DecrementBy, "-=", p.i, 2), nil
+ }
+ if p.peekRuneEqual(p.i+1, '-') {
+ return NewToken(Decrement, "--", p.i, 2), nil
+ }
+ return NewToken(Dash, "-", p.i, 1), nil
+ case '>':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(GreaterThanOrEqual, ">=", p.i, 2), nil
+ }
+ return NewToken(GreaterThan, ">", p.i, 1), nil
+ case '<':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(LessThanOrEqual, "<>>=", p.i, 2), nil
+ }
+ return NewToken(LessThan, "<", p.i, 1), nil
+ case '!':
+ if p.peekRuneEqual(p.i+1, '=') {
+ return NewToken(NotEqual, "!=", p.i, 2), nil
+ }
+ if p.peekRuneEqual(p.i+1, '~') {
+ return NewToken(NotLike, "!~", p.i, 2), nil
+ }
+ return NewToken(Exclamation, "!", p.i, 1), nil
+ case '&':
+ if p.peekRuneEqual(p.i+1, '&') {
+ return NewToken(And, "&&", p.i, 2), nil
+ }
+ return Token{}, &UnexpectedTokenError{
+ Pos: p.i,
+ Token: rune(p.src[p.i]),
+ }
+ case '|':
+ if p.peekRuneEqual(p.i+1, '|') {
+ return NewToken(Or, "||", p.i, 2), nil
+ }
+ return Token{}, &UnexpectedTokenError{
+ Pos: p.i,
+ Token: rune(p.src[p.i]),
+ }
+ case '?':
+ if p.peekRuneEqual(p.i+1, '?') {
+ return NewToken(DoubleQuestionMark, "??", p.i, 2), nil
+ }
+ return NewToken(QuestionMark, "?", p.i, 1), nil
+ case '"', '\'':
+ pos := p.i
+ buf := make([]rune, 0)
+ pos++
+ foundCloseRune := false
+ for pos < p.srcLen {
+ if p.src[pos] == p.src[p.i] {
+ foundCloseRune = true
+ break
+ }
+ if p.src[pos] == '\\' {
+ pos++
+ buf = append(buf, rune(p.src[pos]))
+ pos++
+ continue
+ }
+ buf = append(buf, rune(p.src[pos]))
+ pos++
+ }
+ if !foundCloseRune {
+ // We didn't find a closing quote.
+ if pos < p.srcLen {
+ // This shouldn't be possible.
+ return Token{}, &UnexpectedTokenError{
+ Pos: p.i,
+ Token: rune(p.src[pos]),
+ }
+ }
+ // This can happen if the selector ends before the closing quote.
+ return Token{}, &UnexpectedEOFError{
+ Pos: pos,
+ }
+ }
+ res := NewToken(String, string(buf), p.i, pos+1-p.i)
+ return res, nil
+ default:
+ pos := p.i
+
+ matchStr := func(pos int, m string, caseInsensitive bool, kind TokenKind) *Token {
+ l := len(m)
+ if pos+(l-1) >= p.srcLen {
+ return nil
+ }
+ other := p.src[pos : pos+l]
+ if m != other && !(caseInsensitive && strings.EqualFold(m, other)) {
+ return nil
+ }
+
+ if pos+(l) < p.srcLen && (unicode.IsLetter(rune(p.src[pos+l])) || unicode.IsDigit(rune(p.src[pos+l]))) {
+ // There is a follow letter or digit.
+ return nil
+ }
+
+ return ptr.To(NewToken(kind, other, pos, l))
+ }
+
+ matchRegexPattern := func(pos int) *Token {
+ if !(p.src[pos] == 'r' && p.peekRuneEqual(pos+1, '/')) {
+ return nil
+ }
+ start := pos
+ pos += 2
+ for !p.peekRuneEqual(pos, '/') {
+ pos++
+ }
+ pos++
+ return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start))
+ }
+
+ if t := matchStr(pos, "null", true, Null); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "true", true, Bool); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "false", true, Bool); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "elseif", false, ElseIf); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "if", false, If); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "else", false, Else); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "branch", false, Branch); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "map", false, Map); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "filter", false, Filter); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "sortBy", false, SortBy); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "asc", false, Asc); t != nil {
+ return *t, nil
+ }
+ if t := matchStr(pos, "desc", false, Desc); t != nil {
+ return *t, nil
+ }
+
+ if t := matchRegexPattern(pos); t != nil {
+ return *t, nil
+ }
+
+ if unicode.IsDigit(rune(p.src[pos])) {
+ // Handle whole numbers
+ for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) {
+ pos++
+ }
+ // Handle floats
+ if pos < p.srcLen && p.src[pos] == '.' && pos+1 < p.srcLen && unicode.IsDigit(rune(p.src[pos+1])) {
+ pos++
+ for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) {
+ pos++
+ }
+ }
+ return NewToken(Number, p.src[p.i:pos], p.i, pos-p.i), nil
+ }
+
+ if unicode.IsLetter(rune(p.src[pos])) {
+ for pos < p.srcLen && (unicode.IsLetter(rune(p.src[pos])) || unicode.IsDigit(rune(p.src[pos]))) {
+ pos++
+ }
+ return NewToken(Symbol, p.src[p.i:pos], p.i, pos-p.i), nil
+ }
+
+ return Token{}, &UnexpectedTokenError{
+ Pos: p.i,
+ Token: rune(p.src[p.i]),
+ }
+ }
+}
+
+func (p *Tokenizer) Next() (Token, error) {
+ if p.i >= len(p.src) {
+ return NewToken(EOF, "", p.i, 0), nil
+ }
+
+ t, err := p.parseCurRune()
+ if err != nil {
+ return Token{}, err
+ }
+ p.i += t.Len
+ return t, nil
+}
diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go
new file mode 100644
index 00000000..3d89c8f9
--- /dev/null
+++ b/selector/lexer/tokenize_test.go
@@ -0,0 +1,135 @@
+package lexer_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+type testCase struct {
+ in string
+ out []lexer.TokenKind
+}
+
+func (tc testCase) run(t *testing.T) {
+ tok := lexer.NewTokenizer(tc.in)
+ tokens, err := tok.Tokenize()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tokens) != len(tc.out) {
+ t.Fatalf("unexpected number of tokens: %d", len(tokens))
+ }
+ for i := range tokens {
+ if tokens[i].Kind != tc.out[i] {
+ t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind)
+ return
+ }
+ }
+}
+
+type errTestCase struct {
+ in string
+ match func(error) bool
+}
+
+func (tc errTestCase) run(t *testing.T) {
+ tok := lexer.NewTokenizer(tc.in)
+ tokens, err := tok.Tokenize()
+ if !tc.match(err) {
+ t.Errorf("unexpected error, got %v", err)
+ }
+ if tokens != nil {
+ t.Errorf("unexpected tokens: %v", tokens)
+ }
+}
+
+func matchUnexpectedError(r rune, p int) func(error) bool {
+ return func(err error) bool {
+ var e *lexer.UnexpectedTokenError
+ if !errors.As(err, &e) {
+ return false
+ }
+
+ return e.Token == r && e.Pos == p
+ }
+}
+
+func matchUnexpectedEOFError(p int) func(error) bool {
+ return func(err error) bool {
+ var e *lexer.UnexpectedEOFError
+ if !errors.As(err, &e) {
+ return false
+ }
+
+ return e.Pos == p
+ }
+}
+
+func TestTokenizer_Parse(t *testing.T) {
+ t.Run("variables", testCase{
+ in: "$foo $bar123 $baz $",
+ out: []lexer.TokenKind{
+ lexer.Variable,
+ lexer.Variable,
+ lexer.Variable,
+ lexer.Dollar,
+ },
+ }.run)
+
+ t.Run("if", testCase{
+ in: `if elseif else`,
+ out: []lexer.TokenKind{
+ lexer.If,
+ lexer.ElseIf,
+ lexer.Else,
+ },
+ }.run)
+
+ t.Run("regex", testCase{
+ in: `r/asd/ r/hello there/`,
+ out: []lexer.TokenKind{
+ lexer.RegexPattern,
+ lexer.RegexPattern,
+ },
+ }.run)
+
+ t.Run("sort by", testCase{
+ in: `sortBy(foo, asc)`,
+ out: []lexer.TokenKind{
+ lexer.SortBy,
+ lexer.OpenParen,
+ lexer.Symbol,
+ lexer.Comma,
+ lexer.Asc,
+ lexer.CloseParen,
+ },
+ }.run)
+
+ t.Run("everything", testCase{
+ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null",
+ out: []lexer.TokenKind{
+ lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.Number, lexer.CloseBracket, lexer.NotEqual, lexer.Number,
+ lexer.Or,
+ lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.String, lexer.CloseBracket, lexer.Equal, lexer.Number,
+ lexer.And,
+ lexer.Symbol, lexer.Equal, lexer.String,
+ lexer.Plus, lexer.Bool, lexer.Bool,
+ lexer.Dot, lexer.Spread, lexer.Dot,
+ lexer.Symbol, lexer.Spread,
+ lexer.Variable, lexer.Null,
+ },
+ }.run)
+
+ t.Run("unhappy", func(t *testing.T) {
+ t.Run("unfinished double quote", errTestCase{
+ in: `"hello`,
+ match: matchUnexpectedEOFError(6),
+ }.run)
+ t.Run("unfinished single quote", errTestCase{
+ in: `'hello`,
+ match: matchUnexpectedEOFError(6),
+ }.run)
+ })
+}
diff --git a/selector/parser.go b/selector/parser.go
new file mode 100644
index 00000000..b54fa1fe
--- /dev/null
+++ b/selector/parser.go
@@ -0,0 +1,16 @@
+package selector
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+ "github.com/tomwright/dasel/v3/selector/parser"
+)
+
+func Parse(selector string) (ast.Expr, error) {
+ tokens, err := lexer.NewTokenizer(selector).Tokenize()
+ if err != nil {
+ return nil, err
+ }
+
+ return parser.NewParser(tokens).Parse()
+}
diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go
new file mode 100644
index 00000000..8828cc8c
--- /dev/null
+++ b/selector/parser/denotations.go
@@ -0,0 +1,93 @@
+package parser
+
+import "github.com/tomwright/dasel/v3/selector/lexer"
+
+// null denotation tokens are tokens that expect no token to the left of them.
+var nullDenotationTokens = []lexer.TokenKind{}
+
+// left denotation tokens are tokens that expect a token to the left of them.
+var leftDenotationTokens = []lexer.TokenKind{
+ lexer.Plus,
+ lexer.Dash,
+ lexer.Slash,
+ lexer.Star,
+ lexer.Percent,
+ lexer.Equal,
+ lexer.NotEqual,
+ lexer.GreaterThan,
+ lexer.GreaterThanOrEqual,
+ lexer.LessThan,
+ lexer.LessThanOrEqual,
+ lexer.And,
+ lexer.Or,
+ lexer.Like,
+ lexer.NotLike,
+ lexer.Equals,
+ lexer.DoubleQuestionMark,
+}
+
+// right denotation tokens are tokens that expect a token to the right of them.
+var rightDenotationTokens = []lexer.TokenKind{
+ lexer.Exclamation, // Not operator
+}
+
+type bindingPower int
+
+const (
+ bpDefault bindingPower = iota
+ bpAssignment
+ bpLogical
+ bpEarlyLogical
+ bpRelational
+ bpAdditive
+ bpMultiplicative
+ bpUnary
+ bpCall
+ bpProperty
+ bpLiteral
+)
+
+var tokenBindingPowers = map[lexer.TokenKind]bindingPower{
+ lexer.String: bpLiteral,
+ lexer.Number: bpLiteral,
+ lexer.Bool: bpLiteral,
+ lexer.Null: bpLiteral,
+
+ lexer.Variable: bpProperty,
+ lexer.Dot: bpProperty,
+ lexer.OpenBracket: bpProperty,
+
+ lexer.OpenParen: bpCall,
+
+ lexer.Exclamation: bpUnary,
+
+ lexer.Star: bpMultiplicative,
+ lexer.Slash: bpMultiplicative,
+ lexer.Percent: bpMultiplicative,
+
+ lexer.Plus: bpAdditive,
+ lexer.Dash: bpAdditive,
+
+ lexer.Equal: bpRelational,
+ lexer.NotEqual: bpRelational,
+ lexer.GreaterThan: bpRelational,
+ lexer.GreaterThanOrEqual: bpRelational,
+ lexer.LessThan: bpRelational,
+ lexer.LessThanOrEqual: bpRelational,
+
+ lexer.And: bpLogical,
+ lexer.Or: bpLogical,
+ lexer.Like: bpLogical,
+ lexer.NotLike: bpLogical,
+
+ lexer.DoubleQuestionMark: bpEarlyLogical,
+
+ lexer.Equals: bpAssignment,
+}
+
+func getTokenBindingPower(t lexer.TokenKind) bindingPower {
+ if bp, ok := tokenBindingPowers[t]; ok {
+ return bp
+ }
+ return bpDefault
+}
diff --git a/selector/parser/error.go b/selector/parser/error.go
new file mode 100644
index 00000000..e3611b35
--- /dev/null
+++ b/selector/parser/error.go
@@ -0,0 +1,24 @@
+package parser
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+type PositionalError struct {
+ Position int
+ Err error
+}
+
+func (e *PositionalError) Error() string {
+ return fmt.Sprintf("%v. Position %d.", e.Err, e.Position)
+}
+
+type UnexpectedTokenError struct {
+ Token lexer.Token
+}
+
+func (e *UnexpectedTokenError) Error() string {
+ return fmt.Sprintf("failed to parse: unexpected token %v %q at position %d.", e.Token.Kind, e.Token.Value, e.Token.Pos)
+}
diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go
new file mode 100644
index 00000000..8f3cadb2
--- /dev/null
+++ b/selector/parser/parse_array.go
@@ -0,0 +1,149 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseArray(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.OpenBracket); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ elements, err := p.parseExpressionsAsSlice(
+ lexer.TokenKinds(lexer.CloseBracket),
+ lexer.TokenKinds(lexer.Comma),
+ false,
+ bpDefault,
+ true,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ arr := ast.ArrayExpr{
+ Exprs: elements,
+ }
+
+ res, err := parseFollowingSymbol(p, arr)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+func parseIndex(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.Symbol, lexer.Variable); err != nil {
+ return nil, err
+ }
+ if err := p.expectN(1, lexer.OpenBracket); err != nil {
+ return nil, err
+ }
+ token := p.current()
+ p.advance()
+
+ idx, err := parseIndexSquareBrackets(p)
+ if err != nil {
+ return nil, err
+ }
+
+ var e ast.Expr
+
+ switch {
+ case token.IsKind(lexer.Variable):
+ e = ast.VariableExpr{
+ Name: token.Value,
+ }
+ case token.IsKind(lexer.Symbol):
+ e = ast.PropertyExpr{
+ Property: ast.StringExpr{Value: token.Value},
+ }
+ default:
+ panic("unexpected token kind")
+ }
+
+ return ast.ChainExprs(
+ e,
+ idx,
+ ), nil
+}
+
+// parseIndexSquareBrackets parses square bracket array access.
+// E.g. [0], [0:1], [0:], [:2]
+func parseIndexSquareBrackets(p *Parser) (ast.Expr, error) {
+ // Handle index (from bracket)
+ if err := p.expect(lexer.OpenBracket); err != nil {
+ return nil, err
+ }
+
+ p.advance()
+
+ // Spread [...]
+ if p.current().IsKind(lexer.Spread) {
+ p.advance()
+ if err := p.expect(lexer.CloseBracket); err != nil {
+ return nil, err
+ }
+ p.advance()
+ return ast.SpreadExpr{}, nil
+ }
+
+ var (
+ start ast.Expr
+ end ast.Expr
+ err error
+ )
+
+ if p.current().IsKind(lexer.Colon) {
+ p.advance()
+ // We have no start index
+ end, err = p.parseExpression(bpDefault)
+ if err != nil {
+ return nil, err
+ }
+ p.advance()
+ return ast.RangeExpr{
+ End: end,
+ }, nil
+ }
+
+ start, err = p.parseExpression(bpDefault)
+ if err != nil {
+ return nil, err
+ }
+
+ if p.current().IsKind(lexer.CloseBracket) {
+ // This is an index
+ p.advance()
+ return ast.IndexExpr{
+ Index: start,
+ }, nil
+ }
+
+ if err := p.expect(lexer.Colon); err != nil {
+ return nil, err
+ }
+
+ p.advance()
+
+ if p.current().IsKind(lexer.CloseBracket) {
+ // There is no end
+ p.advance()
+ return ast.RangeExpr{
+ Start: start,
+ }, nil
+ }
+
+ end, err = p.parseExpression(bpDefault)
+ if err != nil {
+ return nil, err
+ }
+
+ p.advance()
+ return ast.RangeExpr{
+ Start: start,
+ End: end,
+ }, nil
+}
diff --git a/selector/parser/parse_branch.go b/selector/parser/parse_branch.go
new file mode 100644
index 00000000..0ba77757
--- /dev/null
+++ b/selector/parser/parse_branch.go
@@ -0,0 +1,33 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseBranch(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.Branch); err != nil {
+ return nil, err
+ }
+
+ p.advance()
+ if err := p.expect(lexer.OpenParen); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ expressions, err := p.parseExpressionsAsSlice(
+ []lexer.TokenKind{lexer.CloseParen},
+ []lexer.TokenKind{lexer.Comma},
+ true,
+ bpDefault,
+ true,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return ast.BranchExpr{
+ Exprs: expressions,
+ }, nil
+}
diff --git a/selector/parser/parse_filter.go b/selector/parser/parse_filter.go
new file mode 100644
index 00000000..c9462a34
--- /dev/null
+++ b/selector/parser/parse_filter.go
@@ -0,0 +1,28 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseFilter(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.Filter); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ expr, err := p.parseExpressionsFromTo(
+ lexer.OpenParen,
+ lexer.CloseParen,
+ []lexer.TokenKind{},
+ true,
+ bpDefault,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return ast.FilterExpr{
+ Expr: expr,
+ }, nil
+}
diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go
new file mode 100644
index 00000000..0935d3ec
--- /dev/null
+++ b/selector/parser/parse_func.go
@@ -0,0 +1,37 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseFunc(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.Symbol); err != nil {
+ return nil, err
+ }
+ if err := p.expectN(1, lexer.OpenParen); err != nil {
+ return nil, err
+ }
+
+ token := p.current()
+
+ p.advanceN(2)
+ args, err := parseArgs(p)
+ if err != nil {
+ return nil, err
+ }
+ return ast.CallExpr{
+ Function: token.Value,
+ Args: args,
+ }, nil
+}
+
+func parseArgs(p *Parser) (ast.Expressions, error) {
+ return p.parseExpressionsAsSlice(
+ []lexer.TokenKind{lexer.CloseParen},
+ []lexer.TokenKind{lexer.Comma},
+ false,
+ bpCall,
+ true,
+ )
+}
diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go
new file mode 100644
index 00000000..f6ff0175
--- /dev/null
+++ b/selector/parser/parse_group.go
@@ -0,0 +1,21 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseGroup(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.OpenParen); err != nil {
+ return nil, err
+ }
+ p.advance() // skip the open paren
+
+ return p.parseExpressions(
+ []lexer.TokenKind{lexer.CloseParen},
+ []lexer.TokenKind{},
+ true,
+ bpDefault,
+ true,
+ )
+}
diff --git a/selector/parser/parse_if.go b/selector/parser/parse_if.go
new file mode 100644
index 00000000..63d90f6c
--- /dev/null
+++ b/selector/parser/parse_if.go
@@ -0,0 +1,102 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseIfBody(p *Parser) (ast.Expr, error) {
+ return p.parseExpressionsFromTo(lexer.OpenCurly, lexer.CloseCurly, []lexer.TokenKind{}, true, bpDefault)
+}
+
+func parseIfCondition(p *Parser) (ast.Expr, error) {
+ return p.parseExpressionsFromTo(lexer.OpenParen, lexer.CloseParen, []lexer.TokenKind{}, true, bpDefault)
+}
+
+func parseIf(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.If); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ var exprs []*ast.ConditionalExpr
+
+ process := func(parseCond bool) error {
+ var err error
+ var cond ast.Expr
+ if parseCond {
+ cond, err = parseIfCondition(p)
+ if err != nil {
+ return err
+ }
+ }
+
+ body, err := parseIfBody(p)
+ if err != nil {
+ return err
+ }
+
+ exprs = append(exprs, &ast.ConditionalExpr{
+ Cond: cond,
+ Then: body,
+ })
+
+ return nil
+ }
+
+ if err := process(true); err != nil {
+ return nil, err
+ }
+
+ for p.current().IsKind(lexer.ElseIf) {
+ p.advance()
+
+ if err := process(true); err != nil {
+ return nil, err
+ }
+ }
+
+ if p.current().IsKind(lexer.Else) {
+ p.advance()
+
+ body, err := parseIfBody(p)
+ if err != nil {
+ return nil, err
+ }
+ exprs[len(exprs)-1].Else = body
+ }
+
+ for i := len(exprs) - 1; i >= 0; i-- {
+ if i > 0 {
+ exprs[i-1].Else = *exprs[i]
+ }
+ }
+
+ return *exprs[0], nil
+}
+
+func (p *Parser) parseExpressionsFromTo(
+ from lexer.TokenKind,
+ to lexer.TokenKind,
+ splitOn []lexer.TokenKind,
+ requireExpressions bool,
+ bp bindingPower,
+) (ast.Expr, error) {
+ if err := p.expect(from); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ t, err := p.parseExpressions(
+ []lexer.TokenKind{to},
+ splitOn,
+ requireExpressions,
+ bp,
+ true,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return t, nil
+}
diff --git a/selector/parser/parse_literal.go b/selector/parser/parse_literal.go
new file mode 100644
index 00000000..932934a3
--- /dev/null
+++ b/selector/parser/parse_literal.go
@@ -0,0 +1,85 @@
+package parser
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseStringLiteral(p *Parser) (ast.Expr, error) {
+ token := p.current()
+ p.advance()
+ return ast.StringExpr{
+ Value: token.Value,
+ }, nil
+}
+
+func parseBoolLiteral(p *Parser) (ast.Expr, error) {
+ token := p.current()
+ p.advance()
+ return ast.BoolExpr{
+ Value: strings.EqualFold(token.Value, "true"),
+ }, nil
+}
+
+func parseSpread(p *Parser) (ast.Expr, error) {
+ p.advance()
+ return ast.SpreadExpr{}, nil
+}
+
+func parseNumberLiteral(p *Parser) (ast.Expr, error) {
+ token := p.current()
+ next := p.advance()
+ switch {
+ case next.IsKind(lexer.Symbol) && strings.EqualFold(next.Value, "f"):
+ value, err := strconv.ParseFloat(token.Value, 64)
+ if err != nil {
+ return nil, err
+ }
+ p.advance()
+ return ast.NumberFloatExpr{
+ Value: value,
+ }, nil
+
+ case strings.Contains(token.Value, "."):
+ value, err := strconv.ParseFloat(token.Value, 64)
+ if err != nil {
+ return nil, err
+ }
+ return ast.NumberFloatExpr{
+ Value: value,
+ }, nil
+
+ default:
+ value, err := strconv.ParseInt(token.Value, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ return ast.NumberIntExpr{
+ Value: value,
+ }, nil
+ }
+}
+
+func parseRegexPattern(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.RegexPattern); err != nil {
+ return nil, err
+ }
+
+ pattern := p.current()
+
+ p.advance()
+
+ comp, err := regexp.Compile(pattern.Value)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile regexp pattern: %w", err)
+ }
+
+ return ast.RegexExpr{
+ Regex: comp,
+ }, nil
+}
diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go
new file mode 100644
index 00000000..5ad21fc0
--- /dev/null
+++ b/selector/parser/parse_map.go
@@ -0,0 +1,28 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseMap(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.Map); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ expr, err := p.parseExpressionsFromTo(
+ lexer.OpenParen,
+ lexer.CloseParen,
+ []lexer.TokenKind{},
+ true,
+ bpDefault,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return ast.MapExpr{
+ Expr: expr,
+ }, nil
+}
diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go
new file mode 100644
index 00000000..d43e954f
--- /dev/null
+++ b/selector/parser/parse_object.go
@@ -0,0 +1,108 @@
+package parser
+
+import (
+ "fmt"
+
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseObject(p *Parser) (ast.Expr, error) {
+
+ //p.parseExpressionsFromTo(
+ // lexer.OpenCurly,
+ // lexer.CloseCurly,
+ // lexer.TokenKinds(lexer.Comma),
+ // false,
+ // bpDefault,
+ //)
+
+ if err := p.expect(lexer.OpenCurly); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ pairs := make([]ast.KeyValue, 0)
+
+ parseKeyValue := func() (ast.KeyValue, error) {
+ var res ast.KeyValue
+ k, err := p.parseExpression(bpDefault)
+ if err != nil {
+ return res, err
+ }
+
+ // Handle spread
+ kSpread, isSpread := ast.LastAsType[ast.SpreadExpr](k)
+ if isSpread {
+ res.Key = kSpread
+ res.Value = ast.RemoveLast(k)
+ if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil {
+ return res, err
+ }
+ return res, nil
+ }
+
+ kProp, kIsProp := ast.AsType[ast.PropertyExpr](k)
+ if p.current().IsKind(lexer.Comma, lexer.CloseCurly) {
+ if !kIsProp {
+ return res, fmt.Errorf("invalid shorthand property")
+ }
+ res.Key = kProp.Property
+ res.Value = kProp
+ return res, nil
+ }
+
+ // Handle unquoted keys
+ if kIsProp {
+ if kStr, ok := ast.AsType[ast.StringExpr](kProp.Property); ok {
+ k = kStr
+ }
+ }
+
+ if err := p.expect(lexer.Colon); err != nil {
+ return res, err
+ }
+ p.advance()
+
+ v, err := p.parseExpression(bpDefault)
+ if err != nil {
+ return res, err
+ }
+
+ res.Key = k
+ res.Value = v
+ return res, nil
+ }
+
+ for !p.current().IsKind(lexer.CloseCurly) {
+ kv, err := parseKeyValue()
+ if err != nil {
+ return nil, err
+ }
+
+ pairs = append(pairs, kv)
+
+ if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil {
+ return nil, fmt.Errorf("expected end of object element: %w", err)
+ }
+ if p.current().IsKind(lexer.Comma) {
+ p.advance()
+ }
+ }
+
+ if err := p.expect(lexer.CloseCurly); err != nil {
+ return nil, fmt.Errorf("expected end of object: %w", err)
+ }
+ p.advance()
+
+ obj := ast.ObjectExpr{
+ Pairs: pairs,
+ }
+
+ res, err := parseFollowingSymbol(p, obj)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/selector/parser/parse_sort_by.go b/selector/parser/parse_sort_by.go
new file mode 100644
index 00000000..040e29e8
--- /dev/null
+++ b/selector/parser/parse_sort_by.go
@@ -0,0 +1,60 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+func parseSortBy(p *Parser) (ast.Expr, error) {
+ if err := p.expect(lexer.SortBy); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ if err := p.expect(lexer.OpenParen); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ sortExpr, err := p.parseExpressions(
+ lexer.TokenKinds(lexer.CloseParen, lexer.Comma),
+ nil,
+ true,
+ bpDefault,
+ false,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ res := ast.SortByExpr{
+ Expr: sortExpr,
+ Descending: false,
+ }
+
+ if p.current().IsKind(lexer.CloseParen) {
+ p.advance()
+ return res, nil
+ }
+
+ if err := p.expect(lexer.Comma); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ if err := p.expect(lexer.Asc, lexer.Desc); err != nil {
+ return nil, err
+ }
+
+ if p.current().IsKind(lexer.Desc) {
+ res.Descending = true
+ }
+
+ p.advance()
+ if err := p.expect(lexer.CloseParen); err != nil {
+ return nil, err
+ }
+ p.advance()
+
+ return res, nil
+}
diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go
new file mode 100644
index 00000000..e38f29c6
--- /dev/null
+++ b/selector/parser/parse_symbol.go
@@ -0,0 +1,89 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+// parseFollowingSymbols deals with the expressions following symbols/variables, e.g.
+// $this[0][1]['name']
+// foo['bar']['baz'][1]
+func parseFollowingSymbol(p *Parser, prev ast.Expr) (ast.Expr, error) {
+ res := ast.Expressions{prev}
+
+ for p.hasToken() {
+ if p.current().IsKind(lexer.Spread) {
+ p.advanceN(1)
+ res = append(res, ast.SpreadExpr{})
+ continue
+ }
+
+ // String based indexes
+ if p.current().IsKind(lexer.OpenBracket) {
+
+ if p.peekN(1).IsKind(lexer.Spread) && p.peekN(2).IsKind(lexer.CloseBracket) {
+ p.advanceN(3)
+ res = append(res, ast.SpreadExpr{})
+ continue
+ }
+
+ if p.peekN(1).IsKind(lexer.Star) && p.peekN(2).IsKind(lexer.CloseBracket) {
+ p.advanceN(3)
+ res = append(res, ast.SpreadExpr{})
+ continue
+ }
+
+ e, err := parseIndexSquareBrackets(p)
+ if err != nil {
+ return nil, err
+ }
+ switch ex := e.(type) {
+ case ast.RangeExpr:
+ res = append(res, ex)
+ case ast.IndexExpr:
+ // Convert this to a property expr. This property executor deals
+ // with maps + arrays.
+ res = append(res, ast.PropertyExpr{
+ Property: ex.Index,
+ })
+ }
+
+ //e, err := p.parseExpressionsFromTo(lexer.OpenBracket, lexer.CloseBracket, nil, true, bpDefault)
+ //if err != nil {
+ // return nil, err
+ //}
+ //res = append(res, ast.PropertyExpr{
+ // Property: e,
+ //})
+ continue
+ }
+
+ break
+ }
+
+ return ast.ChainExprs(res...), nil
+}
+
+func parseSymbol(p *Parser) (ast.Expr, error) {
+ token := p.current()
+
+ next := p.peek()
+
+ // Handle functions
+ if next.IsKind(lexer.OpenParen) {
+ return parseFunc(p)
+ }
+
+ prop := ast.PropertyExpr{
+ Property: ast.StringExpr{Value: token.Value},
+ }
+
+ p.advance()
+
+ res, err := parseFollowingSymbol(p, prop)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/selector/parser/parse_variable.go b/selector/parser/parse_variable.go
new file mode 100644
index 00000000..26b38ea2
--- /dev/null
+++ b/selector/parser/parse_variable.go
@@ -0,0 +1,22 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func parseVariable(p *Parser) (ast.Expr, error) {
+ token := p.current()
+
+ prop := ast.VariableExpr{
+ Name: token.Value,
+ }
+
+ p.advance()
+
+ res, err := parseFollowingSymbol(p, prop)
+ if err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
diff --git a/selector/parser/parser.go b/selector/parser/parser.go
new file mode 100644
index 00000000..a500d851
--- /dev/null
+++ b/selector/parser/parser.go
@@ -0,0 +1,251 @@
+package parser
+
+import (
+ "slices"
+
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+)
+
+type Parser struct {
+ tokens lexer.Tokens
+ i int
+}
+
+func NewParser(tokens lexer.Tokens) *Parser {
+ return &Parser{
+ tokens: tokens,
+ }
+}
+
+func (p *Parser) parseExpressionsAsSlice(
+ breakOn []lexer.TokenKind,
+ splitOn []lexer.TokenKind,
+ requireExpressions bool,
+ bp bindingPower,
+ advanceOnBreak bool,
+) (ast.Expressions, error) {
+ var finalExpr ast.Expressions
+ var current ast.Expressions
+ for p.hasToken() {
+ if p.current().IsKind(breakOn...) {
+ if advanceOnBreak {
+ p.advance()
+ }
+ break
+ }
+ if p.current().IsKind(splitOn...) {
+ if requireExpressions && len(current) == 0 {
+ return nil, &UnexpectedTokenError{Token: p.current()}
+ }
+ finalExpr = append(finalExpr, ast.ChainExprs(current...))
+ current = nil
+ p.advance()
+ continue
+ }
+ expr, err := p.parseExpression(bp)
+ if err != nil {
+ return nil, err
+ }
+ current = append(current, expr)
+ }
+
+ if len(current) > 0 {
+ finalExpr = append(finalExpr, ast.ChainExprs(current...))
+ }
+
+ if len(finalExpr) == 0 {
+ return nil, nil
+ }
+
+ return finalExpr, nil
+}
+
+func (p *Parser) parseExpressions(
+ breakOn []lexer.TokenKind,
+ splitOn []lexer.TokenKind,
+ requireExpressions bool,
+ bp bindingPower,
+ advanceOnBreak bool,
+) (ast.Expr, error) {
+ expressions, err := p.parseExpressionsAsSlice(breakOn, splitOn, requireExpressions, bp, advanceOnBreak)
+ if err != nil {
+ return nil, err
+ }
+ switch len(expressions) {
+ case 0:
+ return nil, nil
+ default:
+ return ast.ChainExprs(expressions...), nil
+ }
+}
+
+func (p *Parser) Parse() (ast.Expr, error) {
+ return p.parseExpressions([]lexer.TokenKind{lexer.EOF}, nil, true, bpDefault, true)
+}
+
+func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) {
+ if p.hasToken() && slices.Contains(rightDenotationTokens, p.current().Kind) {
+ unary := ast.UnaryExpr{
+ Operator: p.current(),
+ Right: nil,
+ }
+ p.advance()
+ expr, err := p.parseExpression(getTokenBindingPower(unary.Operator.Kind))
+ if err != nil {
+ return nil, err
+ }
+ p.advance()
+ unary.Right = expr
+ left = unary
+ }
+
+ if !p.hasToken() {
+ return
+ }
+
+ switch p.current().Kind {
+ case lexer.String:
+ left, err = parseStringLiteral(p)
+ case lexer.Number:
+ left, err = parseNumberLiteral(p)
+ case lexer.Symbol:
+ left, err = parseSymbol(p)
+ case lexer.OpenBracket:
+ left, err = parseArray(p)
+ case lexer.OpenCurly:
+ left, err = parseObject(p)
+ case lexer.Bool:
+ left, err = parseBoolLiteral(p)
+ case lexer.Spread:
+ left, err = parseSpread(p)
+ case lexer.Variable:
+ left, err = parseVariable(p)
+ case lexer.OpenParen:
+ left, err = parseGroup(p)
+ case lexer.If:
+ left, err = parseIf(p)
+ case lexer.Branch:
+ left, err = parseBranch(p)
+ case lexer.Map:
+ left, err = parseMap(p)
+ case lexer.Filter:
+ left, err = parseFilter(p)
+ case lexer.RegexPattern:
+ left, err = parseRegexPattern(p)
+ case lexer.SortBy:
+ left, err = parseSortBy(p)
+ case lexer.Null:
+ left = ast.NullExpr{}
+ err = nil
+ p.advance()
+ default:
+ return nil, &UnexpectedTokenError{
+ Token: p.current(),
+ }
+ }
+
+ if err != nil {
+ return
+ }
+
+ toChain := ast.Expressions{left}
+ // Ensure dot separated chains are parsed as a sequence of expressions
+ if p.hasToken() && p.current().IsKind(lexer.Dot) {
+ for p.hasToken() && p.current().IsKind(lexer.Dot) {
+ p.advance()
+ expr, err := p.parseExpression(bpUnary)
+ if err != nil {
+ return nil, err
+ }
+ toChain = append(toChain, expr)
+ }
+ }
+
+ // Handle spread
+ if p.hasToken() && p.current().IsKind(lexer.Spread) {
+ expr, err := p.parseExpression(bpLiteral)
+ if err != nil {
+ return nil, err
+ }
+ toChain = append(toChain, expr)
+ }
+
+ if len(toChain) > 1 {
+ left = ast.ChainExprs(toChain...)
+ }
+
+ // Handle binding powers
+ for p.hasToken() && slices.Contains(leftDenotationTokens, p.current().Kind) && getTokenBindingPower(p.current().Kind) > bp {
+ left, err = parseBinary(p, left)
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
+
+func (p *Parser) hasToken() bool {
+ return p.i < len(p.tokens) && !p.tokens[p.i].IsKind(lexer.EOF)
+}
+
+func (p *Parser) hasTokenN(n int) bool {
+ return p.i+n < len(p.tokens) && !p.tokens[p.i+n].IsKind(lexer.EOF)
+}
+
+func (p *Parser) current() lexer.Token {
+ if p.hasToken() {
+ return p.tokens[p.i]
+ }
+ return lexer.Token{Kind: lexer.EOF}
+}
+
+func (p *Parser) previous() lexer.Token {
+ i := p.i - 1
+ if i > 0 && i < len(p.tokens) {
+ return p.tokens[i]
+ }
+ return lexer.Token{Kind: lexer.EOF}
+}
+
+func (p *Parser) advance() lexer.Token {
+ p.i++
+ return p.current()
+}
+
+func (p *Parser) advanceN(n int) lexer.Token {
+ p.i += n
+ return p.current()
+}
+
+func (p *Parser) peek() lexer.Token {
+ return p.peekN(1)
+}
+
+func (p *Parser) peekN(n int) lexer.Token {
+ if p.i+n >= len(p.tokens) {
+ return lexer.Token{Kind: lexer.EOF}
+ }
+ return p.tokens[p.i+n]
+}
+
+func (p *Parser) expect(kind ...lexer.TokenKind) error {
+ t := p.current()
+ if p.current().IsKind(kind...) {
+ return nil
+ }
+ return &UnexpectedTokenError{
+ Token: t,
+ }
+}
+
+func (p *Parser) expectN(n int, kind ...lexer.TokenKind) error {
+ t := p.peekN(n)
+ if t.IsKind(kind...) {
+ return nil
+ }
+ return &UnexpectedTokenError{
+ Token: t,
+ }
+}
diff --git a/selector/parser/parser_binary.go b/selector/parser/parser_binary.go
new file mode 100644
index 00000000..64e4e7f4
--- /dev/null
+++ b/selector/parser/parser_binary.go
@@ -0,0 +1,61 @@
+package parser
+
+import (
+ "github.com/tomwright/dasel/v3/selector/ast"
+)
+
+func parseBinary(p *Parser, left ast.Expr) (ast.Expr, error) {
+ if err := p.expect(leftDenotationTokens...); err != nil {
+ return nil, err
+ }
+ operator := p.current()
+ p.advance()
+ right, err := p.parseExpression(getTokenBindingPower(operator.Kind))
+ if err != nil {
+ return nil, err
+ }
+
+ //if l, ok := left.(ast.BinaryExpr); ok && l.Operator.Kind == lexer.DoubleQuestionMark {
+ // if r, ok := right.(ast.BinaryExpr); ok && r.Operator.Kind == lexer.DoubleQuestionMark {
+ // return ast.BinaryExpr{
+ // Left: l.Left,
+ // Operator: l.Operator,
+ // Right: ast.BinaryExpr{
+ // Left: l.Right,
+ // Operator: r.Operator,
+ // Right: r.Right,
+ // },
+ // }, nil
+ // }
+ //}
+ //
+ //if r, ok := right.(ast.BinaryExpr); ok && r.Operator.Kind == lexer.DoubleQuestionMark {
+ // return ast.BinaryExpr{
+ // Left: ast.BinaryExpr{
+ // Left: left,
+ // Operator: operator,
+ // Right: r.Left,
+ // },
+ // Operator: r.Operator,
+ // Right: r.Right,
+ // }, nil
+ //}
+ //
+ //if l, ok := left.(ast.BinaryExpr); ok && l.Operator.Kind == lexer.DoubleQuestionMark {
+ // return ast.BinaryExpr{
+ // Left: l.Left,
+ // Operator: l.Operator,
+ // Right: ast.BinaryExpr{
+ // Left: l.Right,
+ // Operator: operator,
+ // Right: right,
+ // },
+ // }, nil
+ //}
+
+ return ast.BinaryExpr{
+ Left: left,
+ Operator: operator,
+ Right: right,
+ }, nil
+}
diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go
new file mode 100644
index 00000000..43debac1
--- /dev/null
+++ b/selector/parser/parser_test.go
@@ -0,0 +1,468 @@
+package parser_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/tomwright/dasel/v3/selector/ast"
+ "github.com/tomwright/dasel/v3/selector/lexer"
+ "github.com/tomwright/dasel/v3/selector/parser"
+)
+
+type happyTestCase struct {
+ input string
+ expected ast.Expr
+}
+
+func (tc happyTestCase) run(t *testing.T) {
+ tokens, err := lexer.NewTokenizer(tc.input).Tokenize()
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := parser.NewParser(tokens).Parse()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !cmp.Equal(tc.expected, got) {
+ t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got))
+ }
+}
+
+func TestParser_Parse_HappyPath(t *testing.T) {
+ t.Run("branching", func(t *testing.T) {
+ t.Run("two branches", happyTestCase{
+ input: `branch("hello", len("world"))`,
+ expected: ast.BranchExprs(
+ ast.StringExpr{Value: "hello"},
+ ast.ChainExprs(
+ ast.CallExpr{
+ Function: "len",
+ Args: ast.Expressions{ast.StringExpr{Value: "world"}},
+ },
+ ),
+ ),
+ }.run)
+ t.Run("three branches", happyTestCase{
+ input: `branch("foo", "bar", "baz")`,
+ expected: ast.BranchExprs(
+ ast.StringExpr{Value: "foo"},
+ ast.StringExpr{Value: "bar"},
+ ast.StringExpr{Value: "baz"},
+ ),
+ }.run)
+ })
+
+ t.Run("literal access", func(t *testing.T) {
+ t.Run("string", happyTestCase{
+ input: `"hello world"`,
+ expected: ast.StringExpr{Value: "hello world"},
+ }.run)
+ t.Run("int", happyTestCase{
+ input: "42",
+ expected: ast.NumberIntExpr{Value: 42},
+ }.run)
+ t.Run("float", happyTestCase{
+ input: "42.1",
+ expected: ast.NumberFloatExpr{Value: 42.1},
+ }.run)
+ t.Run("whole number float", happyTestCase{
+ input: "42f",
+ expected: ast.NumberFloatExpr{Value: 42},
+ }.run)
+ t.Run("bool true lowercase", happyTestCase{
+ input: "true",
+ expected: ast.BoolExpr{Value: true},
+ }.run)
+ t.Run("bool true uppercase", happyTestCase{
+ input: "TRUE",
+ expected: ast.BoolExpr{Value: true},
+ }.run)
+ t.Run("bool true mixed case", happyTestCase{
+ input: "TrUe",
+ expected: ast.BoolExpr{Value: true},
+ }.run)
+ t.Run("bool false lowercase", happyTestCase{
+ input: "false",
+ expected: ast.BoolExpr{Value: false},
+ }.run)
+ t.Run("bool false uppercase", happyTestCase{
+ input: "FALSE",
+ expected: ast.BoolExpr{Value: false},
+ }.run)
+ t.Run("bool false mixed case", happyTestCase{
+ input: "FaLsE",
+ expected: ast.BoolExpr{Value: false},
+ }.run)
+ })
+
+ t.Run("property access", func(t *testing.T) {
+ t.Run("single property access", happyTestCase{
+ input: "foo",
+ expected: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ }.run)
+ t.Run("chained property access", happyTestCase{
+ input: "foo.bar",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}},
+ ),
+ }.run)
+ t.Run("property access spread", happyTestCase{
+ input: "foo...",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.SpreadExpr{},
+ ),
+ }.run)
+ t.Run("property access spread into property access", happyTestCase{
+ input: "foo....bar",
+ expected: ast.ChainExprs(
+ ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.SpreadExpr{},
+ ),
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}},
+ ),
+ }.run)
+ })
+
+ t.Run("array access", func(t *testing.T) {
+ t.Run("root array", func(t *testing.T) {
+ t.Run("index", happyTestCase{
+ input: "$this[1]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}},
+ ),
+ }.run)
+ t.Run("range", func(t *testing.T) {
+ t.Run("start and end funcs", happyTestCase{
+ input: "$this[calcStart(1):calcEnd()]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.RangeExpr{
+ Start: ast.CallExpr{
+ Function: "calcStart",
+ Args: ast.Expressions{
+ ast.NumberIntExpr{Value: 1},
+ },
+ },
+ End: ast.CallExpr{
+ Function: "calcEnd",
+ },
+ },
+ ),
+ }.run)
+ t.Run("start and end", happyTestCase{
+ input: "$this[5:10]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}},
+ ),
+ }.run)
+ t.Run("start", happyTestCase{
+ input: "$this[5:]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}},
+ ),
+ }.run)
+ t.Run("end", happyTestCase{
+ input: "$this[:10]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}},
+ ),
+ }.run)
+ })
+ t.Run("spread", func(t *testing.T) {
+ t.Run("standard", happyTestCase{
+ input: "$this...",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.SpreadExpr{},
+ ),
+ }.run)
+ t.Run("brackets", happyTestCase{
+ input: "$this[...]",
+ expected: ast.ChainExprs(
+ ast.VariableExpr{Name: "this"},
+ ast.SpreadExpr{},
+ ),
+ }.run)
+ })
+ })
+ t.Run("property array", func(t *testing.T) {
+ t.Run("index", happyTestCase{
+ input: "foo[1]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}},
+ ),
+ }.run)
+ t.Run("range", func(t *testing.T) {
+ t.Run("start and end funcs", happyTestCase{
+ input: "foo[calcStart(1):calcEnd()]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.RangeExpr{
+ Start: ast.CallExpr{
+ Function: "calcStart",
+ Args: ast.Expressions{
+ ast.NumberIntExpr{Value: 1},
+ },
+ },
+ End: ast.CallExpr{
+ Function: "calcEnd",
+ },
+ },
+ ),
+ }.run)
+ t.Run("start and end", happyTestCase{
+ input: "foo[5:10]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}},
+ ),
+ }.run)
+ t.Run("start", happyTestCase{
+ input: "foo[5:]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}},
+ ),
+ }.run)
+ t.Run("end", happyTestCase{
+ input: "foo[:10]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}},
+ ),
+ }.run)
+ })
+ t.Run("spread", func(t *testing.T) {
+ t.Run("standard", happyTestCase{
+ input: "foo...",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.SpreadExpr{},
+ ),
+ }.run)
+ t.Run("brackets", happyTestCase{
+ input: "foo[...]",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.SpreadExpr{},
+ ),
+ }.run)
+ })
+ })
+ })
+
+ t.Run("map", func(t *testing.T) {
+ t.Run("single property", happyTestCase{
+ input: "foo.map(x)",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.MapExpr{
+ Expr: ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}},
+ },
+ ),
+ }.run)
+ t.Run("nested property", happyTestCase{
+ input: "foo.map(x.y)",
+ expected: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ ast.MapExpr{
+ Expr: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}},
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}},
+ ),
+ },
+ ),
+ }.run)
+ })
+
+ t.Run("object", func(t *testing.T) {
+ t.Run("get single property", happyTestCase{
+ input: "{foo}",
+ expected: ast.ObjectExpr{Pairs: []ast.KeyValue{
+ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}},
+ }},
+ }.run)
+ t.Run("get multiple properties", happyTestCase{
+ input: "{foo, bar, baz}",
+ expected: ast.ObjectExpr{Pairs: []ast.KeyValue{
+ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}},
+ {Key: ast.StringExpr{Value: "bar"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}},
+ {Key: ast.StringExpr{Value: "baz"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}},
+ }},
+ }.run)
+ t.Run("set single property", happyTestCase{
+ input: `{"foo":1}`,
+ expected: ast.ObjectExpr{Pairs: []ast.KeyValue{
+ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}},
+ }},
+ }.run)
+ t.Run("set multiple properties", happyTestCase{
+ input: `{foo: 1, bar: 2, baz: 3}`,
+ expected: ast.ObjectExpr{Pairs: []ast.KeyValue{
+ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}},
+ {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}},
+ {Key: ast.StringExpr{Value: "baz"}, Value: ast.NumberIntExpr{Value: 3}},
+ }},
+ }.run)
+ t.Run("combine get set", happyTestCase{
+ input: `{
+ ...,
+ nestedSpread...,
+ foo,
+ bar: 2,
+ "baz": evalSomething(),
+ "Name": "Tom",
+ }`,
+ expected: ast.ObjectExpr{Pairs: []ast.KeyValue{
+ {Key: ast.SpreadExpr{}, Value: nil},
+ {Key: ast.SpreadExpr{}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "nestedSpread"}}},
+ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}},
+ {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}},
+ {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething"}},
+ {Key: ast.StringExpr{Value: "Name"}, Value: ast.StringExpr{Value: "Tom"}},
+ }},
+ }.run)
+ })
+
+ t.Run("variables", func(t *testing.T) {
+ t.Run("single variable", happyTestCase{
+ input: `$foo`,
+ expected: ast.VariableExpr{Name: "foo"},
+ }.run)
+ t.Run("variable passed to func", happyTestCase{
+ input: `len($foo)`,
+ expected: ast.CallExpr{Function: "len", Args: ast.Expressions{ast.VariableExpr{Name: "foo"}}},
+ }.run)
+ })
+
+ t.Run("combinations and grouping", func(t *testing.T) {
+ t.Run("string concat with grouping", happyTestCase{
+ input: `(foo.a) + (foo.b)`,
+ expected: ast.BinaryExpr{
+ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}),
+ Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 8, Len: 1},
+ Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}),
+ },
+ }.run)
+ t.Run("string concat with nested properties", happyTestCase{
+ input: `foo.a + foo.b`,
+ expected: ast.BinaryExpr{
+ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}),
+ Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 6, Len: 1},
+ Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}),
+ },
+ }.run)
+ })
+
+ t.Run("conditional", func(t *testing.T) {
+ t.Run("if", happyTestCase{
+ input: `if (foo == 1) { "yes" } else { "no" }`,
+ expected: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2},
+ Right: ast.NumberIntExpr{Value: 1},
+ },
+ Then: ast.StringExpr{Value: "yes"},
+ Else: ast.StringExpr{Value: "no"},
+ },
+ }.run)
+ t.Run("if elseif else", happyTestCase{
+ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } else { "no" }`,
+ expected: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2},
+ Right: ast.NumberIntExpr{Value: 1},
+ },
+ Then: ast.StringExpr{Value: "yes"},
+ Else: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 36, Len: 2},
+ Right: ast.NumberIntExpr{Value: 2},
+ },
+ Then: ast.StringExpr{Value: "maybe"},
+ Else: ast.StringExpr{Value: "no"},
+ },
+ },
+ }.run)
+ t.Run("if elseif elseif else", happyTestCase{
+ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } elseif (foo == 3) { "probably" } else { "no" }`,
+ expected: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2},
+ Right: ast.NumberIntExpr{Value: 1},
+ },
+ Then: ast.StringExpr{Value: "yes"},
+ Else: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 36, Len: 2},
+ Right: ast.NumberIntExpr{Value: 2},
+ },
+ Then: ast.StringExpr{Value: "maybe"},
+ Else: ast.ConditionalExpr{
+ Cond: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 66, Len: 2},
+ Right: ast.NumberIntExpr{Value: 3},
+ },
+ Then: ast.StringExpr{Value: "probably"},
+ Else: ast.StringExpr{Value: "no"},
+ },
+ },
+ },
+ }.run)
+ })
+
+ t.Run("coalesce", func(t *testing.T) {
+ t.Run("chained on left side", happyTestCase{
+ input: `foo ?? bar ?? baz`,
+ expected: ast.BinaryExpr{
+ Left: ast.BinaryExpr{
+ Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}},
+ Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 4, Len: 2},
+ Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}},
+ },
+ Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2},
+ Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}},
+ },
+ }.run)
+
+ t.Run("chained nested on left side", happyTestCase{
+ input: `nested.one ?? nested.two ?? nested.three ?? 10`,
+ expected: ast.BinaryExpr{
+ Left: ast.BinaryExpr{
+ Left: ast.BinaryExpr{
+ Left: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}},
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "one"}},
+ ),
+ Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2},
+ Right: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}},
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "two"}},
+ ),
+ },
+ Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 25, Len: 2},
+ Right: ast.ChainExprs(
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}},
+ ast.PropertyExpr{Property: ast.StringExpr{Value: "three"}},
+ ),
+ },
+ Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 41, Len: 2},
+ Right: ast.NumberIntExpr{Value: 10},
+ },
+ }.run)
+ })
+}
diff --git a/selector_test.go b/selector_test.go
deleted file mode 100644
index 4b0e2bca..00000000
--- a/selector_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package dasel
-
-import (
- "errors"
- "reflect"
- "testing"
-)
-
-func collectAll(r SelectorResolver) ([]Selector, error) {
- res := make([]Selector, 0)
-
- for {
- s, err := r.Next()
- if err != nil {
- return res, err
- }
- if s == nil {
- break
- }
- res = append(res, *s)
- }
-
- return res, nil
-}
-
-func TestStandardSelectorResolver_Next(t *testing.T) {
- r := NewSelectorResolver("index(1).property(user).name.property(first,last?)", nil)
-
- got, err := collectAll(r)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- exp := []Selector{
- {
- funcName: "index",
- funcArgs: []string{"1"},
- },
- {
- funcName: "property",
- funcArgs: []string{"user"},
- },
- {
- funcName: "property",
- funcArgs: []string{"name"},
- },
- {
- funcName: "property",
- funcArgs: []string{"first", "last?"},
- },
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("exp: %v, got: %v", exp, got)
- }
-}
-
-func TestStandardSelectorResolver_Next_Nested(t *testing.T) {
- r := NewSelectorResolver("nested(a().b(),c(),d()).nested(a().b(),c(),d())", nil)
-
- got, err := collectAll(r)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- exp := []Selector{
- {
- funcName: "nested",
- funcArgs: []string{"a().b()", "c()", "d()"},
- },
- {
- funcName: "nested",
- funcArgs: []string{"a().b()", "c()", "d()"},
- },
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("exp: %v, got: %v", exp, got)
- }
-}
-
-func TestStandardSelectorResolver_Next_ExtraClosingBracket(t *testing.T) {
- r := NewSelectorResolver("all().filter(not(equal(x,true))))", nil)
-
- expErr := &ErrBadSelectorSyntax{
- Part: "filter(not(equal(x,true))))",
- Message: "too many closing brackets",
- }
-
- _, err := collectAll(r)
-
- if !errors.Is(err, expErr) {
- t.Errorf("expected error: %v, got %v", expErr, err)
- return
- }
-}
-
-func TestStandardSelectorResolver_Next_EscapedDot(t *testing.T) {
- r := NewSelectorResolver("plugins.io\\.containerd\\.grpc\\.v1\\.cri.registry", nil)
-
- got, err := collectAll(r)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- exp := []Selector{
- {
- funcName: "property",
- funcArgs: []string{"plugins"},
- },
- {
- funcName: "property",
- funcArgs: []string{"io.containerd.grpc.v1.cri"},
- },
- {
- funcName: "property",
- funcArgs: []string{"registry"},
- },
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("exp: %v, got: %v", exp, got)
- }
-}
-
-func TestStandardSelectorResolver_Next_EscapedEverything(t *testing.T) {
- r := NewSelectorResolver("a.b\\(\\.asdw\\\\\\].c(\\))", nil)
-
- got, err := collectAll(r)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- return
- }
-
- exp := []Selector{
- {
- funcName: "property",
- funcArgs: []string{"a"},
- },
- {
- funcName: "property",
- funcArgs: []string{"b(.asdw\\]"},
- },
- {
- funcName: "c",
- funcArgs: []string{")"},
- },
- }
-
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("exp: %v, got: %v", exp, got)
- }
-}
diff --git a/step.go b/step.go
deleted file mode 100644
index 8dcc667d..00000000
--- a/step.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package dasel
-
-// Step is a single step in the query.
-// Each function call has its own step.
-// Each value in the output is simply a pointer to the actual data point in the context data.
-type Step struct {
- context *Context
- selector Selector
- index int
- output Values
-}
-
-func (s *Step) Selector() Selector {
- return s.selector
-}
-
-func (s *Step) Index() int {
- return s.index
-}
-
-func (s *Step) Output() Values {
- return s.output
-}
-
-func (s *Step) execute() error {
- f, err := s.context.functions.Get(s.selector.funcName)
- if err != nil {
- return err
- }
- output, err := f(s.context, s, s.selector.funcArgs)
- s.output = output
- return err
-}
-
-func (s *Step) inputs() Values {
- prevStep := s.context.Step(s.index - 1)
- if prevStep == nil {
- return Values{}
- }
- return prevStep.output
-}
diff --git a/storage/colourise.go b/storage/colourise.go
deleted file mode 100644
index 48ee712d..00000000
--- a/storage/colourise.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package storage
-
-import (
- "bytes"
- "github.com/alecthomas/chroma/v2/quick"
-)
-
-// ColouriseStyle is the style used when colourising output.
-const ColouriseStyle = "solarized-dark256"
-
-// ColouriseFormatter is the formatter used when colourising output.
-const ColouriseFormatter = "terminal"
-
-// ColouriseBuffer colourises the given buffer in-place.
-func ColouriseBuffer(content *bytes.Buffer, lexer string) error {
- contentString := content.String()
- content.Reset()
- return quick.Highlight(content, contentString, lexer, ColouriseFormatter, ColouriseStyle)
-}
-
-// Colourise colourises the given string.
-func Colourise(content string, lexer string) (*bytes.Buffer, error) {
- buf := new(bytes.Buffer)
- return buf, quick.Highlight(buf, content, lexer, ColouriseFormatter, ColouriseStyle)
-}
diff --git a/storage/csv.go b/storage/csv.go
deleted file mode 100644
index e89ad6b1..00000000
--- a/storage/csv.go
+++ /dev/null
@@ -1,271 +0,0 @@
-package storage
-
-import (
- "bytes"
- "encoding/csv"
- "fmt"
- "sort"
-
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/util"
-)
-
-func init() {
- registerReadParser([]string{"csv"}, []string{".csv"}, &CSVParser{})
- registerWriteParser([]string{"csv"}, []string{".csv"}, &CSVParser{})
-}
-
-// CSVParser is a Parser implementation to handle csv files.
-type CSVParser struct {
-}
-
-// CSVDocument represents a CSV file.
-// This is required to keep headers in the expected order.
-type CSVDocument struct {
- Value []map[string]interface{}
- Headers []string
-}
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *CSVParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- if byteData == nil {
- return dasel.Value{}, fmt.Errorf("could not read csv file: no data")
- }
-
- reader := csv.NewReader(bytes.NewBuffer(byteData))
-
- for _, o := range options {
- switch o.Key {
- case OptionCSVComma:
- if value, ok := o.Value.(rune); ok {
- reader.Comma = value
- }
- case OptionCSVComment:
- if value, ok := o.Value.(rune); ok {
- reader.Comment = value
- }
- }
- }
-
- res := make([]*dencoding.Map, 0)
- records, err := reader.ReadAll()
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not read csv file: %w", err)
- }
- if len(records) == 0 {
- return dasel.Value{}, nil
- }
- var headers []string
- for i, row := range records {
- if i == 0 {
- headers = row
- continue
- }
- rowRes := dencoding.NewMap()
- allEmpty := true
- for index, val := range row {
- if val != "" {
- allEmpty = false
- }
- rowRes.Set(headers[index], val)
- }
- if !allEmpty {
- res = append(res, rowRes)
- }
- }
-
- return dasel.ValueOf(res).
- WithMetadata("csvHeaders", headers), nil
-}
-
-func interfaceToCSVDocument(val interface{}) (*CSVDocument, error) {
- switch v := val.(type) {
- case map[string]interface{}:
- headers := make([]string, 0)
- for k := range v {
- headers = append(headers, k)
- }
- sort.Strings(headers)
- return &CSVDocument{
- Value: []map[string]interface{}{v},
- Headers: headers,
- }, nil
-
- case *dencoding.Map:
- return &CSVDocument{
- Value: []map[string]any{v.UnorderedData()},
- Headers: v.Keys(),
- }, nil
-
- case []interface{}:
- mapVals := make([]map[string]interface{}, 0)
- headers := make([]string, 0)
- for _, val := range v {
- switch x := val.(type) {
- case map[string]any:
- mapVals = append(mapVals, x)
- for objectKey := range x {
- found := false
- for _, existingHeader := range headers {
- if existingHeader == objectKey {
- found = true
- break
- }
- }
- if !found {
- headers = append(headers, objectKey)
- }
- }
- case *dencoding.Map:
- mapVals = append(mapVals, x.UnorderedData())
- for _, objectKey := range x.Keys() {
- found := false
- for _, existingHeader := range headers {
- if existingHeader == objectKey {
- found = true
- break
- }
- }
- if !found {
- headers = append(headers, objectKey)
- }
- }
- }
- }
- sort.Strings(headers)
- return &CSVDocument{
- Value: mapVals,
- Headers: headers,
- }, nil
-
- case []*dencoding.Map:
- mapVals := make([]map[string]interface{}, 0)
- headers := make([]string, 0)
- for _, val := range v {
- mapVals = append(mapVals, val.UnorderedData())
- for _, objectKey := range val.Keys() {
- found := false
- for _, existingHeader := range headers {
- if existingHeader == objectKey {
- found = true
- break
- }
- }
- if !found {
- headers = append(headers, objectKey)
- }
- }
- }
- sort.Strings(headers)
- return &CSVDocument{
- Value: mapVals,
- Headers: headers,
- }, nil
-
- default:
- return nil, fmt.Errorf("CSVParser.toBytes cannot handle type %T", val)
- }
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *CSVParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- buffer := new(bytes.Buffer)
- writer := csv.NewWriter(buffer)
-
- for _, o := range options {
- switch o.Key {
- case OptionCSVComma:
- if value, ok := o.Value.(rune); ok {
- writer.Comma = value
- }
- case OptionCSVComment:
- if value, ok := o.Value.(bool); ok {
- writer.UseCRLF = value
- }
- }
- }
-
- // Allow for multi document output by just appending documents on the end of each other.
- // This is really only supported so as we have nicer output when converting to CSV from
- // other multi-document formats.
-
- docs := make([]*CSVDocument, 0)
-
- // headers, _ := value.Metadata("csvHeaders").([]string)
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- doc, err := interfaceToCSVDocument(value.Interface())
- if err != nil {
- return nil, err
- }
- docs = append(docs, doc)
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- doc, err := interfaceToCSVDocument(value.Index(i).Interface())
- if err != nil {
- return nil, err
- }
- docs = append(docs, doc)
- }
- default:
- doc, err := interfaceToCSVDocument(value.Interface())
- if err != nil {
- return nil, err
- }
- docs = append(docs, doc)
- }
-
- for _, doc := range docs {
- if err := p.toBytesHandleDoc(writer, doc); err != nil {
- return nil, err
- }
- }
-
- return buffer.Bytes(), nil
-}
-
-func (p *CSVParser) toBytesHandleDoc(writer *csv.Writer, doc *CSVDocument) error {
- // Iterate through the rows and detect any new headers.
- for _, r := range doc.Value {
- for k := range r {
- headerExists := false
- for _, header := range doc.Headers {
- if k == header {
- headerExists = true
- break
- }
- }
- if !headerExists {
- doc.Headers = append(doc.Headers, k)
- }
- }
- }
-
- // Iterate through the rows and write the output.
- for i, r := range doc.Value {
- if i == 0 {
- if err := writer.Write(doc.Headers); err != nil {
- return fmt.Errorf("could not write headers: %w", err)
- }
- }
-
- values := make([]string, 0)
- for _, header := range doc.Headers {
- val, ok := r[header]
- if !ok {
- val = ""
- }
- values = append(values, util.ToString(val))
- }
-
- if err := writer.Write(values); err != nil {
- return fmt.Errorf("could not write values: %w", err)
- }
-
- writer.Flush()
- }
-
- return nil
-}
diff --git a/storage/csv_test.go b/storage/csv_test.go
deleted file mode 100644
index a8eed7f0..00000000
--- a/storage/csv_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package storage_test
-
-import (
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/storage"
- "reflect"
- "testing"
-)
-
-var csvBytes = []byte(`id,name
-1,Tom
-2,Jim
-`)
-var csvMap = []*dencoding.Map{
- dencoding.NewMap().
- Set("id", "1").
- Set("name", "Tom"),
- dencoding.NewMap().
- Set("id", "2").
- Set("name", "Jim"),
-}
-
-func TestCSVParser_FromBytes(t *testing.T) {
- got, err := (&storage.CSVParser{}).FromBytes(csvBytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := dasel.ValueOf(csvMap).WithMetadata("csvHeaders", []string{"id", "name"})
- if !reflect.DeepEqual(exp.Interface(), got.Interface()) {
- t.Errorf("expected %v, got %v", exp, got)
- }
-}
-
-func TestCSVParser_FromBytes_Error(t *testing.T) {
- _, err := (&storage.CSVParser{}).FromBytes(nil)
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
- _, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b
-a,b,c`))
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
- _, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b,c
-a,b`))
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
-}
-
-func TestCSVParser_ToBytes(t *testing.T) {
- t.Run("SingleDocument", func(t *testing.T) {
- value := dasel.ValueOf(map[string]interface{}{
- "id": "1",
- "name": "Tom",
- }).
- WithMetadata("isSingleDocument", true)
- got, err := (&storage.CSVParser{}).ToBytes(value)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- deepEqualOneOf(t, got, []byte(`id,name
-1,Tom
-`), []byte(`name,id
-Tom,1
-`))
- })
- t.Run("SingleDocumentSlice", func(t *testing.T) {
- value := dasel.ValueOf([]interface{}{
- map[string]interface{}{
- "id": "1",
- "name": "Tom",
- },
- map[string]interface{}{
- "id": "2",
- "name": "Tommy",
- },
- }).
- WithMetadata("isSingleDocument", true)
- got, err := (&storage.CSVParser{}).ToBytes(value)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- deepEqualOneOf(t, got, []byte(`id,name
-1,Tom
-2,Tommy
-`), []byte(`name,id
-Tom,1
-`))
- })
- t.Run("MultiDocument", func(t *testing.T) {
- value := dasel.ValueOf([]interface{}{
- map[string]interface{}{
- "id": "1",
- "name": "Tom",
- },
- map[string]interface{}{
- "id": "2",
- "name": "Jim",
- },
- }).
- WithMetadata("isMultiDocument", true)
- got, err := (&storage.CSVParser{}).ToBytes(value)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- deepEqualOneOf(t, got, []byte(`id,name
-1,Tom
-id,name
-2,Jim
-`), []byte(`name,id
-Tom,1
-id,name
-2,Jim
-`), []byte(`id,name
-1,Tom
-name,id
-Jim,2
-`), []byte(`name,id
-Tom,1
-name,id
-Jim,2
-`))
- })
-}
-
-func deepEqualOneOf(t *testing.T, got []byte, exps ...[]byte) {
- for _, exp := range exps {
- if reflect.DeepEqual(exp, got) {
- return
- }
- }
- t.Errorf("%s did not match any of the expected values", string(got))
-}
diff --git a/storage/json.go b/storage/json.go
deleted file mode 100644
index 115a8851..00000000
--- a/storage/json.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package storage
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "io"
-)
-
-func init() {
- registerReadParser([]string{"json"}, []string{".json"}, &JSONParser{})
- registerWriteParser([]string{"json"}, []string{".json"}, &JSONParser{})
-}
-
-// JSONParser is a Parser implementation to handle json files.
-type JSONParser struct {
-}
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *JSONParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- res := make([]any, 0)
-
- decoder := dencoding.NewJSONDecoder(bytes.NewReader(byteData))
-
-docLoop:
- for {
- var docData any
- if err := decoder.Decode(&docData); err != nil {
- if err == io.EOF {
- break docLoop
- }
- return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
- } else {
- res = append(res, docData)
- }
- }
-
- switch len(res) {
- case 0:
- return dasel.Value{}, nil
- case 1:
- return dasel.ValueOf(res[0]).
- WithMetadata("isSingleDocument", true), nil
- default:
- return dasel.ValueOf(res).
- WithMetadata("isMultiDocument", true), nil
- }
-}
-
-type toBytesOptions struct {
- indent string
- prefix string
- prettyPrint bool
- colourise bool
- escapeHTML bool
-}
-
-func getToBytesOptions(options ...ReadWriteOption) toBytesOptions {
- res := toBytesOptions{
- indent: " ",
- prefix: "",
- prettyPrint: true,
- colourise: false,
- escapeHTML: false,
- }
-
- for _, o := range options {
- switch o.Key {
- case OptionIndent:
- if value, ok := o.Value.(string); ok {
- res.indent = value
- }
- case OptionPrettyPrint:
- if value, ok := o.Value.(bool); ok {
- res.prettyPrint = value
- }
- case OptionColourise:
- if value, ok := o.Value.(bool); ok {
- res.colourise = value
- }
- case OptionEscapeHTML:
- if value, ok := o.Value.(bool); ok {
- res.escapeHTML = value
- }
- }
- }
-
- if !res.prettyPrint {
- res.indent = ""
- }
-
- return res
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *JSONParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- encoderOptions := make([]dencoding.JSONEncoderOption, 0)
-
- baseOptions := getToBytesOptions(options...)
- encoderOptions = append(encoderOptions, dencoding.JSONEscapeHTML(baseOptions.escapeHTML))
- encoderOptions = append(encoderOptions, dencoding.JSONEncodeIndent(baseOptions.prefix, baseOptions.indent))
-
- buffer := new(bytes.Buffer)
- encoder := dencoding.NewJSONEncoder(buffer, encoderOptions...)
- defer encoder.Close()
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode single document: %w", err)
- }
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- if err := encoder.Encode(value.Index(i).Interface()); err != nil {
- return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err)
- }
- }
- default:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode default document type: %w", err)
- }
- }
-
- if baseOptions.colourise {
- if err := ColouriseBuffer(buffer, "json"); err != nil {
- return nil, fmt.Errorf("could not colourise output: %w", err)
- }
- }
-
- return buffer.Bytes(), nil
-}
diff --git a/storage/json_test.go b/storage/json_test.go
deleted file mode 100644
index 95473153..00000000
--- a/storage/json_test.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package storage_test
-
-import (
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/storage"
- "reflect"
- "testing"
-)
-
-var jsonBytes = []byte(`{
- "name": "Tom"
-}
-`)
-var jsonMap = dencoding.NewMap().Set("name", "Tom")
-
-func TestJSONParser_FromBytes(t *testing.T) {
- t.Run("Valid", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).FromBytes(jsonBytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := jsonMap
- if !reflect.DeepEqual(exp, got.Interface()) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- t.Run("ValidMultiDocument", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMulti)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := jsonMapMulti
- if !reflect.DeepEqual(exp, got.Interface()) {
- t.Errorf("expected %v, got %v", jsonMap, got)
- }
- })
- t.Run("ValidMultiDocumentMixed", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMultiMixed)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := jsonMapMultiMixed
- if !reflect.DeepEqual(exp, got.Interface()) {
- t.Errorf("expected %v, got %v", jsonMap, got)
- }
- })
- t.Run("Empty", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).FromBytes([]byte(``))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(dasel.Value{}, got) {
- t.Errorf("expected %v, got %v", nil, got)
- }
- })
-}
-
-func TestJSONParser_FromBytes_Error(t *testing.T) {
- _, err := (&storage.JSONParser{}).FromBytes(yamlBytes)
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
-}
-
-func TestJSONParser_ToBytes(t *testing.T) {
- t.Run("Valid", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(jsonBytes) != string(got) {
- t.Errorf("expected %v, got %v", string(jsonBytes), string(got))
- }
- })
-
- t.Run("ValidSingle", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(jsonBytes) != string(got) {
- t.Errorf("expected %v, got %v", string(jsonBytes), string(got))
- }
- })
-
- t.Run("ValidSingleNoPrettyPrint", func(t *testing.T) {
- res, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.PrettyPrintOption(false))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- got := string(res)
- exp := `{"name":"Tom"}
-`
- if exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- t.Run("ValidSingleColourise", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- expBuf, _ := storage.Colourise(`{
- "name": "Tom"
-}
-`, "json")
- exp := expBuf.Bytes()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- t.Run("ValidSingleCustomIndent", func(t *testing.T) {
- res, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.IndentOption(" "))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- got := string(res)
- exp := `{
- "name": "Tom"
-}
-`
- if exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
-
- t.Run("ValidMulti", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMapMulti).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(jsonBytesMulti) != string(got) {
- t.Errorf("expected %v, got %v", string(jsonBytesMulti), string(got))
- }
- })
-
- t.Run("ValidMultiMixed", func(t *testing.T) {
- got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMapMultiMixed).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(jsonBytesMultiMixed) != string(got) {
- t.Errorf("expected %v, got %v", string(jsonBytesMultiMixed), string(got))
- }
- })
-}
-
-var jsonBytesMulti = []byte(`{
- "name": "Tom"
-}
-{
- "name": "Ellis"
-}
-`)
-
-var jsonMapMulti = []any{
- dencoding.NewMap().Set("name", "Tom"),
- dencoding.NewMap().Set("name", "Ellis"),
-}
-
-var jsonBytesMultiMixed = []byte(`{
- "name": "Tom",
- "other": true
-}
-{
- "name": "Ellis"
-}
-`)
-
-var jsonMapMultiMixed = []interface{}{
- dencoding.NewMap().Set("name", "Tom").Set("other", true),
- dencoding.NewMap().Set("name", "Ellis"),
-}
diff --git a/storage/option.go b/storage/option.go
deleted file mode 100644
index 4a625ff6..00000000
--- a/storage/option.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package storage
-
-// IndentOption returns a write option that sets the given indent.
-func IndentOption(indent string) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionIndent,
- Value: indent,
- }
-}
-
-// PrettyPrintOption returns an option that enables or disables pretty printing.
-func PrettyPrintOption(enabled bool) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionPrettyPrint,
- Value: enabled,
- }
-}
-
-// ColouriseOption returns an option that enables or disables colourised output.
-func ColouriseOption(enabled bool) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionColourise,
- Value: enabled,
- }
-}
-
-// EscapeHTMLOption returns an option that enables or disables HTML escaping.
-func EscapeHTMLOption(enabled bool) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionEscapeHTML,
- Value: enabled,
- }
-}
-
-// CsvCommaOption returns an option that modifies the separator character for CSV files.
-func CsvCommaOption(comma rune) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionCSVComma,
- Value: comma,
- }
-}
-
-// CsvCommentOption returns an option that modifies the comment character for CSV files.
-func CsvCommentOption(comma rune) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionCSVComment,
- Value: comma,
- }
-}
-
-// CsvUseCRLFOption returns an option that modifies the comment character for CSV files.
-func CsvUseCRLFOption(enabled bool) ReadWriteOption {
- return ReadWriteOption{
- Key: OptionCSVUseCRLF,
- Value: enabled,
- }
-}
-
-// OptionKey is a defined type for keys within a ReadWriteOption.
-type OptionKey string
-
-const (
- // OptionIndent is the key used with IndentOption.
- OptionIndent OptionKey = "indent"
- // OptionPrettyPrint is the key used with PrettyPrintOption.
- OptionPrettyPrint OptionKey = "prettyPrint"
- // OptionColourise is the key used with ColouriseOption.
- OptionColourise OptionKey = "colourise"
- // OptionEscapeHTML is the key used with EscapeHTMLOption.
- OptionEscapeHTML OptionKey = "escapeHtml"
- // OptionCSVComma is the key used with CsvCommaOption.
- OptionCSVComma OptionKey = "csvComma"
- // OptionCSVComment is the key used with CsvCommentOption.
- OptionCSVComment OptionKey = "csvComment"
- // OptionCSVUseCRLF is the key used with CsvUseCRLFOption.
- OptionCSVUseCRLF OptionKey = "csvUseCRLF"
-)
-
-// ReadWriteOption is an option to be used when writing.
-type ReadWriteOption struct {
- Key OptionKey
- Value interface{}
-}
diff --git a/storage/parser.go b/storage/parser.go
deleted file mode 100644
index 4aea06e8..00000000
--- a/storage/parser.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package storage
-
-import (
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/tomwright/dasel/v2"
-)
-
-var readParsersByExtension = map[string]ReadParser{}
-var writeParsersByExtension = map[string]WriteParser{}
-var readParsersByName = map[string]ReadParser{}
-var writeParsersByName = map[string]WriteParser{}
-
-func registerReadParser(names []string, extensions []string, parser ReadParser) {
- for _, n := range names {
- readParsersByName[n] = parser
- }
- for _, e := range extensions {
- readParsersByExtension[e] = parser
- }
-}
-
-func registerWriteParser(names []string, extensions []string, parser WriteParser) {
- for _, n := range names {
- writeParsersByName[n] = parser
- }
- for _, e := range extensions {
- writeParsersByExtension[e] = parser
- }
-}
-
-// UnknownParserErr is returned when an invalid parser name is given.
-type UnknownParserErr struct {
- Parser string
-}
-
-func (e UnknownParserErr) Is(other error) bool {
- _, ok := other.(*UnknownParserErr)
- return ok
-}
-
-// Error returns the error message.
-func (e UnknownParserErr) Error() string {
- return fmt.Sprintf("unknown parser: %s", e.Parser)
-}
-
-// ReadParser can be used to convert bytes to data.
-type ReadParser interface {
- // FromBytes returns some data that is represented by the given bytes.
- FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error)
-}
-
-// WriteParser can be used to convert data to bytes.
-type WriteParser interface {
- // ToBytes returns a slice of bytes that represents the given value.
- ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error)
-}
-
-// Parser can be used to load and save files from/to disk.
-type Parser interface {
- ReadParser
- WriteParser
-}
-
-// NewReadParserFromFilename returns a ReadParser from the given filename.
-func NewReadParserFromFilename(filename string) (ReadParser, error) {
- ext := strings.ToLower(filepath.Ext(filename))
- p, ok := readParsersByExtension[ext]
- if !ok {
- return nil, &UnknownParserErr{Parser: ext}
- }
- return p, nil
-}
-
-// NewReadParserFromString returns a ReadParser from the given parser name.
-func NewReadParserFromString(parser string) (ReadParser, error) {
- p, ok := readParsersByName[parser]
- if !ok {
- return nil, &UnknownParserErr{Parser: parser}
- }
- return p, nil
-}
-
-// NewWriteParserFromFilename returns a WriteParser from the given filename.
-func NewWriteParserFromFilename(filename string) (WriteParser, error) {
- ext := strings.ToLower(filepath.Ext(filename))
- p, ok := writeParsersByExtension[ext]
- if !ok {
- return nil, &UnknownParserErr{Parser: ext}
- }
- return p, nil
-}
-
-// NewWriteParserFromString returns a WriteParser from the given parser name.
-func NewWriteParserFromString(parser string) (WriteParser, error) {
- p, ok := writeParsersByName[parser]
- if !ok {
- return nil, &UnknownParserErr{Parser: parser}
- }
- return p, nil
-}
-
-// LoadFromFile loads data from the given file.
-func LoadFromFile(filename string, p ReadParser, options ...ReadWriteOption) (dasel.Value, error) {
- f, err := os.Open(filename)
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not open file for reading: %w", err)
- }
- return Load(p, f, options...)
-}
-
-// Load loads data from the given io.Reader.
-func Load(p ReadParser, reader io.Reader, options ...ReadWriteOption) (dasel.Value, error) {
- byteData, err := io.ReadAll(reader)
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not read data: %w", err)
- }
- return p.FromBytes(byteData, options...)
-}
-
-// Write writes the value to the given io.Writer.
-func Write(p WriteParser, value dasel.Value, writer io.Writer, options ...ReadWriteOption) error {
- byteData, err := p.ToBytes(value, options...)
- if err != nil {
- return fmt.Errorf("could not get byte data for file: %w", err)
- }
- if _, err := writer.Write(byteData); err != nil {
- return fmt.Errorf("could not write data: %w", err)
- }
- return nil
-}
diff --git a/storage/parser_test.go b/storage/parser_test.go
deleted file mode 100644
index 0a29ea51..00000000
--- a/storage/parser_test.go
+++ /dev/null
@@ -1,286 +0,0 @@
-package storage_test
-
-import (
- "bytes"
- "errors"
- "reflect"
- "strings"
- "testing"
-
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/storage"
-)
-
-func TestUnknownParserErr_Error(t *testing.T) {
- if exp, got := "unknown parser: x", (&storage.UnknownParserErr{Parser: "x"}).Error(); exp != got {
- t.Errorf("expected error %s, got %s", exp, got)
- }
-}
-
-func TestNewReadParserFromString(t *testing.T) {
- tests := []struct {
- In string
- Out storage.Parser
- Err error
- }{
- {In: "json", Out: &storage.JSONParser{}},
- {In: "yaml", Out: &storage.YAMLParser{}},
- {In: "yml", Out: &storage.YAMLParser{}},
- {In: "toml", Out: &storage.TOMLParser{}},
- {In: "xml", Out: &storage.XMLParser{}},
- {In: "csv", Out: &storage.CSVParser{}},
- {In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}},
- {In: "-", Out: nil, Err: &storage.UnknownParserErr{Parser: "-"}},
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run(tc.In, func(t *testing.T) {
- got, err := storage.NewReadParserFromString(tc.In)
- if tc.Err == nil && err != nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err == nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Out != got {
- t.Errorf("expected result %v, got %v", tc.Out, got)
- }
- })
- }
-}
-
-func TestNewWriteParserFromString(t *testing.T) {
- tests := []struct {
- In string
- Out storage.Parser
- Err error
- }{
- {In: "json", Out: &storage.JSONParser{}},
- {In: "yaml", Out: &storage.YAMLParser{}},
- {In: "yml", Out: &storage.YAMLParser{}},
- {In: "toml", Out: &storage.TOMLParser{}},
- {In: "xml", Out: &storage.XMLParser{}},
- {In: "csv", Out: &storage.CSVParser{}},
- {In: "-", Out: &storage.PlainParser{}},
- {In: "plain", Out: &storage.PlainParser{}},
- {In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}},
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run(tc.In, func(t *testing.T) {
- got, err := storage.NewWriteParserFromString(tc.In)
- if tc.Err == nil && err != nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err == nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Out != got {
- t.Errorf("expected result %v, got %v", tc.Out, got)
- }
- })
- }
-}
-
-func TestNewReadParserFromFilename(t *testing.T) {
- tests := []struct {
- In string
- Out storage.Parser
- Err error
- }{
- {In: "a.json", Out: &storage.JSONParser{}},
- {In: "a.yaml", Out: &storage.YAMLParser{}},
- {In: "a.yml", Out: &storage.YAMLParser{}},
- {In: "a.toml", Out: &storage.TOMLParser{}},
- {In: "a.xml", Out: &storage.XMLParser{}},
- {In: "a.csv", Out: &storage.CSVParser{}},
- {In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}},
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run(tc.In, func(t *testing.T) {
- got, err := storage.NewReadParserFromFilename(tc.In)
- if tc.Err == nil && err != nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err == nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Out != got {
- t.Errorf("expected result %v, got %v", tc.Out, got)
- }
- })
- }
-}
-
-func TestNewWriteParserFromFilename(t *testing.T) {
- tests := []struct {
- In string
- Out storage.Parser
- Err error
- }{
- {In: "a.json", Out: &storage.JSONParser{}},
- {In: "a.yaml", Out: &storage.YAMLParser{}},
- {In: "a.yml", Out: &storage.YAMLParser{}},
- {In: "a.toml", Out: &storage.TOMLParser{}},
- {In: "a.xml", Out: &storage.XMLParser{}},
- {In: "a.csv", Out: &storage.CSVParser{}},
- {In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}},
- }
-
- for _, testCase := range tests {
- tc := testCase
- t.Run(tc.In, func(t *testing.T) {
- got, err := storage.NewWriteParserFromFilename(tc.In)
- if tc.Err == nil && err != nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err == nil {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
- t.Errorf("expected err %v, got %v", tc.Err, err)
- return
- }
- if tc.Out != got {
- t.Errorf("expected result %v, got %v", tc.Out, got)
- }
- })
- }
-}
-
-var jsonData = dencoding.NewMap().
- Set("name", "Tom").
- Set("preferences", dencoding.NewMap().
- Set("favouriteColour", "red"),
- ).
- Set("colours", []any{"red", "green", "blue"}).
- Set("colourCodes", []any{
- dencoding.NewMap().
- Set("name", "red").
- Set("rgb", "ff0000"),
- dencoding.NewMap().
- Set("name", "green").
- Set("rgb", "00ff00"),
- dencoding.NewMap().
- Set("name", "blue").
- Set("rgb", "0000ff"),
- })
-
-func TestLoadFromFile(t *testing.T) {
- t.Run("ValidJSON", func(t *testing.T) {
- data, err := storage.LoadFromFile("../tests/assets/example.json", &storage.JSONParser{})
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := jsonData.KeyValues()
- got := data.Interface().(*dencoding.Map).KeyValues()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("data does not match: exp %v, got %v", exp, got)
- }
- })
- t.Run("BaseFilePath", func(t *testing.T) {
- _, err := storage.LoadFromFile("x.json", &storage.JSONParser{})
- if err == nil || !strings.Contains(err.Error(), "could not open file for reading") {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
-}
-
-func TestLoad(t *testing.T) {
- t.Run("ReaderErrHandled", func(t *testing.T) {
- if _, err := storage.Load(&storage.JSONParser{}, &failingReader{}); !errors.Is(err, errFailingReaderErr) {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
-}
-
-var errFailingParserErr = errors.New("i am meant to fail at parsing")
-
-type failingParser struct {
-}
-
-func (fp *failingParser) FromBytes(_ []byte) (dasel.Value, error) {
- return dasel.Value{}, errFailingParserErr
-}
-
-func (fp *failingParser) ToBytes(_ dasel.Value, options ...storage.ReadWriteOption) ([]byte, error) {
- return nil, errFailingParserErr
-}
-
-var errFailingWriterErr = errors.New("i am meant to fail at writing")
-
-type failingWriter struct {
-}
-
-func (fp *failingWriter) Write(_ []byte) (int, error) {
- return 0, errFailingWriterErr
-}
-
-var errFailingReaderErr = errors.New("i am meant to fail at reading")
-
-type failingReader struct {
-}
-
-func (fp *failingReader) Read(_ []byte) (n int, err error) {
- return 0, errFailingReaderErr
-}
-
-func TestWrite(t *testing.T) {
- t.Run("Success", func(t *testing.T) {
- var buf bytes.Buffer
- if err := storage.Write(&storage.JSONParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &buf); err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
-
- if exp, got := `{
- "name": "Tom"
-}
-`, buf.String(); exp != got {
- t.Errorf("unexpected output:\n%s\ngot:\n%s", exp, got)
- }
- })
-
- t.Run("ParserErrHandled", func(t *testing.T) {
- var buf bytes.Buffer
- if err := storage.Write(&failingParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &buf); !errors.Is(err, errFailingParserErr) {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
-
- t.Run("WriterErrHandled", func(t *testing.T) {
- if err := storage.Write(&storage.JSONParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &failingWriter{}); !errors.Is(err, errFailingWriterErr) {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
-}
diff --git a/storage/plain.go b/storage/plain.go
deleted file mode 100644
index a0d1f333..00000000
--- a/storage/plain.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package storage
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
-)
-
-func init() {
- registerWriteParser([]string{"-", "plain"}, []string{}, &PlainParser{})
-}
-
-// PlainParser is a Parser implementation to handle plain files.
-type PlainParser struct {
-}
-
-// ErrPlainParserNotImplemented is returned when you try to use the PlainParser.FromBytes func.
-var ErrPlainParserNotImplemented = fmt.Errorf("PlainParser.FromBytes not implemented")
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *PlainParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- return dasel.Value{}, ErrPlainParserNotImplemented
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *PlainParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- buf := new(bytes.Buffer)
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface())))
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- buf.Write([]byte(fmt.Sprintf("%v\n", value.Index(i).Interface())))
- }
- default:
- buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface())))
- }
-
- return buf.Bytes(), nil
-}
diff --git a/storage/plain_test.go b/storage/plain_test.go
deleted file mode 100644
index 93e50c4f..00000000
--- a/storage/plain_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package storage_test
-
-import (
- "errors"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/storage"
- "testing"
-)
-
-func TestPlainParser_FromBytes(t *testing.T) {
- _, err := (&storage.PlainParser{}).FromBytes(nil)
- if !errors.Is(err, storage.ErrPlainParserNotImplemented) {
- t.Errorf("unexpected error: %v", err)
- }
-}
-
-func TestPlainParser_ToBytes(t *testing.T) {
- t.Run("Basic", func(t *testing.T) {
- gotVal, err := (&storage.PlainParser{}).ToBytes(dasel.ValueOf("asd"))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `asd
-`
- got := string(gotVal)
- if exp != got {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- t.Run("SingleDocument", func(t *testing.T) {
- gotVal, err := (&storage.PlainParser{}).ToBytes(dasel.ValueOf("asd").WithMetadata("isSingleDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `asd
-`
- got := string(gotVal)
- if exp != got {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- t.Run("MultiDocument", func(t *testing.T) {
- val := dasel.ValueOf([]interface{}{"asd", "123"})
- daselVal := dasel.ValueOf(val).WithMetadata("isMultiDocument", true)
-
- gotVal, err := (&storage.PlainParser{}).ToBytes(daselVal)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `asd
-123
-`
- got := string(gotVal)
- if exp != got {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
-}
diff --git a/storage/toml.go b/storage/toml.go
deleted file mode 100644
index 14266b47..00000000
--- a/storage/toml.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package storage
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "io"
-)
-
-func init() {
- registerReadParser([]string{"toml"}, []string{".toml"}, &TOMLParser{})
- registerWriteParser([]string{"toml"}, []string{".toml"}, &TOMLParser{})
-}
-
-// TOMLParser is a Parser implementation to handle toml files.
-type TOMLParser struct {
-}
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *TOMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- res := make([]interface{}, 0)
-
- decoder := dencoding.NewTOMLDecoder(bytes.NewReader(byteData))
-
-docLoop:
- for {
- var docData interface{}
- if err := decoder.Decode(&docData); err != nil {
- if err == io.EOF {
- break docLoop
- }
- return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
- }
-
- formattedDocData := cleanupYamlMapValue(docData)
-
- res = append(res, formattedDocData)
- }
- switch len(res) {
- case 0:
- return dasel.Value{}, nil
- case 1:
- return dasel.ValueOf(res[0]).WithMetadata("isSingleDocument", true), nil
- default:
- return dasel.ValueOf(res).WithMetadata("isMultiDocument", true), nil
- }
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *TOMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- buffer := new(bytes.Buffer)
-
- colourise := false
-
- encoderOptions := make([]dencoding.TOMLEncoderOption, 0)
-
- for _, o := range options {
- switch o.Key {
- case OptionColourise:
- if value, ok := o.Value.(bool); ok {
- colourise = value
- }
- case OptionIndent:
- if value, ok := o.Value.(string); ok {
- encoderOptions = append(encoderOptions, dencoding.TOMLIndentSymbol(value))
- }
- }
- }
-
- encoder := dencoding.NewTOMLEncoder(buffer, encoderOptions...)
- defer encoder.Close()
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode single document: %w", err)
- }
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- if err := encoder.Encode(value.Index(i).Interface()); err != nil {
- return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err)
- }
- }
- default:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode default document type: %w", err)
- }
- }
-
- if colourise {
- if err := ColouriseBuffer(buffer, "toml"); err != nil {
- return nil, fmt.Errorf("could not colourise output: %w", err)
- }
- }
-
- return buffer.Bytes(), nil
-}
diff --git a/storage/toml_test.go b/storage/toml_test.go
deleted file mode 100644
index 6687f7b1..00000000
--- a/storage/toml_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package storage_test
-
-import (
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/storage"
- "reflect"
- "strings"
- "testing"
-)
-
-var tomlBytes = []byte(`names = ['John', 'Frank']
-
-[person]
-name = 'Tom'
-`)
-var tomlMap = map[string]interface{}{
- "person": map[string]interface{}{
- "name": "Tom",
- },
- "names": []interface{}{"John", "Frank"},
-}
-
-func TestTOMLParser_FromBytes(t *testing.T) {
- t.Run("Valid", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).FromBytes(tomlBytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(tomlMap, got.Interface()) {
- t.Errorf("expected %v, got %v", tomlMap, got)
- }
- })
- t.Run("Invalid", func(t *testing.T) {
- _, err := (&storage.TOMLParser{}).FromBytes([]byte(`x:x`))
- if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
-}
-
-func TestTOMLParser_ToBytes(t *testing.T) {
- t.Run("Default", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(tomlBytes) != string(got) {
- t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", tomlBytes, got)
- }
- })
- t.Run("SingleDocument", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(tomlBytes) != string(got) {
- t.Errorf("expected:\n%s\ngot:\n%s", tomlBytes, got)
- }
- })
- t.Run("SingleDocumentColourise", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
-
- expBuf, _ := storage.Colourise(string(tomlBytes), "toml")
- exp := expBuf.Bytes()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", string(exp), string(got))
- }
- })
- t.Run("SingleDocumentCustomIndent", func(t *testing.T) {
- res, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true), storage.IndentOption(" "))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- got := string(res)
- exp := `names = ['John', 'Frank']
-
-[person]
- name = 'Tom'
-`
- if exp != got {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- t.Run("MultiDocument", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{tomlMap, tomlMap}).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := append([]byte{}, tomlBytes...)
- exp = append(exp, tomlBytes...)
- if string(exp) != string(got) {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- t.Run("SingleDocumentValue", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf("asd"))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `'asd'
-`
- if exp != string(got) {
- t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", exp, got)
- }
- })
- t.Run("DefaultValue", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf("asd"))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `'asd'
-`
- if exp != string(got) {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- t.Run("MultiDocumentValue", func(t *testing.T) {
- got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", 123}).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := `'asd'
-123
-`
- if exp != string(got) {
- t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- }
- })
- // t.Run("time.Time", func(t *testing.T) {
- // v, _ := time.Parse(time.RFC3339, "2022-01-02T12:34:56Z")
- // got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(v))
- // if err != nil {
- // t.Errorf("unexpected error: %s", err)
- // return
- // }
- // exp := `2022-01-02T12:34:56Z
- // `
- // if exp != string(got) {
- // t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
- // }
- // })
-}
diff --git a/storage/xml.go b/storage/xml.go
deleted file mode 100644
index 34b98491..00000000
--- a/storage/xml.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package storage
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "strings"
-
- "github.com/clbanning/mxj/v2"
- "golang.org/x/net/html/charset"
-)
-
-func init() {
- // Required for https://github.com/TomWright/dasel/issues/61
- mxj.XMLEscapeCharsDecoder(true)
-
- // Required for https://github.com/TomWright/dasel/issues/164
- mxj.XmlCharsetReader = charset.NewReaderLabel
-
- registerReadParser([]string{"xml"}, []string{".xml"}, &XMLParser{})
- registerWriteParser([]string{"xml"}, []string{".xml"}, &XMLParser{})
-}
-
-// XMLParser is a Parser implementation to handle xml files.
-type XMLParser struct {
-}
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *XMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- if byteData == nil {
- return dasel.Value{}, fmt.Errorf("cannot parse nil xml data")
- }
- if len(byteData) == 0 || strings.TrimSpace(string(byteData)) == "" {
- return dasel.Value{}, nil
- }
- data, err := mxj.NewMapXml(byteData)
- if err != nil {
- return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
- }
- return dasel.ValueOf(map[string]interface{}(data)).WithMetadata("isSingleDocument", true), nil
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *XMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- buf := new(bytes.Buffer)
-
- prettyPrint := true
- colourise := false
- indent := " "
-
- for _, o := range options {
- switch o.Key {
- case OptionIndent:
- if value, ok := o.Value.(string); ok {
- indent = value
- }
- case OptionPrettyPrint:
- if value, ok := o.Value.(bool); ok {
- prettyPrint = value
- }
- case OptionColourise:
- if value, ok := o.Value.(bool); ok {
- colourise = value
- }
- }
- }
-
- writeMap := func(val interface{}) error {
- var m map[string]interface{}
-
- switch v := val.(type) {
- case *dencoding.Map:
- m = v.UnorderedData()
- case map[string]any:
- m = v
- default:
- _, err := buf.Write([]byte(fmt.Sprintf("%v\n", val)))
- return err
- }
-
- mv := mxj.New()
- for k, v := range m {
- mv[k] = v
- }
-
- var byteData []byte
- var err error
- if prettyPrint {
- byteData, err = mv.XmlIndent("", indent)
- } else {
- byteData, err = mv.Xml()
- }
-
- if err != nil {
- return err
- }
- buf.Write(byteData)
- buf.Write([]byte("\n"))
- return nil
- }
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- if err := writeMap(value.Interface()); err != nil {
- return nil, err
- }
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- if err := writeMap(value.Index(i).Interface()); err != nil {
- return nil, err
- }
- }
- case value.IsDencodingMap():
- dm := value.Interface().(*dencoding.Map)
- if err := writeMap(dm.UnorderedData()); err != nil {
- return nil, err
- }
- default:
- if err := writeMap(value.Interface()); err != nil {
- return nil, err
- }
- }
-
- if colourise {
- if err := ColouriseBuffer(buf, "xml"); err != nil {
- return nil, fmt.Errorf("could not colourise output: %w", err)
- }
- }
-
- return buf.Bytes(), nil
-}
diff --git a/storage/xml_test.go b/storage/xml_test.go
deleted file mode 100644
index 00aceac1..00000000
--- a/storage/xml_test.go
+++ /dev/null
@@ -1,244 +0,0 @@
-package storage_test
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/storage"
- "io"
- "reflect"
- "testing"
-
- "golang.org/x/text/encoding/charmap"
- "golang.org/x/text/encoding/unicode"
-)
-
-var xmlBytes = []byte(`
- Tom
-
-`)
-var xmlMap = map[string]interface{}{
- "user": map[string]interface{}{
- "name": "Tom",
- },
-}
-var encodedXmlMap = map[string]interface{}{
- "user": map[string]interface{}{
- "name": "Tõm",
- },
-}
-
-func TestXMLParser_FromBytes(t *testing.T) {
- got, err := (&storage.XMLParser{}).FromBytes(xmlBytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(xmlMap, got.Interface()) {
- t.Errorf("expected %v, got %v", xmlMap, got)
- }
-}
-
-func TestXMLParser_FromBytes_Empty(t *testing.T) {
- got, err := (&storage.XMLParser{}).FromBytes([]byte{})
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !got.IsEmpty() {
- t.Errorf("expected %v, got %v", nil, got)
- }
-}
-
-func TestXMLParser_FromBytes_Error(t *testing.T) {
- _, err := (&storage.XMLParser{}).FromBytes(nil)
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
- _, err = (&storage.XMLParser{}).FromBytes(yamlBytes)
- if err == nil {
- t.Errorf("expected error but got none")
- return
- }
-}
-
-func TestXMLParser_ToBytes_Default(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(xmlBytes, got) {
- t.Errorf("expected %v, got %v", string(xmlBytes), string(got))
- }
-}
-func TestXMLParser_ToBytes_SingleDocument(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap).WithMetadata("isSingleDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(xmlBytes, got) {
- t.Errorf("expected %v, got %v", string(xmlBytes), string(got))
- }
-}
-func TestXMLParser_ToBytes_SingleDocument_Colourise(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
-
- expBuf, _ := storage.Colourise(string(xmlBytes), "xml")
- exp := expBuf.Bytes()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
-}
-func TestXMLParser_ToBytes_MultiDocument(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf([]interface{}{xmlMap, xmlMap}).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := append([]byte{}, xmlBytes...)
- exp = append(exp, xmlBytes...)
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", string(exp), string(got))
- }
-}
-func TestXMLParser_ToBytes_DefaultValue(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf("asd"))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := []byte(`asd
-`)
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", string(exp), string(got))
- }
-}
-func TestXMLParser_ToBytes_SingleDocumentValue(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf("asd"))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := []byte(`asd
-`)
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", string(exp), string(got))
- }
-}
-func TestXMLParser_ToBytes_MultiDocumentValue(t *testing.T) {
- got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", "123"}).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := []byte(`asd
-123
-`)
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", string(exp), string(got))
- }
-}
-func TestXMLParser_ToBytes_Entities(t *testing.T) {
- bytes := []byte(`
-
- sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% </dev/tty >/dev/tty
- .rp .sh
- RetroPie
- retropie
- /home/fozz/RetroPie/retropiemenu
-
- retropie
-
-
-`)
-
- p := &storage.XMLParser{}
- var doc interface{}
-
- t.Run("FromBytes", func(t *testing.T) {
- res, err := p.FromBytes(bytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- doc = res.Interface()
- got := doc.(map[string]interface{})["systemList"].(map[string]interface{})["system"].(map[string]interface{})["command"]
- exp := "sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% </dev/tty >/dev/tty"
- if exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
- })
-
- t.Run("ToBytes", func(t *testing.T) {
- gotBytes, err := p.ToBytes(dasel.ValueOf(doc))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- got := string(gotBytes)
- exp := string(bytes)
- if exp != got {
- t.Errorf("expected %s, got %s", exp, got)
- }
- })
-}
-
-func TestXMLParser_DifferentEncodings(t *testing.T) {
- newXmlBytes := func(newWriter func(io.Writer) io.Writer, encoding, text string) []byte {
- const encodedXmlBytesFmt = ``
- const xmlBody = `%s`
-
- var buf bytes.Buffer
-
- w := newWriter(&buf)
- fmt.Fprintf(w, xmlBody, text)
-
- return []byte(fmt.Sprintf(encodedXmlBytesFmt, encoding) + buf.String())
- }
-
- testCases := []struct {
- name string
- xml []byte
- }{
- {
- name: "supports ISO-8859-1",
- xml: newXmlBytes(charmap.ISO8859_1.NewEncoder().Writer, "ISO-8859-1", "Tõm"),
- },
- {
- name: "supports UTF-8",
- xml: newXmlBytes(unicode.UTF8.NewEncoder().Writer, "UTF-8", "Tõm"),
- },
- {
- name: "supports latin1",
- xml: newXmlBytes(charmap.Windows1252.NewEncoder().Writer, "latin1", "Tõm"),
- },
- {
- name: "supports UTF-16",
- xml: newXmlBytes(unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16", "Tõm"),
- },
- {
- name: "supports UTF-16 (big endian)",
- xml: newXmlBytes(unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16BE", "Tõm"),
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- got, err := (&storage.XMLParser{}).FromBytes(tc.xml)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(encodedXmlMap, got.Interface()) {
- t.Errorf("expected %v, got %v", encodedXmlMap, got)
- }
- })
- }
-}
diff --git a/storage/yaml.go b/storage/yaml.go
deleted file mode 100644
index 0c8c9bc2..00000000
--- a/storage/yaml.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package storage
-
-import (
- "bytes"
- "fmt"
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/util"
- "io"
-)
-
-func init() {
- registerReadParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{})
- registerWriteParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{})
-}
-
-// YAMLParser is a Parser implementation to handle yaml files.
-type YAMLParser struct {
-}
-
-// FromBytes returns some data that is represented by the given bytes.
-func (p *YAMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) {
- res := make([]interface{}, 0)
-
- decoder := dencoding.NewYAMLDecoder(bytes.NewReader(byteData))
-
-docLoop:
- for {
- var docData interface{}
- if err := decoder.Decode(&docData); err != nil {
- if err == io.EOF {
- break docLoop
- }
- return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
- }
-
- formattedDocData := cleanupYamlMapValue(docData)
-
- res = append(res, formattedDocData)
- }
- switch len(res) {
- case 0:
- return dasel.Value{}, nil
- case 1:
- return dasel.ValueOf(res[0]).WithMetadata("isSingleDocument", true), nil
- default:
- return dasel.ValueOf(res).WithMetadata("isMultiDocument", true), nil
- }
-}
-
-func cleanupYamlInterfaceArray(in []interface{}) []interface{} {
- res := make([]interface{}, len(in))
- for i, v := range in {
- res[i] = cleanupYamlMapValue(v)
- }
- return res
-}
-
-func cleanupYamlInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
- res := make(map[string]interface{})
- for k, v := range in {
- res[util.ToString(k)] = cleanupYamlMapValue(v)
- }
- return res
-}
-
-func cleanupYamlMapValue(v interface{}) interface{} {
- switch v := v.(type) {
- case []interface{}:
- return cleanupYamlInterfaceArray(v)
- case map[interface{}]interface{}:
- return cleanupYamlInterfaceMap(v)
- case string:
- return v
- default:
- return v
- }
-}
-
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *YAMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
- buffer := new(bytes.Buffer)
-
- colourise := false
-
- encoderOptions := make([]dencoding.YAMLEncoderOption, 0)
-
- for _, o := range options {
- switch o.Key {
- case OptionColourise:
- if value, ok := o.Value.(bool); ok {
- colourise = value
- }
- case OptionIndent:
- if value, ok := o.Value.(string); ok {
- encoderOptions = append(encoderOptions, dencoding.YAMLEncodeIndent(len(value)))
- }
- }
- }
-
- encoder := dencoding.NewYAMLEncoder(buffer, encoderOptions...)
- defer encoder.Close()
-
- switch {
- case value.Metadata("isSingleDocument") == true:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode single document: %w", err)
- }
- case value.Metadata("isMultiDocument") == true:
- for i := 0; i < value.Len(); i++ {
- if err := encoder.Encode(value.Index(i).Interface()); err != nil {
- return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err)
- }
- }
- default:
- if err := encoder.Encode(value.Interface()); err != nil {
- return nil, fmt.Errorf("could not encode default document type: %w", err)
- }
- }
-
- if colourise {
- if err := ColouriseBuffer(buffer, "yaml"); err != nil {
- return nil, fmt.Errorf("could not colourise output: %w", err)
- }
- }
-
- return buffer.Bytes(), nil
-}
diff --git a/storage/yaml_test.go b/storage/yaml_test.go
deleted file mode 100644
index 53d8f8f2..00000000
--- a/storage/yaml_test.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package storage_test
-
-import (
- "github.com/tomwright/dasel/v2"
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/storage"
- "reflect"
- "strings"
- "testing"
-)
-
-var yamlBytes = []byte(`name: Tom
-numbers:
- - 1
- - 2
-`)
-var yamlMap = dencoding.NewMap().
- Set("name", "Tom").
- Set("numbers", []interface{}{
- int64(1),
- int64(2),
- })
-
-var yamlBytesMulti = []byte(`name: Tom
----
-name: Jim
-`)
-var yamlMapMulti = []interface{}{
- dencoding.NewMap().Set("name", "Tom"),
- dencoding.NewMap().Set("name", "Jim"),
-}
-
-func TestYAMLParser_FromBytes(t *testing.T) {
- t.Run("Valid", func(t *testing.T) {
- gotFromBytes, err := (&storage.YAMLParser{}).FromBytes(yamlBytes)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := yamlMap.KeyValues()
- got := gotFromBytes.Interface().(*dencoding.Map).KeyValues()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- t.Run("ValidMultiDocument", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).FromBytes(yamlBytesMulti)
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- exp := yamlMapMulti
-
- if !reflect.DeepEqual(exp, got.Interface()) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- t.Run("Invalid", func(t *testing.T) {
- _, err := (&storage.YAMLParser{}).FromBytes([]byte(`{1:asd`))
- if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") {
- t.Errorf("unexpected error: %v", err)
- return
- }
- })
- t.Run("Empty", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).FromBytes([]byte(``))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if !reflect.DeepEqual(dasel.Value{}, got) {
- t.Errorf("expected %v, got %v", nil, got)
- }
- })
-}
-
-func TestYAMLParser_ToBytes(t *testing.T) {
- t.Run("Valid", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(yamlBytes) != string(got) {
- t.Errorf("expected %s, got %s", yamlBytes, got)
- }
- })
- t.Run("ValidSingle", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap).WithMetadata("isSingleDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(yamlBytes) != string(got) {
- t.Errorf("expected %s, got %s", yamlBytes, got)
- }
- })
- t.Run("ValidSingleColourise", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- expBuf, _ := storage.Colourise(string(yamlBytes), "yaml")
- exp := expBuf.Bytes()
- if !reflect.DeepEqual(exp, got) {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- t.Run("ValidMulti", func(t *testing.T) {
- got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMapMulti).WithMetadata("isMultiDocument", true))
- if err != nil {
- t.Errorf("unexpected error: %s", err)
- return
- }
- if string(yamlBytesMulti) != string(got) {
- t.Errorf("expected %s, got %s", yamlBytesMulti, got)
- }
- })
-}
diff --git a/tests/assets/broken.json b/tests/assets/broken.json
deleted file mode 100644
index 0c6ec1f5..00000000
--- a/tests/assets/broken.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "Tom",
- "preferences": {
- "favouriteColour": "red"
- },
- "colours": [
- "red",
- "green"
-}
\ No newline at end of file
diff --git a/tests/assets/broken.xml b/tests/assets/broken.xml
deleted file mode 100644
index a0b0dbab..00000000
--- a/tests/assets/broken.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/tests/assets/deployment.yaml b/tests/assets/deployment.yaml
deleted file mode 100644
index 8a102bc3..00000000
--- a/tests/assets/deployment.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: auth-deployment
-spec:
- replicas: 3
- selector:
- matchLabels:
- component: auth
- template:
- metadata:
- labels:
- component: auth
- spec:
- containers:
- - env:
- - name: BUSINESS_SERVICE
- value: business-cluster-ip:9000
- - name: PASSWORD
- valueFrom:
- secretKeyRef:
- key: pgpassword
- name: PGPASSWORD
- - name: MY_NEW_ENV_VAR
- value: NEW_VALUE
- image: tomwright/auth:dev
- name: auth
- ports:
- - containerPort: 9000
- - containerPort: 8000
diff --git a/tests/assets/example.json b/tests/assets/example.json
deleted file mode 100644
index 845a5bbc..00000000
--- a/tests/assets/example.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "Tom",
- "preferences": {
- "favouriteColour": "red"
- },
- "colours": [
- "red",
- "green",
- "blue"
- ],
- "colourCodes": [
- {
- "name": "red",
- "rgb": "ff0000"
- },
- {
- "name": "green",
- "rgb": "00ff00"
- },
- {
- "name": "blue",
- "rgb": "0000ff"
- }
- ]
-}
\ No newline at end of file
diff --git a/tests/assets/example.xml b/tests/assets/example.xml
deleted file mode 100644
index 5da9860b..00000000
--- a/tests/assets/example.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
- Tom
-
- red
-
- red
- green
- blue
-
- red
- ff0000
-
-
- green
- 00ff00
-
-
- blue
- 0000ff
-
-
\ No newline at end of file
diff --git a/tests/assets/example.yaml b/tests/assets/example.yaml
deleted file mode 100644
index a4ceb131..00000000
--- a/tests/assets/example.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Tom
-preferences:
- favouriteColour: red
-colours:
- - red
- - green
- - blue
-colourCodes:
- - name: red
- rgb: ff0000
- - name: green
- rgb: 00ff00
- - name: blue
- rgb: 0000ff
\ No newline at end of file
diff --git a/tests/assets/int-value.txt b/tests/assets/int-value.txt
deleted file mode 100644
index bd41cba7..00000000
--- a/tests/assets/int-value.txt
+++ /dev/null
@@ -1 +0,0 @@
-12345
\ No newline at end of file
diff --git a/tests/assets/json-value.json b/tests/assets/json-value.json
deleted file mode 100644
index 89634894..00000000
--- a/tests/assets/json-value.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "this": "is a value"
-}
\ No newline at end of file
diff --git a/tests/assets/string-value.txt b/tests/assets/string-value.txt
deleted file mode 100644
index 8b94090c..00000000
--- a/tests/assets/string-value.txt
+++ /dev/null
@@ -1 +0,0 @@
-This is a string value
\ No newline at end of file
diff --git a/truthy.go b/truthy.go
deleted file mode 100644
index 043e657b..00000000
--- a/truthy.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package dasel
-
-import (
- "reflect"
- "strings"
-)
-
-func IsTruthy(value interface{}) bool {
- switch v := value.(type) {
- case Value:
- return IsTruthy(v.Unpack().Interface())
-
- case reflect.Value:
- return IsTruthy(unpackReflectValue(v).Interface())
-
- case bool:
- return v
-
- case string:
- v = strings.ToLower(strings.TrimSpace(v))
- switch v {
- case "false", "no", "0":
- return false
- default:
- return v != ""
- }
-
- case []byte:
- return IsTruthy(string(v))
-
- case int:
- return v > 0
- case int8:
- return v > 0
- case int16:
- return v > 0
- case int32:
- return v > 0
- case int64:
- return v > 0
-
- case uint:
- return v > 0
- case uint8:
- return v > 0
- case uint16:
- return v > 0
- case uint32:
- return v > 0
- case uint64:
- return v > 0
-
- case float32:
- return v >= 1
- case float64:
- return v >= 1
-
- default:
- return false
- }
-}
diff --git a/truthy_test.go b/truthy_test.go
deleted file mode 100644
index 8bca5227..00000000
--- a/truthy_test.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package dasel
-
-import (
- "fmt"
- "reflect"
- "testing"
-)
-
-func TestIsTruthy(t *testing.T) {
-
- type testDef struct {
- name string
- in interface{}
- out bool
- }
-
- baseData := []testDef{
- {"bool:true", true, true},
- {"bool:false", false, false},
- {"string:lowercaseTrue", "true", true},
- {"string:lowercaseFalse", "false", false},
- {"string:uppercaseTrue", "TRUE", true},
- {"string:uppercaseFalse", "FALSE", false},
- {"string:lowercaseYes", "yes", true},
- {"string:lowercaseNo", "no", false},
- {"string:uppercaseYes", "YES", true},
- {"string:lowercaseNo", "NO", false},
- {"[]byte:lowercaseTrue", []byte("true"), true},
- {"[]byte:lowercaseFalse", []byte("false"), false},
- {"[]byte:uppercaseTrue", []byte("TRUE"), true},
- {"[]byte:uppercaseFalse", []byte("FALSE"), false},
- {"[]byte:lowercaseYes", []byte("yes"), true},
- {"[]byte:lowercaseNo", []byte("no"), false},
- {"[]byte:uppercaseYes", []byte("YES"), true},
- {"[]byte:lowercaseNo", []byte("NO"), false},
- {"int:0", int(0), false},
- {"int8:0", int8(0), false},
- {"int16:0", int16(0), false},
- {"int32:0", int32(0), false},
- {"int64:0", int64(0), false},
- {"int:-1", int(-1), false},
- {"int8:-1", int8(-1), false},
- {"int16:-1", int16(-1), false},
- {"int32:-1", int32(-1), false},
- {"int64:-1", int64(-1), false},
- {"uint:0", uint(0), false},
- {"uint8:0", uint8(0), false},
- {"uint16:0", uint16(0), false},
- {"uint32:0", uint32(0), false},
- {"uint64:0", uint64(0), false},
- {"int:1", int(1), true},
- {"int8:1", int8(1), true},
- {"int16:1", int16(1), true},
- {"int32:1", int32(1), true},
- {"int64:1", int64(1), true},
- {"uint:1", uint(1), true},
- {"uint8:1", uint8(1), true},
- {"uint16:1", uint16(1), true},
- {"uint32:1", uint32(1), true},
- {"uint64:1", uint64(1), true},
- {"float32:0", float32(0), false},
- {"float64:0", float64(0), false},
- {"float32:-1", float32(-1), false},
- {"float64:-1", float64(-1), false},
- {"float32:1", float32(1), true},
- {"float64:1", float64(1), true},
- {"unhandled:[]string", []string{}, false},
- }
-
- testData := make([]testDef, 0)
-
- for _, td := range baseData {
- testData = append(
- testData,
- td,
- testDef{
- name: fmt.Sprintf("reflect.Value:%s", td.name),
- in: reflect.ValueOf(td.in),
- out: td.out,
- },
- testDef{
- name: fmt.Sprintf("dasel.Value:%s", td.name),
- in: ValueOf(td.in),
- out: td.out,
- },
- )
- }
-
- for _, test := range testData {
- tc := test
- t.Run(tc.name, func(t *testing.T) {
- if exp, got := tc.out, IsTruthy(tc.in); exp != got {
- t.Errorf("expected %v, got %v", exp, got)
- }
- })
- }
-}
diff --git a/util/to_string.go b/util/to_string.go
deleted file mode 100644
index 04958ec8..00000000
--- a/util/to_string.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package util
-
-import (
- "fmt"
-)
-
-// ToString converts the given value to a string.
-func ToString(value any) string {
- switch v := value.(type) {
- case nil:
- return "null"
- case string:
- return v
- case []byte:
- return string(v)
- case int, int8, int16, int32, int64,
- uint, uint8, uint16, uint32, uint64:
- return fmt.Sprintf("%d", v)
- case float32, float64:
- return fmt.Sprintf("%f", v)
- case bool:
- return fmt.Sprint(v)
- default:
- return fmt.Sprint(v)
- }
-}
diff --git a/value.go b/value.go
deleted file mode 100644
index e809bb64..00000000
--- a/value.go
+++ /dev/null
@@ -1,544 +0,0 @@
-package dasel
-
-import (
- "reflect"
-
- "github.com/tomwright/dasel/v2/dencoding"
- "github.com/tomwright/dasel/v2/util"
-)
-
-// Value is a wrapper around reflect.Value that adds some handy helper funcs.
-type Value struct {
- reflect.Value
- setFn func(value Value)
- deleteFn func()
- metadata map[string]interface{}
-}
-
-// ValueOf wraps value in a Value.
-func ValueOf(value interface{}) Value {
- switch v := value.(type) {
- case Value:
- return v
- case reflect.Value:
- return Value{
- Value: v,
- }
- default:
- return Value{
- Value: reflect.ValueOf(value),
- }
- }
-}
-
-// Metadata returns the metadata with a key of key for v.
-func (v Value) Metadata(key string) interface{} {
- if v.metadata == nil {
- return nil
- }
- if m, ok := v.metadata[key]; ok {
- return m
- }
- return nil
-}
-
-// WithMetadata sets the given value into the values metadata.
-func (v Value) WithMetadata(key string, value interface{}) Value {
- if v.metadata == nil {
- v.metadata = map[string]interface{}{}
- }
- v.metadata[key] = value
- return v
-}
-
-// Interface returns the interface{} value of v.
-func (v Value) Interface() interface{} {
- return v.Unpack().Interface()
-}
-
-// Len returns v's length.
-func (v Value) Len() int {
- if v.IsDencodingMap() {
- return len(v.Interface().(*dencoding.Map).Keys())
- }
- switch v.Kind() {
- case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
- return v.Unpack().Len()
- case reflect.Bool:
- if v.Interface() == true {
- return 1
- } else {
- return 0
- }
- default:
- return len(util.ToString(v.Interface()))
- }
-}
-
-// String returns the string v's underlying value, as a string.
-func (v Value) String() string {
- return v.Unpack().String()
-}
-
-// IsEmpty returns true is v represents an empty reflect.Value.
-func (v Value) IsEmpty() bool {
- return isEmptyReflectValue(unpackReflectValue(v.Value))
-}
-
-func (v Value) IsNil() bool {
- switch v.Kind() {
- case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
- return v.Value.IsNil()
- default:
- return false
- }
-}
-
-func isEmptyReflectValue(v reflect.Value) bool {
- if (v == reflect.Value{}) {
- return true
- }
- return v.Kind() == reflect.String && v.Interface() == UninitialisedPlaceholder
-}
-
-// Kind returns the underlying type of v.
-func (v Value) Kind() reflect.Kind {
- return v.Unpack().Kind()
-}
-
-func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool {
- for _, v := range kinds {
- if v == kind {
- return true
- }
- }
- return false
-}
-
-var dencodingMapType = reflect.TypeOf(&dencoding.Map{})
-
-func isDencodingMap(value reflect.Value) bool {
- return value.Kind() == reflect.Ptr && value.Type() == dencodingMapType
-}
-
-func unpackReflectValue(value reflect.Value, kinds ...reflect.Kind) reflect.Value {
- if len(kinds) == 0 {
- kinds = append(kinds, reflect.Ptr, reflect.Interface)
- }
- res := value
- for {
- if isDencodingMap(res) {
- return res
- }
- if !containsKind(kinds, res.Kind()) {
- return res
- }
- if res.IsNil() {
- return res
- }
- res = res.Elem()
- }
-}
-
-func (v Value) FirstAddressable() reflect.Value {
- res := v.Value
- for !res.CanAddr() {
- res = res.Elem()
- }
- return res
-}
-
-// Unpack returns the underlying reflect.Value after resolving any pointers or interface types.
-func (v Value) Unpack(kinds ...reflect.Kind) reflect.Value {
- if !v.Value.IsValid() {
- return reflect.ValueOf(new(any)).Elem()
- }
- return unpackReflectValue(v.Value, kinds...)
-}
-
-func (v Value) Type() reflect.Type {
- return v.Unpack().Type()
-}
-
-// Set sets underlying value of v.
-// Depends on setFn since the implementation can differ depending on how the Value was initialised.
-func (v Value) Set(value Value) {
- if v.setFn != nil {
- v.setFn(value)
- return
- }
- panic("unable to set value with missing setFn")
-}
-
-// Delete deletes the current element.
-// Depends on deleteFn since the implementation can differ depending on how the Value was initialised.
-func (v Value) Delete() {
- if v.deleteFn != nil {
- v.deleteFn()
- return
- }
- panic("unable to delete value with missing deleteFn")
-}
-
-func (v Value) IsDencodingMap() bool {
- if v.Kind() != reflect.Ptr {
- return false
- }
- _, ok := v.Interface().(*dencoding.Map)
- return ok
-}
-
-func (v Value) dencodingMapIndex(key Value) Value {
- getValueByKey := func() reflect.Value {
- if !v.IsDencodingMap() {
- return reflect.Value{}
- }
- om := v.Interface().(*dencoding.Map)
- if v, ok := om.Get(key.Value.String()); !ok {
- return reflect.Value{}
- } else {
- if v == nil {
- return reflect.ValueOf(new(any)).Elem()
- }
- return reflect.ValueOf(v)
- }
- }
- index := Value{
- Value: getValueByKey(),
- setFn: func(value Value) {
- // Note that we do not use Interface() here as it will dereference the received value.
- // Instead, we only dereference the interface type to receive the pointer.
- v.Interface().(*dencoding.Map).Set(key.Value.String(), value.Unpack(reflect.Interface).Interface())
- },
- deleteFn: func() {
- v.Interface().(*dencoding.Map).Delete(key.Value.String())
- },
- }
- return index.
- WithMetadata("key", key.Interface()).
- WithMetadata("parent", v)
-}
-
-// MapIndex returns the value associated with key in the map v.
-// It returns the zero Value if no field was found.
-func (v Value) MapIndex(key Value) Value {
- index := Value{
- Value: v.Unpack().MapIndex(key.Value),
- setFn: func(value Value) {
- v.Unpack().SetMapIndex(key.Value, value.Value)
- },
- deleteFn: func() {
- v.Unpack().SetMapIndex(key.Value, reflect.Value{})
- },
- }
- return index.
- WithMetadata("key", key.Interface()).
- WithMetadata("parent", v)
-}
-
-func (v Value) MapKeys() []Value {
- res := make([]Value, 0)
- for _, k := range v.Unpack().MapKeys() {
- res = append(res, Value{Value: k})
- }
- return res
-}
-
-// FieldByName returns the struct field with the given name.
-// It returns the zero Value if no field was found.
-func (v Value) FieldByName(name string) Value {
- return Value{
- Value: v.Unpack().FieldByName(name),
- setFn: func(value Value) {
- v.Unpack().FieldByName(name).Set(value.Value)
- },
- deleteFn: func() {
- field := v.Unpack().FieldByName(name)
- field.Set(reflect.New(field.Type()))
- },
- }.
- WithMetadata("key", name).
- WithMetadata("parent", v)
-}
-
-// NumField returns the number of fields in the struct v.
-func (v Value) NumField() int {
- return v.Unpack().NumField()
-}
-
-// Index returns v's i'th element.
-// It panics if v's Kind is not Array, Slice, or String or i is out of range.
-func (v Value) Index(i int) Value {
- return Value{
- Value: v.Unpack().Index(i),
- setFn: func(value Value) {
- v.Unpack().Index(i).Set(value.Value)
- },
- deleteFn: func() {
- currentLen := v.Len()
- updatedSlice := reflect.MakeSlice(sliceInterfaceType, currentLen-1, v.Len()-1)
- // Rebuild the slice excluding the deleted element
- for indexToRead := 0; indexToRead < currentLen; indexToRead++ {
- indexToWrite := indexToRead
- if indexToRead == i {
- continue
- }
- if indexToRead > i {
- indexToWrite--
- }
- updatedSlice.Index(indexToWrite).Set(
- v.Index(indexToRead).Value,
- )
- }
-
- v.Unpack().Set(updatedSlice)
- },
- }.
- WithMetadata("key", i).
- WithMetadata("parent", v)
-}
-
-// Append appends an empty value to the end of the slice.
-func (v Value) Append() Value {
- currentLen := v.Len()
- newLen := currentLen + 1
-
- updatedSlice := reflect.MakeSlice(reflect.TypeOf(v.Interface()), newLen, newLen)
- // copy all existing elements into updatedSlice.
- // this leaves the last element empty.
- for i := 0; i < currentLen; i++ {
- updatedSlice.Index(i).Set(
- v.Index(i).Value,
- )
- }
-
- firstAddressable := v.FirstAddressable()
- firstAddressable.Set(updatedSlice)
-
- // This code was causing a panic...
- // It doesn't seem necessary. Leaving here for reference in-case it was needed.
- // See https://github.com/TomWright/dasel/issues/392
- // Set the last element to uninitialised.
- //updatedSlice.Index(currentLen).Set(
- // v.Index(currentLen).asUninitialised().Value,
- //)
-
- return v
-}
-
-var sliceInterfaceType = reflect.TypeFor[[]any]()
-var mapStringInterfaceType = reflect.TypeFor[map[string]interface{}]()
-
-var UninitialisedPlaceholder interface{} = "__dasel_not_found__"
-
-func (v Value) asUninitialised() Value {
- v.Value = reflect.ValueOf(UninitialisedPlaceholder)
- return v
-}
-
-func (v Value) initEmptyMap() Value {
- emptyMap := reflect.MakeMap(mapStringInterfaceType)
- v.Set(Value{Value: emptyMap})
- v.Value = emptyMap
- return v
-}
-
-func (v Value) initEmptydencodingMap() Value {
- om := dencoding.NewMap()
- rom := reflect.ValueOf(om)
- v.Set(Value{Value: rom})
- v.Value = rom
- return v
-}
-
-func (v Value) initEmptySlice() Value {
- emptySlice := reflect.MakeSlice(sliceInterfaceType, 0, 0)
-
- addressableSlice := reflect.New(emptySlice.Type())
- addressableSlice.Elem().Set(emptySlice)
-
- v.Set(Value{Value: addressableSlice})
- v.Value = addressableSlice
- return v
-}
-
-func makeAddressableSlice(value reflect.Value) reflect.Value {
- if !unpackReflectValue(value, reflect.Ptr).CanAddr() {
- unpacked := unpackReflectValue(value)
-
- emptySlice := reflect.MakeSlice(unpacked.Type(), unpacked.Len(), unpacked.Len())
-
- for i := 0; i < unpacked.Len(); i++ {
- emptySlice.Index(i).Set(makeAddressable(unpacked.Index(i)))
- }
-
- addressableSlice := reflect.New(emptySlice.Type())
- addressableSlice.Elem().Set(emptySlice)
-
- return addressableSlice
- } else {
- // Make contained values addressable
- unpacked := unpackReflectValue(value)
- for i := 0; i < unpacked.Len(); i++ {
- unpacked.Index(i).Set(makeAddressable(unpacked.Index(i)))
- }
-
- return value
- }
-}
-
-func makeAddressableMap(value reflect.Value) reflect.Value {
- if !unpackReflectValue(value, reflect.Ptr).CanAddr() {
- unpacked := unpackReflectValue(value)
-
- emptyMap := reflect.MakeMap(unpacked.Type())
-
- for _, key := range unpacked.MapKeys() {
- emptyMap.SetMapIndex(key, makeAddressable(unpacked.MapIndex(key)))
- }
-
- addressableMap := reflect.New(emptyMap.Type())
- addressableMap.Elem().Set(emptyMap)
-
- return addressableMap
- } else {
- // Make contained values addressable
- unpacked := unpackReflectValue(value)
-
- for _, key := range unpacked.MapKeys() {
- unpacked.SetMapIndex(key, makeAddressable(unpacked.MapIndex(key)))
- }
-
- return value
- }
-}
-
-func makeAddressable(value reflect.Value) reflect.Value {
- unpacked := unpackReflectValue(value)
-
- if isDencodingMap(unpacked) {
- om := value.Interface().(*dencoding.Map)
- for _, kv := range om.KeyValues() {
- var val any
- if v := deref(reflect.ValueOf(kv.Value)); v.IsValid() {
- val = makeAddressable(v).Interface()
- } else {
- val = nil
- }
- om.Set(kv.Key, val)
- }
- return value
- }
-
- switch unpacked.Kind() {
- case reflect.Slice:
- return makeAddressableSlice(value)
- case reflect.Map:
- return makeAddressableMap(value)
- default:
- return value
- }
-}
-
-func derefSlice(value reflect.Value) reflect.Value {
- unpacked := unpackReflectValue(value)
-
- res := reflect.MakeSlice(unpacked.Type(), unpacked.Len(), unpacked.Len())
-
- for i := 0; i < unpacked.Len(); i++ {
- if v := deref(unpacked.Index(i)); v.IsValid() {
- res.Index(i).Set(v)
- }
- }
-
- return res
-}
-
-func derefMap(value reflect.Value) reflect.Value {
- unpacked := unpackReflectValue(value)
-
- res := reflect.MakeMap(unpacked.Type())
-
- for _, key := range unpacked.MapKeys() {
- if v := deref(unpacked.MapIndex(key)); v.IsValid() {
- res.SetMapIndex(key, v)
- } else {
- res.SetMapIndex(key, reflect.ValueOf(new(any)))
- }
- }
-
- return res
-}
-
-func deref(value reflect.Value) reflect.Value {
- unpacked := unpackReflectValue(value)
-
- if isDencodingMap(unpacked) {
- om := value.Interface().(*dencoding.Map)
- for _, kv := range om.KeyValues() {
- if v := deref(reflect.ValueOf(kv.Value)); v.IsValid() {
- om.Set(kv.Key, v.Interface())
- } else {
- om.Set(kv.Key, nil)
- }
- }
- return value
- }
-
- switch unpacked.Kind() {
- case reflect.Slice:
- return derefSlice(value)
- case reflect.Map:
- return derefMap(value)
- default:
- return unpackReflectValue(value)
- }
-}
-
-// Values represents a list of Value's.
-type Values []Value
-
-// Interfaces returns the interface values for the underlying values stored in v.
-func (v Values) Interfaces() []interface{} {
- res := make([]interface{}, 0)
- for _, val := range v {
- res = append(res, val.Interface())
- }
- return res
-}
-
-//func (v Values) initEmptyMaps() Values {
-// res := make(Values, len(v))
-// for k, value := range v {
-// if value.IsEmpty() {
-// res[k] = value.initEmptyMap()
-// } else {
-// res[k] = value
-// }
-// }
-// return res
-//}
-
-func (v Values) initEmptydencodingMaps() Values {
- res := make(Values, len(v))
- for k, value := range v {
- if value.IsEmpty() || value.IsNil() {
- res[k] = value.initEmptydencodingMap()
- } else {
- res[k] = value
- }
- }
- return res
-}
-
-func (v Values) initEmptySlices() Values {
- res := make(Values, len(v))
- for k, value := range v {
- if value.IsEmpty() || value.IsNil() {
- res[k] = value.initEmptySlice()
- } else {
- res[k] = value
- }
- }
- return res
-}