Skip to content

Commit

Permalink
Handle null values (#398)
Browse files Browse the repository at this point in the history
* Handle null values

* Fix changelog

* Add a delete test
  • Loading branch information
TomWright authored Mar 14, 2024
1 parent e9e9d14 commit 5d94a30
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 14 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func standardFunctions() *FunctionCollection {
TypeFunc,
JoinFunc,
StringFunc,
NullFunc,

// Selectors
IndexFunc,
Expand Down
24 changes: 24 additions & 0 deletions func_null.go
Original file line number Diff line number Diff line change
@@ -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
},
}
29 changes: 29 additions & 0 deletions func_null_test.go
Original file line number Diff line number Diff line change
@@ -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,
},
),
)
}
32 changes: 23 additions & 9 deletions func_or_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions internal/command/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
))
})
}
32 changes: 32 additions & 0 deletions internal/command/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
))
})

}
25 changes: 22 additions & 3 deletions value.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
Expand All @@ -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...)
}

Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 5d94a30

Please sign in to comment.