From b6d08a9aa1b44f7927199f690c4b8850e44746d0 Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Wed, 10 Jan 2024 15:47:52 -0800 Subject: [PATCH 1/4] feat: provide a custom `isSorted` function for determining whether a slice is sorted ascending. Inject the function into the playground, and fix the test that needs to verify sort order. fixes #5 --- README.md | 18 +++ cmd/server/main.go | 1 - eval/eval.go | 7 +- eval/eval_test.go | 8 +- examples.yaml | 4 +- functions/doc.go | 17 +++ functions/is_sorted.go | 128 ++++++++++++++++ functions/is_sorted_test.go | 296 ++++++++++++++++++++++++++++++++++++ go.mod | 3 + 9 files changed, 473 insertions(+), 9 deletions(-) create mode 100644 functions/doc.go create mode 100644 functions/is_sorted.go create mode 100644 functions/is_sorted_test.go diff --git a/README.md b/README.md index 6e5a86a..6554ab4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ CEL standard library and CEL Playground. Take a look at [all the environment options](eval/eval.go#L31). +### Playground Methods + +The following custom methods are available in the playground: + +#### isSorted(array) + +Returns whether the list is sorted in ascending order. +```expr +isSorted([1, 2, 3]) == true +isSorted([1, 3, 2]) == false +isSorted(["apple", "banana", "cherry"]) == true +``` +This custom function is importable in your own Expr code by importing github.com/polds/expr-playground/functions and +adding `functions.IsSorted()` to your environment. The library supports sorting on types that satisfy the +`sort.Interface` interface. + + + ## Development Build the Wasm binary: diff --git a/cmd/server/main.go b/cmd/server/main.go index a57624d..be8fcc8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,5 +1,4 @@ // Copyright 2023 Undistro Authors -// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/eval/eval.go b/eval/eval.go index f9a7314..a66a106 100644 --- a/eval/eval.go +++ b/eval/eval.go @@ -21,6 +21,7 @@ import ( "github.com/expr-lang/expr" "github.com/expr-lang/expr/vm" + "github.com/polds/expr-playground/functions" ) type RunResponse struct { @@ -30,12 +31,14 @@ type RunResponse struct { var exprEnvOptions = []expr.Option{ expr.AsAny(), + // Inject a custom isSorted function into the environment. + functions.IsSorted(), } // Eval evaluates the expr expression against the given input. func Eval(exp string, input map[string]any) (string, error) { - exprEnvOptions = append(exprEnvOptions, expr.Env(input)) - program, err := expr.Compile(exp, exprEnvOptions...) + localOpts := append([]expr.Option{expr.Env(input)}, exprEnvOptions...) + program, err := expr.Compile(exp, localOpts...) if err != nil { return "", fmt.Errorf("failed to compile the Expr expression: %w", err) } diff --git a/eval/eval_test.go b/eval/eval_test.go index 73d14e8..93a9bff 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -1,4 +1,5 @@ // Copyright 2023 Undistro Authors +// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -73,11 +74,8 @@ func TestEval(t *testing.T) { }, { name: "list", - // For some reason object.items == sort(object.items) is false here, but the playground evaluates it as true. - // Needs further investigation. - exp: `object.items == sort(object.items) && sum(object.items) == 6 && sort(object.items)[-1] == 3 && findIndex(object.items, # == 1) == 0`, + exp: `isSorted(object.items) && sum(object.items) == 6 && sort(object.items)[-1] == 3 && findIndex(object.items, # == 1) == 0`, want: true, - skip: true, // https://github.com/polds/expr-playground/issues/5 }, { name: "optional", @@ -183,6 +181,7 @@ func TestEval(t *testing.T) { skip: true, // https://github.com/polds/expr-playground/issues/20 }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.skip { @@ -190,7 +189,6 @@ func TestEval(t *testing.T) { } got, err := Eval(tt.exp, input) - if (err != nil) != tt.wantErr { t.Errorf("Eval() got error = %v, wantErr %t", err, tt.wantErr) return diff --git a/examples.yaml b/examples.yaml index 76852ba..b9c9e15 100644 --- a/examples.yaml +++ b/examples.yaml @@ -17,12 +17,14 @@ examples: - name: "default" expr: | // Welcome to the Expr Playground! - // Expr Playground is an interactive WebAssembly powered environment to explore and experiment with the Expr-lang. + // Expr Playground is an interactive WebAssembly powered environment to explore and experiment with Expr-lang. // // - Write your Expr expression here // - Use the area on the side for input data, in YAML or JSON format // - Press 'Run' to evaluate your Expr expression against the input data // - Explore our collection of examples for inspiration + // + // See the README on Github for information about what custom functions are available in the context. account.balance >= transaction.withdrawal || (account.overdraftProtection diff --git a/functions/doc.go b/functions/doc.go new file mode 100644 index 0000000..859cf8d --- /dev/null +++ b/functions/doc.go @@ -0,0 +1,17 @@ +// Copyright 2024 Peter Olds +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package functions provides custom importable functions for Expr runtimes that may be injected into your Expr +// runtime environments. +package functions diff --git a/functions/is_sorted.go b/functions/is_sorted.go new file mode 100644 index 0000000..d0d5c45 --- /dev/null +++ b/functions/is_sorted.go @@ -0,0 +1,128 @@ +// Copyright 2024 Peter Olds +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package functions + +import ( + "cmp" + "fmt" + "reflect" + "slices" + "sort" + + "github.com/expr-lang/expr" +) + +// IsSorted provides the isSorted function as an Expr function. It will verify that the provided type +// is sorted ascending. It supports the following types: +// - Injected types that support the sort.Interface +// - []int +// - []float64 +// - []string +// +// Usage: +// +// // Inject into your environment. +// _, err := expr.Compile(`foo`, expr.Env(nil), functions.ExprIsSorted()) +// +// Expression: +// +// isSorted([1, 2, 3]) +// isSorted(["a", "b", "c"]) +// isSorted([1.0, 2.0, 3.0]) +// isSorted(myCustomType) // myCustomType must implement sort.Interface +func IsSorted() expr.Option { + return expr.Function("isSorted", func(params ...any) (any, error) { + if len(params) != 1 { + return false, fmt.Errorf("expected one parameter, got %d", len(params)) + } + return isSorted(params[0]) + }, + new(func(sort.Interface) (bool, error)), + new(func([]any) (bool, error)), + new(func([]int) (bool, error)), + new(func([]float64) (bool, error)), + new(func([]string) (bool, error)), + ) +} + +// isSorted attempts to determine if v is sortable, first by determine if it satisfies the sort.Interface interface, +// then by checking if it is a slice of a sortable type. If the type is a slice of type []any pass it to the +// isSliceSorted method which builds a new slice of the correct type and validates that it is sorted. +func isSorted(v any) (any, error) { + if v == nil { + return false, nil + } + + switch t := v.(type) { + case sort.Interface: + return sort.IsSorted(t), nil + + // There are cases where Expr is passing around an []any instead of a []int, []float64, or []string. + // This logic will attempt to do its own sorting to determine if the slice is sorted. + case []any: + return isSliceSorted(t) + case []int: + return slices.IsSorted(t), nil + case []float64: + return slices.IsSorted(t), nil + case []string: + return slices.IsSorted(t), nil + } + return false, fmt.Errorf("type %s is not sortable", reflect.TypeOf(v)) +} + +func convertTo[E cmp.Ordered](x any) (E, error) { + var r E + v, ok := x.(E) + if !ok { + return r, fmt.Errorf("mis-typed slice, expected %T, got %T", r, x) + } + return v, nil +} + +func less[E cmp.Ordered](vv []any) (bool, error) { + for i := len(vv) - 1; i > 0; i-- { + l, err := convertTo[E](vv[i-1]) + if err != nil { + return false, err + } + h, err := convertTo[E](vv[i]) + if err != nil { + return false, err + } + if cmp.Less(h, l) { + return false, nil + } + } + return true, nil +} + +// isSliceSorted attempts to determine if v is a slice of a sortable type. +// Instead of building a slice it just walks the slice and validates that it is sorted. The first unsorted element +// causes the function to return false. +// Expr only supports int, float, and string types. +func isSliceSorted(vv []any) (bool, error) { + // We have to peek the first element to determine the type of the slice. + switch t := vv[0].(type) { + case int: + return less[int](vv) + case float64: + return less[float64](vv) + case string: + return less[string](vv) + default: + return false, fmt.Errorf("unsupported type %T", t) + } +} diff --git a/functions/is_sorted_test.go b/functions/is_sorted_test.go new file mode 100644 index 0000000..c75ef72 --- /dev/null +++ b/functions/is_sorted_test.go @@ -0,0 +1,296 @@ +// Copyright 2024 Peter Olds +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package functions + +import ( + "fmt" + "testing" + + "github.com/expr-lang/expr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type Person struct { + Name string + Age int +} + +func (p Person) String() string { + return fmt.Sprintf("%s: %d", p.Name, p.Age) +} + +// ByAge implements sort.Interface for []Person based on +// the Age field. +type ByAge []Person + +func (a ByAge) Len() int { return len(a) } +func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } + +func Test_isSorted(t *testing.T) { + t.Run("nil", func(t *testing.T) { + sorted, err := isSorted(nil) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("sort.Interface - not sorted", func(t *testing.T) { + people := []Person{ + {"Bob", 31}, + {"John", 42}, + {"Michael", 17}, + {"Jenny", 26}, + } + sorted, err := isSorted(ByAge(people)) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("sort.Interface - sorted", func(t *testing.T) { + people := []Person{ + {"Michael", 17}, + {"Jenny", 26}, + {"Bob", 31}, + {"John", 42}, + } + sorted, err := isSorted(ByAge(people)) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("any - int slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]any{5, 4, 3, 2, 1}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - int slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]any{1, 2, 3, 4, 5}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("any - mis-typed int slice", func(t *testing.T) { + sorted, err := isSorted([]any{1, 2, 3, "4", 5}) + require.Error(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - float slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]any{5.0, 4.0, 3.0, 2.0, 1.0}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - float slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]any{1.0, 2.0, 3.0, 4.0, 5.0}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("any - mis-typed float slice", func(t *testing.T) { + sorted, err := isSorted([]any{1.0, 2.0, 3.0, "4.0", 5.0}) + require.Error(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - string slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]any{"5", "4", "3", "2", "1"}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - string slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]any{"1", "2", "3", "4", "5"}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("any - mis-typed string slice", func(t *testing.T) { + sorted, err := isSorted([]any{"1", "2", "3", 4, "5"}) + require.Error(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("any - unsupported type", func(t *testing.T) { + sorted, err := isSorted([]any{Person{"Bob", 31}}) + require.Error(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("int slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]int{5, 4, 3, 2, 1}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("int slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]int{1, 2, 3, 4, 5}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("float slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]float64{5.0, 4.0, 3.0, 2.0, 1.0}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("float slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]float64{1.0, 2.0, 3.0, 4.0, 5.0}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("string slice - not sorted", func(t *testing.T) { + sorted, err := isSorted([]string{"5", "4", "3", "2", "1"}) + require.NoError(t, err) + assert.False(t, sorted.(bool)) + }) + t.Run("string slice - sorted", func(t *testing.T) { + sorted, err := isSorted([]string{"1", "2", "3", "4", "5"}) + require.NoError(t, err) + assert.True(t, sorted.(bool)) + }) + t.Run("unsupported type", func(t *testing.T) { + sorted, err := isSorted(Person{"Bob", 31}) + require.Error(t, err) + assert.False(t, sorted.(bool)) + }) +} + +func TestIsSorted(t *testing.T) { + tests := []struct { + name string + exp string + want bool + wantCompileErr bool + wantRuntimeErr bool + }{ + { + name: "nil", + exp: `isSorted(nil)`, + want: false, + }, + { + name: "sort.Interface - not sorted", + exp: `isSorted(people_unsorted)`, + }, + { + name: "sort.Interface - sorted", + exp: `isSorted(people_sorted)`, + want: true, + }, + { + name: "int slice - not sorted", + exp: `isSorted(ints_unsorted)`, + }, + { + name: "int slice - sorted", + exp: `isSorted(ints_sorted)`, + want: true, + }, + { + name: "float slice - not sorted", + exp: `isSorted(floats_unsorted)`, + }, + { + name: "float slice - sorted", + exp: `isSorted(floats_sorted)`, + want: true, + }, + { + name: "string slice - not sorted", + exp: `isSorted(strings_unsorted)`, + }, + { + name: "string slice - sorted", + exp: `isSorted(strings_sorted)`, + want: true, + }, + { + name: "any - int slice - not sorted", + exp: `isSorted(any_unsorted)`, + }, + { + name: "any - int slice - sorted", + exp: `isSorted(any_sorted)`, + want: true, + }, + { + name: "any - mis-typed int slice", + exp: `isSorted(any_mixed_slice)`, + wantRuntimeErr: true, + }, + { + name: "unsupported type", + exp: `isSorted(v)`, + wantCompileErr: true, + }, + { + name: "no argument", + exp: `isSorted()`, + wantCompileErr: true, + }, + { + name: "too many arguments", + exp: `isSorted(ints_sorted, ints_sorted)`, + wantCompileErr: true, + }, + } + + people := []Person{ + {"Michael", 17}, + {"Jenny", 26}, + {"Bob", 31}, + {"John", 42}, + } + input := map[string]any{ + "people_sorted": ByAge(people), + "people_unsorted": func() ByAge { + ii := make([]Person, len(people)) + copy(ii, people) + ii[0], ii[1] = ii[1], ii[0] + return ii + }(), + "ints_sorted": []int{1, 2, 3, 4, 5}, + "ints_unsorted": []int{5, 4, 3, 2, 1}, + "floats_sorted": []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + "floats_unsorted": []float64{5.0, 4.0, 3.0, 2.0, 1.0}, + "strings_sorted": []string{"1", "2", "3", "4", "5"}, + "strings_unsorted": []string{"5", "4", "3", "2", "1"}, + "any_unsorted": []any{5, 4, 3, 2, 1}, + "any_sorted": []any{1, 2, 3, 4, 5}, + "any_mixed_slice": []any{1, 2, 3, "4", 5}, + "v": true, + } + opts := []expr.Option{ + expr.Env(input), + expr.AsBool(), + expr.DisableAllBuiltins(), + IsSorted(), + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + program, err := expr.Compile(tc.exp, opts...) + if tc.wantCompileErr && err == nil { + require.Error(t, err) + } + if !tc.wantCompileErr && err != nil { + require.NoError(t, err) + } + if tc.wantCompileErr { + return + } + + got, err := expr.Run(program, input) + if tc.wantRuntimeErr && err == nil { + require.Error(t, err) + } + if !tc.wantRuntimeErr && err != nil { + require.NoError(t, err) + } + if tc.wantRuntimeErr { + return + } + assert.IsType(t, tc.want, got) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/go.mod b/go.mod index d76a51c..1df8a82 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,14 @@ go 1.21 require ( github.com/expr-lang/expr v1.15.8 github.com/google/go-cmp v0.6.0 + github.com/stretchr/testify v1.8.4 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) From 4da46192971aec4f8682c308a5f7e66a0240df70 Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Wed, 10 Jan 2024 16:01:12 -0800 Subject: [PATCH 2/4] remove superfluous sort --- eval/eval_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eval/eval_test.go b/eval/eval_test.go index 93a9bff..9a75d84 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -74,7 +74,7 @@ func TestEval(t *testing.T) { }, { name: "list", - exp: `isSorted(object.items) && sum(object.items) == 6 && sort(object.items)[-1] == 3 && findIndex(object.items, # == 1) == 0`, + exp: `isSorted(object.items) && sum(object.items) == 6 && object.items[-1] == 3 && findIndex(object.items, # == 1) == 0`, want: true, }, { From b27e4196a98eb74a8f2f7e242fb9b862977829b1 Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Thu, 11 Jan 2024 01:28:53 -0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20add=20=E2=80=9Cregression=E2=80=9D?= =?UTF-8?q?=20suite=20of=20sorts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Something to run against the examples in the Playground to ensure we don’t accidentally publish an example that should work and doesn’t. --- tests/regressions_test.go | 177 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/regressions_test.go diff --git a/tests/regressions_test.go b/tests/regressions_test.go new file mode 100644 index 0000000..3ae3a4f --- /dev/null +++ b/tests/regressions_test.go @@ -0,0 +1,177 @@ +package tests + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/polds/expr-playground/eval" +) + +// TestExamples serves as a regression test for the examples presented in the playground UI. +// If any of the examples change, this test will fail, to help ensure the playground UI is +// updated accordingly, and especially so we don't accidentally push a broken sample. +func TestExamples(t *testing.T) { + examples := setup(t) + + // lookup should exactly match the "name" field in the examples.yaml file. + tests := []struct { + lookup string + want string + wantErr bool + }{ + { + lookup: "default", + want: "true", + }, + { + lookup: "Check image registry", + want: "true", + }, + { + lookup: "Disallow HostPorts", + want: "false", + }, + { + lookup: "Require non-root containers", + want: "false", + }, + { + lookup: "Drop ALL capabilities", + want: "true", + }, + { + lookup: "Semantic version check for image tags (Regex)", + want: "false", + }, + { + lookup: "URLs", + wantErr: true, + }, + { + lookup: "Check JWT custom claims", + want: "true", + }, + { + lookup: "Optional", + want: "fallback", + }, + { + lookup: "Duration and timestamp", + want: "true", + }, + { + lookup: "Quantity", + wantErr: true, + }, + { + lookup: "Access Log Filtering", + want: "true", + }, + { + lookup: "Custom Metrics", + want: "echo", + }, + { + lookup: "Blank", + wantErr: true, + }, + } + for _, tc := range tests { + t.Run(tc.lookup, func(t *testing.T) { + var exp Example + for _, e := range examples { + if e.Name == tc.lookup { + exp = e + break + } + } + if exp.Name == "" { + t.Fatalf("failed to find example %q", tc.lookup) + } + + got, err := eval.Eval(exp.Expr, marshal(t, exp.Data)) + if (err != nil) != tc.wantErr { + t.Errorf("Eval() got error %v, expected error %v", err, tc.wantErr) + } + if tc.wantErr { + return + } + + var obj map[string]AlwaysString + if err := json.Unmarshal([]byte(got), &obj); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if s := obj["result"].Value; s != tc.want { + t.Errorf("Eval() got %q, expected %q", s, tc.want) + } + }) + } + // Ensure these tests are updated when the examples are updated. + // Not a perfect solution, but it's better than nothing. + if len(examples) != len(tests) { + t.Errorf("Regression test counts got %d, expected %d", len(tests), len(examples)) + } +} + +type Example struct { + Name string `yaml:"name"` + Expr string `yaml:"expr"` + Data string `yaml:"data"` +} + +func setup(t *testing.T) []Example { + t.Helper() + + out, err := os.ReadFile("../examples.yaml") + if err != nil { + t.Fatalf("failed to read examples.yaml: %v", err) + } + var examples struct { + Examples []Example `yaml:"examples"` + } + if err := yaml.Unmarshal(out, &examples); err != nil { + t.Fatalf("failed to unmarshal examples.yaml: %v", err) + } + return examples.Examples +} + +// Attempt to get the data into either yaml or json format. +func marshal(t *testing.T, s string) map[string]any { + t.Helper() + + var v map[string]any + if yamlErr := yaml.Unmarshal([]byte(s), &v); yamlErr != nil { + if err := json.Unmarshal([]byte(s), &v); err != nil { + t.Errorf("failed to unmarshal %q as yaml: %v", s, yamlErr) + t.Fatalf("failed to unmarshal %q as json: %v", s, err) + } + } + return v +} + +// AlwaysString attempts to unmarshal the value as a string. +type AlwaysString struct { + Value string +} + +func (c *AlwaysString) UnmarshalJSON(b []byte) error { + var raw any + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + + switch v := raw.(type) { + case bool: + c.Value = strconv.FormatBool(v) + case string: + c.Value = v + default: + return fmt.Errorf("unsupported type %T", v) + } + return nil +} From b9cda44110088f8439754d6063dd073dd55b9339 Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Thu, 11 Jan 2024 01:31:29 -0800 Subject: [PATCH 4/4] Add license header --- tests/regressions_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/regressions_test.go b/tests/regressions_test.go index 3ae3a4f..f0cf726 100644 --- a/tests/regressions_test.go +++ b/tests/regressions_test.go @@ -1,3 +1,17 @@ +// Copyright 2024 Peter Olds +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package tests import (