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 [![Gitbook](https://badges.aleen42.com/src/gitbook_1.svg)](https://daseldocs.tomwright.me) -[![Go Report Card](https://goreportcard.com/badge/github.com/TomWright/dasel/v2)](https://goreportcard.com/report/github.com/TomWright/dasel/v2) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/tomwright/dasel)](https://pkg.go.dev/github.com/tomwright/dasel/v2) +[![Go Report Card](https://goreportcard.com/badge/github.com/tomwright/dasel/v3)](https://goreportcard.com/report/github.com/tomwright/dasel/v3) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/tomwright/dasel)](https://pkg.go.dev/github.com/tomwright/dasel/v3) ![Test](https://github.com/TomWright/dasel/workflows/Test/badge.svg) ![Build](https://github.com/TomWright/dasel/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/TomWright/dasel/branch/master/graph/badge.svg)](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 - -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 - -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 - -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 - -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 - -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 - -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 - -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 - -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 - -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 "\"${name}\"\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

+ //
+ // Test 2 + //
+ //
+ //

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 -}