diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f715f5..63196d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - - Nothing yet. +## [v2.7.0] - 2024-03-14 + +### Added + +- `null()` function. [See docs](https://daseldocs.tomwright.me/functions/null) + +### Fixed + +- Dasel now correctly handles `null` values. + ## [v2.6.0] - 2024-02-15 ### Added @@ -655,7 +664,8 @@ See [documentation](https://daseldocs.tomwright.me) for all changes. - Everything! -[unreleased]: https://github.com/TomWright/dasel/compare/v2.6.0...HEAD +[unreleased]: https://github.com/TomWright/dasel/compare/v2.7.0...HEAD +[v2.7.0]: https://github.com/TomWright/dasel/compare/v2.6.0...v2.7.0 [v2.6.0]: https://github.com/TomWright/dasel/compare/v2.5.0...v2.6.0 [v2.5.0]: https://github.com/TomWright/dasel/compare/v2.4.1...v2.5.0 [v2.4.1]: https://github.com/TomWright/dasel/compare/v2.4.0...v2.4.1 diff --git a/func.go b/func.go index f95c214d..7f01e2a8 100644 --- a/func.go +++ b/func.go @@ -60,6 +60,7 @@ func standardFunctions() *FunctionCollection { TypeFunc, JoinFunc, StringFunc, + NullFunc, // Selectors IndexFunc, diff --git a/func_null.go b/func_null.go new file mode 100644 index 00000000..aacec2ca --- /dev/null +++ b/func_null.go @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..e68a65b4 --- /dev/null +++ b/func_null_test.go @@ -0,0 +1,29 @@ +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_default.go b/func_or_default.go index 49fc8dd3..0b26f07b 100644 --- a/func_or_default.go +++ b/func_or_default.go @@ -20,26 +20,40 @@ var OrDefaultFunc = BasicFunction{ runSubselect := func(value Value, selector string, defaultSelector string) (Value, error) { gotValues, err := c.subSelect(value, selector) + notFound := false if err != nil { - notFound := false if errors.Is(err, &ErrPropertyNotFound{}) { notFound = true } else if errors.Is(err, &ErrIndexNotFound{Index: -1}) { notFound = true - } - if notFound { - gotValues, err = c.subSelect(value, defaultSelector) } else { return Value{}, err } } - if len(gotValues) == 1 && err == nil { - return gotValues[0], nil + + 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 err != nil { - return Value{}, err + + 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 Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value") + + return gotValues[0], nil } res := make(Values, 0) diff --git a/internal/command/delete_test.go b/internal/command/delete_test.go index f06aa55f..9399d008 100644 --- a/internal/command/delete_test.go +++ b/internal/command/delete_test.go @@ -61,4 +61,14 @@ func TestDeleteCommand(t *testing.T) { 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/select_test.go b/internal/command/select_test.go index c9fccc02..87a0c2e9 100644 --- a/internal/command/select_test.go +++ b/internal/command/select_test.go @@ -441,4 +441,36 @@ d.e.f`)), } }) + 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/value.go b/value.go index 0745c104..da7081b8 100644 --- a/value.go +++ b/value.go @@ -1,9 +1,10 @@ package dasel import ( + "reflect" + "github.com/tomwright/dasel/v2/dencoding" "github.com/tomwright/dasel/v2/util" - "reflect" ) // Value is a wrapper around reflect.Value that adds some handy helper funcs. @@ -84,6 +85,15 @@ 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 @@ -123,6 +133,9 @@ func unpackReflectValue(value reflect.Value, kinds ...reflect.Kind) reflect.Valu if !containsKind(kinds, res.Kind()) { return res } + if res.IsNil() { + return res + } res = res.Elem() } } @@ -137,6 +150,9 @@ func (v Value) FirstAddressable() reflect.Value { // 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...) } @@ -181,6 +197,9 @@ func (v Value) dencodingMapIndex(key Value) Value { 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) } } @@ -498,7 +517,7 @@ func (v Values) Interfaces() []interface{} { func (v Values) initEmptydencodingMaps() Values { res := make(Values, len(v)) for k, value := range v { - if value.IsEmpty() { + if value.IsEmpty() || value.IsNil() { res[k] = value.initEmptydencodingMap() } else { res[k] = value @@ -510,7 +529,7 @@ func (v Values) initEmptydencodingMaps() Values { func (v Values) initEmptySlices() Values { res := make(Values, len(v)) for k, value := range v { - if value.IsEmpty() { + if value.IsEmpty() || value.IsNil() { res[k] = value.initEmptySlice() } else { res[k] = value