Repository: TomWright/dasel
Pull request #53 (2020-12-01 12:16:26)

Extend the use of ANY_INDEX and DYNAMIC selectors to maps


Patch:
diff --git a/README.md b/README.md
index b85d610..ac305d1 100644
--- a/README.md
+++ b/README.md
@@ -190,10 +190,20 @@ You should keep this in mind as the maximum filesize it can process will be limi
 
 ### Converting between formats
 
-Dasel allows you to specify different input/output formats. This works well in general but you may see some strange behaviour
-when working with formats that don't suit the data... multi-document YAML to CSV for example.
+Dasel allows you to specify different input/output formats using the `-r`,`--read` and `-w`,`--write` flags.
 
-If you have any questions about a specific issue please raise an issue.
+E.g.
+```bash
+echo '{"name": "Tom"}{"name": "Jim"}' | dasel -r json -w yaml .
+name: Tom
+---
+name: Jim
+```
+
+This works well in general but you may run into issues when converting between data formats that don't typically play
+well together.
+
+You have any questions or concerns around this please raise an issue.
 
 ## Usage 
 
@@ -644,13 +654,13 @@ The next available index selector is used when adding to a list of items. It all
 - `colours.[]`
 
 #### Any Index
-The any index selector is used to select *all* items of a list.
+The any index selector is used to select *all* items of a list or map.
 - `colours.[*]`
 
 This must be used in conjunction with `-m`,`--multiple`.
 
 #### Dynamic
-Dynamic selectors are used with lists when you don't know the index of the item, but instead want to find the index based on some other criteria.
+Dynamic selectors are used with lists/maps when you don't know the index/property of the item, but instead want to find the index based on some other criteria.
  
 Dasel currently supports `key/query=value` checks but I aim to support more check types in the future.
 
diff --git a/node_query.go b/node_query.go
index da56636..b597357 100644
--- a/node_query.go
+++ b/node_query.go
@@ -144,7 +144,8 @@ func findValueDynamic(n *Node, createIfNotExists bool) (reflect.Value, error) {
 
 	value := unwrapValue(n.Previous.Value)
 
-	if value.Kind() == reflect.Slice {
+	switch value.Kind() {
+	case reflect.Slice:
 		for i := 0; i < value.Len(); i++ {
 			object := value.Index(i)
 			found, err := processFindDynamicItem(n, object)
@@ -162,6 +163,21 @@ func findValueDynamic(n *Node, createIfNotExists bool) (reflect.Value, error) {
 			return nilValue(), nil
 		}
 		return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
+
+	case reflect.Map:
+		for _, key := range value.MapKeys() {
+			object := value.MapIndex(key)
+			found, err := processFindDynamicItem(n, object)
+			if err != nil {
+				return nilValue(), err
+			}
+			if found {
+				n.Selector.Type = "PROPERTY"
+				n.Selector.Property = key.String()
+				return object, nil
+			}
+		}
+		return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
 	}
 
 	return nilValue(), &UnsupportedTypeForSelector{Selector: n.Selector, Value: value.Kind()}
diff --git a/node_query_multiple.go b/node_query_multiple.go
index 057b167..323c7a1 100644
--- a/node_query_multiple.go
+++ b/node_query_multiple.go
@@ -165,7 +165,8 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
 	}
 	value := unwrapValue(previousValue)
 
-	if value.Kind() == reflect.Slice {
+	switch value.Kind() {
+	case reflect.Slice:
 		results := make([]*Node, 0)
 		for i := 0; i < value.Len(); i++ {
 			object := value.Index(i)
@@ -195,6 +196,28 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo
 			}, nil
 		}
 		return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
+
+	case reflect.Map:
+		results := make([]*Node, 0)
+		for _, key := range value.MapKeys() {
+			object := value.MapIndex(key)
+			found, err := processFindDynamicItems(selector, object)
+			if err != nil {
+				return nil, err
+			}
+			if found {
+				selector.Type = "PROPERTY"
+				selector.Property = key.String()
+				results = append(results, &Node{
+					Value:    object,
+					Selector: selector,
+				})
+			}
+		}
+		if len(results) > 0 {
+			return results, nil
+		}
+		return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
 	}
 
 	return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
@@ -312,7 +335,8 @@ func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node,
 	}
 	value := unwrapValue(previousValue)
 
-	if value.Kind() == reflect.Slice {
+	switch value.Kind() {
+	case reflect.Slice:
 		results := make([]*Node, 0)
 		for i := 0; i < value.Len(); i++ {
 			object := value.Index(i)
@@ -327,6 +351,21 @@ func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node,
 			return results, nil
 		}
 		return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
+	case reflect.Map:
+		results := make([]*Node, 0)
+		for _, key := range value.MapKeys() {
+			object := value.MapIndex(key)
+			selector.Type = "PROPERTY"
+			selector.Property = key.String()
+			results = append(results, &Node{
+				Value:    object,
+				Selector: selector,
+			})
+		}
+		if len(results) > 0 {
+			return results, nil
+		}
+		return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
 	}
 
 	return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}


Test patch:
diff --git a/internal/command/put_internal_test.go b/internal/command/put_internal_test.go
index d85ea17..dcb579c 100644
--- a/internal/command/put_internal_test.go
+++ b/internal/command/put_internal_test.go
@@ -380,6 +380,20 @@ func TestPut(t *testing.T) {
   ]
 }`, nil))
 
+		t.Run("DynamicMapInt", putTest(`{
+  "numbers": {
+    "high": 3,
+	"low": 1,
+	"mid": 2
+  }
+}`, "json", ".numbers.(.=3)", "5", "int", `{
+  "numbers": {
+    "high": 5,
+    "low": 1,
+    "mid": 2
+  }
+}`, nil))
+
 		t.Run("OverwriteObject", putObjectTest(`{
   "numbers": [
     {
diff --git a/node_query_internal_test.go b/node_query_internal_test.go
index 9fe91bc..a72e492 100644
--- a/node_query_internal_test.go
+++ b/node_query_internal_test.go
@@ -54,6 +54,13 @@ func TestFindValueProperty(t *testing.T) {
 		got, err := findValueProperty(n, false)
 		assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
 	})
+	t.Run("UnsupportedType", func(t *testing.T) {
+		val := 0
+		n := getNodeWithValue(val)
+		n.Selector.Current = "x"
+		got, err := findValueProperty(n, false)
+		assertQueryResult(t, nilValue(), &UnsupportedTypeForSelector{Selector: n.Selector, Value: reflect.TypeOf(val).Kind()}, got, err)
+	})
 }
 
 func TestFindValueIndex(t *testing.T) {
@@ -106,6 +113,15 @@ func TestFindValueDynamic(t *testing.T) {
 		got, err := findValueDynamic(n, false)
 		assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
 	})
+	t.Run("NotFoundMap", func(t *testing.T) {
+		n := getNodeWithValue(map[string]interface{}{})
+		n.Selector.Current = "(name=x)"
+		n.Selector.Conditions = []Condition{
+			&EqualCondition{Key: "name", Value: "x"},
+		}
+		got, err := findValueDynamic(n, false)
+		assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
+	})
 	t.Run("NotFoundWithCreate", func(t *testing.T) {
 		n := getNodeWithValue([]interface{}{})
 		n.Selector.Current = "(name=x)"
@@ -134,8 +150,21 @@ func TestFindValueDynamic(t *testing.T) {
 		got, err := findValueDynamic(n, false)
 		assertQueryResult(t, nilValue(), &UnhandledCheckType{Value: reflect.TypeOf(itemVal).Kind().String()}, got, err)
 	})
+	t.Run("UnsupportedCheckTypeMap", func(t *testing.T) {
+		itemVal := 1
+		val := map[string]interface{}{
+			"x": itemVal,
+		}
+		n := getNodeWithValue(val)
+		n.Selector.Current = "(name=x)"
+		n.Selector.Conditions = []Condition{
+			&EqualCondition{Key: "name", Value: "x"},
+		}
+		got, err := findValueDynamic(n, false)
+		assertQueryResult(t, nilValue(), &UnhandledCheckType{Value: reflect.TypeOf(itemVal).Kind().String()}, got, err)
+	})
 	t.Run("UnsupportedType", func(t *testing.T) {
-		val := map[string]interface{}{}
+		val := 0
 		n := getNodeWithValue(val)
 		n.Selector.Current = "(name=x)"
 		n.Selector.Conditions = []Condition{
diff --git a/node_query_multiple_internal_test.go b/node_query_multiple_internal_test.go
index 4f04fc2..8e46c8f 100644
--- a/node_query_multiple_internal_test.go
+++ b/node_query_multiple_internal_test.go
@@ -35,6 +35,12 @@ func TestFindNodesProperty(t *testing.T) {
 		got, err := findNodesProperty(selector, previousValue, false)
 		assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
 	})
+	t.Run("UnsupportedType", func(t *testing.T) {
+		previousValue := reflect.ValueOf(0)
+		selector := Selector{Current: "x"}
+		got, err := findNodesProperty(selector, previousValue, false)
+		assertQueryMultipleResult(t, []reflect.Value{}, &UnsupportedTypeForSelector{Selector: selector, Value: previousValue.Kind()}, got, err)
+	})
 }
 
 func TestFindNodesIndex(t *testing.T) {
@@ -57,6 +63,32 @@ func TestFindNodesIndex(t *testing.T) {
 	})
 }
 
+func TestFindNodesAnyIndex(t *testing.T) {
+	t.Run("NilValue", func(t *testing.T) {
+		selector := Selector{Current: "[*]", Raw: ".[*]"}
+		got, err := findNodesAnyIndex(selector, nilValue())
+		assertQueryMultipleResult(t, []reflect.Value{}, &UnexpectedPreviousNilValue{Selector: ".[*]"}, got, err)
+	})
+	t.Run("NotFound", func(t *testing.T) {
+		selector := Selector{Current: "[*]", Raw: ".[*]"}
+		previousValue := reflect.ValueOf([]interface{}{})
+		got, err := findNodesAnyIndex(selector, previousValue)
+		assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: "[*]", PreviousValue: previousValue}, got, err)
+	})
+	t.Run("NotFoundMap", func(t *testing.T) {
+		selector := Selector{Current: "[*]", Raw: ".[*]"}
+		previousValue := reflect.ValueOf(map[string]interface{}{})
+		got, err := findNodesAnyIndex(selector, previousValue)
+		assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: "[*]", PreviousValue: previousValue}, got, err)
+	})
+	t.Run("UnsupportedType", func(t *testing.T) {
+		selector := Selector{Current: "[*]", Raw: ".[*]"}
+		previousValue := reflect.ValueOf(0)
+		got, err := findNodesAnyIndex(selector, previousValue)
+		assertQueryMultipleResult(t, []reflect.Value{}, &UnsupportedTypeForSelector{Selector: selector, Value: previousValue.Kind()}, got, err)
+	})
+}
+
 func TestFindNextAvailableIndexNodes(t *testing.T) {
 	t.Run("NotFound", func(t *testing.T) {
 		previousValue := reflect.ValueOf([]interface{}{})
@@ -84,6 +116,17 @@ func TestFindNodesDynamic(t *testing.T) {
 		got, err := findNodesDynamic(selector, previousValue, false)
 		assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
 	})
+	t.Run("NotFoundMap", func(t *testing.T) {
+		previousValue := reflect.ValueOf(map[string]interface{}{})
+		selector := Selector{
+			Current: "(name=x)",
+			Conditions: []Condition{
+				&EqualCondition{Key: "name", Value: "x"},
+			},
+		}
+		got, err := findNodesDynamic(selector, previousValue, false)
+		assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
+	})
 	t.Run("NotFoundWithCreate", func(t *testing.T) {
 		previousValue := reflect.ValueOf([]interface{}{})
 		selector := Selector{
@@ -115,8 +158,21 @@ func TestFindNodesDynamic(t *testing.T) {
 		got, err := findNodesDynamic(selector, previousValue, false)
 		assertQueryMultipleResult(t, []reflect.Value{}, &UnhandledCheckType{Value: previousValue.Kind().String()}, got, err)
 	})
+	t.Run("UnsupportedCheckTypeMap", func(t *testing.T) {
+		previousValue := reflect.ValueOf(map[string]interface{}{
+			"x": 1,
+		})
+		selector := Selector{
+			Current: "(name=x)",
+			Conditions: []Condition{
+				&EqualCondition{Key: "name", Value: "x"},
+			},
+		}
+		got, err := findNodesDynamic(selector, previousValue, false)
+		assertQueryMultipleResult(t, []reflect.Value{}, &UnhandledCheckType{Value: previousValue.Kind().String()}, got, err)
+	})
 	t.Run("UnsupportedType", func(t *testing.T) {
-		previousValue := reflect.ValueOf(map[string]interface{}{})
+		previousValue := reflect.ValueOf(0)
 		selector := Selector{
 			Current: "(name=x)",
 			Conditions: []Condition{
diff --git a/node_test.go b/node_test.go
index 3340734..65d15b2 100644
--- a/node_test.go
+++ b/node_test.go
@@ -209,6 +209,20 @@ func TestNode_QueryMultiple(t *testing.T) {
 			"age":  "25",
 		},
 	}
+	value2 := map[string]interface{}{
+		"personA": map[string]interface{}{
+			"name": "Tom",
+			"age":  27,
+		},
+		"personB": map[string]interface{}{
+			"name": "Jim",
+			"age":  27,
+		},
+		"personC": map[string]interface{}{
+			"name": "Amelia",
+			"age":  25,
+		},
+	}
 	extractValues := func(nodes []*dasel.Node) []interface{} {
 		gotValues := make([]interface{}, len(nodes))
 		for i, n := range nodes {
@@ -283,6 +297,92 @@ func TestNode_QueryMultiple(t *testing.T) {
 			t.Errorf("expected %v, got %v", expValues, gotValues)
 		}
 	})
+	t.Run("MapsSingleResult", func(t *testing.T) {
+		nodes, err := dasel.New(value2).QueryMultiple(".personA.name")
+		if err != nil {
+			t.Errorf("unexpected query error: %s", err)
+			return
+		}
+
+		gotValues := extractValues(nodes)
+		expValues := []interface{}{
+			"Tom",
+		}
+
+		if !valuesAreEqual(expValues, gotValues) {
+			t.Errorf("expected %v, got %v", expValues, gotValues)
+		}
+	})
+	t.Run("MapsSingleResultDynamic", func(t *testing.T) {
+		nodes, err := dasel.New(value2).QueryMultiple(".(age=25).name")
+		if err != nil {
+			t.Errorf("unexpected query error: %s", err)
+			return
+		}
+
+		gotValues := extractValues(nodes)
+		expValues := []interface{}{
+			"Amelia",
+		}
+
+		if !valuesAreEqual(expValues, gotValues) {
+			t.Errorf("expected %v, got %v", expValues, gotValues)
+		}
+	})
+	t.Run("MapsMultipleResultDynamic", func(t *testing.T) {
+		nodes, err := dasel.New(value2).QueryMultiple(".(age=27).name")
+		if err != nil {
+			t.Errorf("unexpected query error: %s", err)
+			return
+		}
+
+		gotValues := extractValues(nodes)
+		expValues := []interface{}{
+			"Tom",
+			"Jim",
+		}
+
+		if !valuesAreEqual(expValues, gotValues) {
+			t.Errorf("expected %v, got %v", expValues, gotValues)
+		}
+	})
+	t.Run("MapsMultipleResultAnyIndex", func(t *testing.T) {
+		nodes, err := dasel.New(value2).QueryMultiple(".[*].name")
+		if err != nil {
+			t.Errorf("unexpected query error: %s", err)
+			return
+		}
+
+		gotValues := extractValues(nodes)
+		expValues := []interface{}{
+			"Tom",
+			"Jim",
+			"Amelia",
+		}
+
+		if !valuesAreEqual(expValues, gotValues) {
+			t.Errorf("expected %v, got %v", expValues, gotValues)
+		}
+	})
+}
+
+func valuesAreEqual(exp []interface{}, got []interface{}) bool {
+	if len(exp) != len(got) {
+		return false
+	}
+	for _, g := range got {
+		found := false
+		for _, e := range exp {
+			if reflect.DeepEqual(g, e) {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
 }
 
 func TestNode_PutMultiple(t *testing.T) {
@@ -603,6 +703,26 @@ func TestNode_Query_Data(t *testing.T) {
 			return
 		}
 
+		if exp, got := "Tom", got.InterfaceValue().(string); exp != got {
+			t.Errorf("expected %s, got %s", exp, got)
+		}
+	})
+	t.Run("DynamicPropertyOnMap", func(t *testing.T) {
+		rootNode := dasel.New(map[string]interface{}{
+			"personA": map[string]interface{}{
+				"name": "Tom",
+			},
+			"personB": map[string]interface{}{
+				"name": "Jim",
+			},
+		})
+
+		got, err := rootNode.Query(".(name=Tom).name")
+		if err != nil {
+			t.Errorf("unexpected query error: %v", err)
+			return
+		}
+
 		if exp, got := "Tom", got.InterfaceValue().(string); exp != got {
 			t.Errorf("expected %s, got %s", exp, got)
 		}
@@ -790,5 +910,4 @@ func TestNode_PutMultiple_Query(t *testing.T) {
 	t.Run("NilChain", putQueryMultipleTest(dasel.New(nil), "my.name", "Thomas", "my.name"))
 	t.Run("NilChainToListIndex", putQueryMultipleTest(dasel.New(nil), "my.favourite.people.[0]", "Tom", "my.favourite.people.[0]"))
 	t.Run("NilChainToListNextAvailableIndex", putQueryMultipleTest(dasel.New(nil), "my.favourite.people.[]", "Tom", "my.favourite.people.[0]"))
-	t.Run("NilChainToDynamic", putQueryMultipleTest(dasel.New(nil), "(name=Jim).name", "Tom", "[0].name"))
 }