Skip to content

Commit 5d94a30

Browse files
authored
Handle null values (#398)
* Handle null values * Fix changelog * Add a delete test
1 parent e9e9d14 commit 5d94a30

File tree

8 files changed

+153
-14
lines changed

8 files changed

+153
-14
lines changed

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9-
109
- Nothing yet.
1110

11+
## [v2.7.0] - 2024-03-14
12+
13+
### Added
14+
15+
- `null()` function. [See docs](https://daseldocs.tomwright.me/functions/null)
16+
17+
### Fixed
18+
19+
- Dasel now correctly handles `null` values.
20+
1221
## [v2.6.0] - 2024-02-15
1322

1423
### Added
@@ -655,7 +664,8 @@ See [documentation](https://daseldocs.tomwright.me) for all changes.
655664

656665
- Everything!
657666

658-
[unreleased]: https://github.com/TomWright/dasel/compare/v2.6.0...HEAD
667+
[unreleased]: https://github.com/TomWright/dasel/compare/v2.7.0...HEAD
668+
[v2.7.0]: https://github.com/TomWright/dasel/compare/v2.6.0...v2.7.0
659669
[v2.6.0]: https://github.com/TomWright/dasel/compare/v2.5.0...v2.6.0
660670
[v2.5.0]: https://github.com/TomWright/dasel/compare/v2.4.1...v2.5.0
661671
[v2.4.1]: https://github.com/TomWright/dasel/compare/v2.4.0...v2.4.1

func.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func standardFunctions() *FunctionCollection {
6060
TypeFunc,
6161
JoinFunc,
6262
StringFunc,
63+
NullFunc,
6364

6465
// Selectors
6566
IndexFunc,

func_null.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dasel
2+
3+
import (
4+
"reflect"
5+
)
6+
7+
var NullFunc = BasicFunction{
8+
name: "null",
9+
runFn: func(c *Context, s *Step, args []string) (Values, error) {
10+
if err := requireNoArgs("null", args); err != nil {
11+
return nil, err
12+
}
13+
14+
input := s.inputs()
15+
16+
res := make(Values, len(input))
17+
18+
for k, _ := range args {
19+
res[k] = ValueOf(reflect.ValueOf(new(any)).Elem())
20+
}
21+
22+
return res, nil
23+
},
24+
}

func_null_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dasel
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNullFunc(t *testing.T) {
8+
t.Run("Args", selectTestErr(
9+
"null(1)",
10+
map[string]interface{}{},
11+
&ErrUnexpectedFunctionArgs{
12+
Function: "null",
13+
Args: []string{"1"},
14+
}),
15+
)
16+
17+
original := map[string]interface{}{}
18+
19+
t.Run(
20+
"Null",
21+
selectTest(
22+
"null()",
23+
original,
24+
[]interface{}{
25+
nil,
26+
},
27+
),
28+
)
29+
}

func_or_default.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,40 @@ var OrDefaultFunc = BasicFunction{
2020

2121
runSubselect := func(value Value, selector string, defaultSelector string) (Value, error) {
2222
gotValues, err := c.subSelect(value, selector)
23+
notFound := false
2324
if err != nil {
24-
notFound := false
2525
if errors.Is(err, &ErrPropertyNotFound{}) {
2626
notFound = true
2727
} else if errors.Is(err, &ErrIndexNotFound{Index: -1}) {
2828
notFound = true
29-
}
30-
if notFound {
31-
gotValues, err = c.subSelect(value, defaultSelector)
3229
} else {
3330
return Value{}, err
3431
}
3532
}
36-
if len(gotValues) == 1 && err == nil {
37-
return gotValues[0], nil
33+
34+
if !notFound {
35+
// Check result of first query
36+
if len(gotValues) != 1 {
37+
return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value")
38+
}
39+
40+
// Consider nil values as not found
41+
if gotValues[0].IsNil() {
42+
notFound = true
43+
}
3844
}
39-
if err != nil {
40-
return Value{}, err
45+
46+
if notFound {
47+
gotValues, err = c.subSelect(value, defaultSelector)
48+
if err != nil {
49+
return Value{}, err
50+
}
51+
if len(gotValues) != 1 {
52+
return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value")
53+
}
4154
}
42-
return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value")
55+
56+
return gotValues[0], nil
4357
}
4458

4559
res := make(Values, 0)

internal/command/delete_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,14 @@ func TestDeleteCommand(t *testing.T) {
6161
nil,
6262
nil,
6363
))
64+
65+
t.Run("Issue346", func(t *testing.T) {
66+
t.Run("DeleteNullValue", runTest(
67+
[]string{"delete", "-r", "json", "foo"},
68+
[]byte(`{"foo":null}`),
69+
newline([]byte("{}")),
70+
nil,
71+
nil,
72+
))
73+
})
6474
}

internal/command/select_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,4 +441,36 @@ d.e.f`)),
441441
}
442442
})
443443

444+
t.Run("Issue346", func(t *testing.T) {
445+
t.Run("Select null or default string", runTest(
446+
[]string{"-r", "json", "orDefault(foo,string(nope))"},
447+
[]byte(`{
448+
"foo": null
449+
}`),
450+
newline([]byte(`"nope"`)),
451+
nil,
452+
nil,
453+
))
454+
455+
t.Run("Select null or default null", runTest(
456+
[]string{"-r", "json", "orDefault(foo,null())"},
457+
[]byte(`{
458+
"foo": null
459+
}`),
460+
newline([]byte(`null`)),
461+
nil,
462+
nil,
463+
))
464+
465+
t.Run("Select null value", runTest(
466+
[]string{"-r", "json", "foo"},
467+
[]byte(`{
468+
"foo": null
469+
}`),
470+
newline([]byte(`null`)),
471+
nil,
472+
nil,
473+
))
474+
})
475+
444476
}

value.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package dasel
22

33
import (
4+
"reflect"
5+
46
"github.com/tomwright/dasel/v2/dencoding"
57
"github.com/tomwright/dasel/v2/util"
6-
"reflect"
78
)
89

910
// Value is a wrapper around reflect.Value that adds some handy helper funcs.
@@ -84,6 +85,15 @@ func (v Value) IsEmpty() bool {
8485
return isEmptyReflectValue(unpackReflectValue(v.Value))
8586
}
8687

88+
func (v Value) IsNil() bool {
89+
switch v.Kind() {
90+
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
91+
return v.Value.IsNil()
92+
default:
93+
return false
94+
}
95+
}
96+
8797
func isEmptyReflectValue(v reflect.Value) bool {
8898
if (v == reflect.Value{}) {
8999
return true
@@ -123,6 +133,9 @@ func unpackReflectValue(value reflect.Value, kinds ...reflect.Kind) reflect.Valu
123133
if !containsKind(kinds, res.Kind()) {
124134
return res
125135
}
136+
if res.IsNil() {
137+
return res
138+
}
126139
res = res.Elem()
127140
}
128141
}
@@ -137,6 +150,9 @@ func (v Value) FirstAddressable() reflect.Value {
137150

138151
// Unpack returns the underlying reflect.Value after resolving any pointers or interface types.
139152
func (v Value) Unpack(kinds ...reflect.Kind) reflect.Value {
153+
if !v.Value.IsValid() {
154+
return reflect.ValueOf(new(any)).Elem()
155+
}
140156
return unpackReflectValue(v.Value, kinds...)
141157
}
142158

@@ -181,6 +197,9 @@ func (v Value) dencodingMapIndex(key Value) Value {
181197
if v, ok := om.Get(key.Value.String()); !ok {
182198
return reflect.Value{}
183199
} else {
200+
if v == nil {
201+
return reflect.ValueOf(new(any)).Elem()
202+
}
184203
return reflect.ValueOf(v)
185204
}
186205
}
@@ -498,7 +517,7 @@ func (v Values) Interfaces() []interface{} {
498517
func (v Values) initEmptydencodingMaps() Values {
499518
res := make(Values, len(v))
500519
for k, value := range v {
501-
if value.IsEmpty() {
520+
if value.IsEmpty() || value.IsNil() {
502521
res[k] = value.initEmptydencodingMap()
503522
} else {
504523
res[k] = value
@@ -510,7 +529,7 @@ func (v Values) initEmptydencodingMaps() Values {
510529
func (v Values) initEmptySlices() Values {
511530
res := make(Values, len(v))
512531
for k, value := range v {
513-
if value.IsEmpty() {
532+
if value.IsEmpty() || value.IsNil() {
514533
res[k] = value.initEmptySlice()
515534
} else {
516535
res[k] = value

0 commit comments

Comments
 (0)