Repository: TomWright/dasel
Pull request #276 (2022-12-27 14:00:16)

Add join func


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54bd8e9..75304ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-- Nothing yet.
+### Added
+
+- Join function.
+- String function.
 
 ## [v2.1.1] - 2023-01-19
 
diff --git a/func.go b/func.go
index 04be83f..2c69d36 100644
--- a/func.go
+++ b/func.go
@@ -57,6 +57,8 @@ func standardFunctions() *FunctionCollection {
 		CountFunc,
 		MapOfFunc,
 		TypeFunc,
+		JoinFunc,
+		StringFunc,
 
 		// Selectors
 		IndexFunc,
diff --git a/func_join.go b/func_join.go
new file mode 100644
index 0000000..f106def
--- /dev/null
+++ b/func_join.go
@@ -0,0 +1,60 @@
+package dasel
+
+import (
+	"fmt"
+	"strings"
+)
+
+var JoinFunc = BasicFunction{
+	name: "join",
+	runFn: func(c *Context, s *Step, args []string) (Values, error) {
+		if err := requireXOrMoreArgs("join", args, 1); err != nil {
+			return nil, err
+		}
+
+		input := s.inputs()
+
+		getValues := func(value Value, selector string) ([]string, error) {
+			gotValues, err := c.subSelect(value, selector)
+			if err != nil {
+				return []string{}, err
+			}
+
+			res := make([]string, len(gotValues))
+			for k, v := range gotValues {
+				res[k] = fmt.Sprint(v.Interface())
+			}
+			return res, nil
+		}
+
+		res := make(Values, 0)
+
+		separator := args[0]
+		args = args[1:]
+
+		// No args - join all input values
+		if len(args) == 0 {
+			values := make([]string, len(input))
+			for k, v := range input {
+				values[k] = fmt.Sprint(v.Interface())
+			}
+			res = append(res, ValueOf(strings.Join(values, separator)))
+			return res, nil
+		}
+
+		// There are args - use each as a selector and join any resulting values.
+		values := make([]string, 0)
+		for _, val := range input {
+			for _, cmp := range args {
+				vals, err := getValues(val, cmp)
+				if err != nil {
+					return nil, err
+				}
+				values = append(values, vals...)
+			}
+		}
+		res = append(res, ValueOf(strings.Join(values, separator)))
+
+		return res, nil
+	},
+}
diff --git a/func_string.go b/func_string.go
new file mode 100644
index 0000000..203c2f2
--- /dev/null
+++ b/func_string.go
@@ -0,0 +1,24 @@
+package dasel
+
+import (
+	"fmt"
+)
+
+var StringFunc = BasicFunction{
+	name: "string",
+	runFn: func(c *Context, s *Step, args []string) (Values, error) {
+		if err := requireExactlyXArgs("string", args, 1); err != nil {
+			return nil, err
+		}
+
+		input := s.inputs()
+
+		res := make(Values, len(input))
+
+		for k, v := range args {
+			res[k] = ValueOf(fmt.Sprint(v))
+		}
+
+		return res, nil
+	},
+}


Test patch:
diff --git a/context_test.go b/context_test.go
index 7153a1f..09c6a92 100644
--- a/context_test.go
+++ b/context_test.go
@@ -50,6 +50,21 @@ func selectTest(selector string, original interface{}, exp []interface{}) func(t
 	}
 }
 
+func selectTestAssert(selector string, original interface{}, assertFn func(t *testing.T, got []any)) func(t *testing.T) {
+	return func(t *testing.T) {
+		c := newSelectContext(original, selector)
+
+		values, err := c.Run()
+		if err != nil {
+			t.Errorf("unexpected error: %v", err)
+			return
+		}
+
+		got := values.Interfaces()
+		assertFn(t, got)
+	}
+}
+
 func selectTestErr(selector string, original interface{}, expErr error) func(t *testing.T) {
 	return func(t *testing.T) {
 		c := newSelectContext(original, selector)
diff --git a/func_join_test.go b/func_join_test.go
new file mode 100644
index 0000000..95fb673
--- /dev/null
+++ b/func_join_test.go
@@ -0,0 +1,171 @@
+package dasel
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestJoinFunc(t *testing.T) {
+	t.Run("Args", selectTestErr(
+		"join()",
+		map[string]interface{}{},
+		&ErrUnexpectedFunctionArgs{
+			Function: "join",
+			Args:     []string{},
+		}),
+	)
+
+	original := map[string]interface{}{
+		"name": map[string]interface{}{
+			"first": "Tom",
+			"last":  "Wright",
+		},
+		"colours": []interface{}{
+			"red", "green", "blue",
+		},
+	}
+
+	t.Run(
+		"JoinCommaSeparator",
+		selectTestAssert(
+			"name.all().join(\\,)",
+			original,
+			func(t *testing.T, got []any) {
+				required := []string{"Tom", "Wright"}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+				str, ok := got[0].(string)
+				if !ok {
+					t.Errorf("expected 1st result to be a string, got %T", got[0])
+					return
+				}
+
+				gotStrs := strings.Split(str, ",")
+				for _, req := range required {
+					found := false
+					for _, got := range gotStrs {
+						if got == req {
+							found = true
+							continue
+						}
+					}
+					if !found {
+						t.Errorf("expected %v, got %v", required, got)
+					}
+				}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+			},
+		),
+	)
+
+	t.Run(
+		"JoinSpaceSeparator",
+		selectTestAssert(
+			"name.all().join( )",
+			original,
+			func(t *testing.T, got []any) {
+				required := []string{"Tom", "Wright"}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+				str, ok := got[0].(string)
+				if !ok {
+					t.Errorf("expected 1st result to be a string, got %T", got[0])
+					return
+				}
+
+				gotStrs := strings.Split(str, " ")
+				for _, req := range required {
+					found := false
+					for _, got := range gotStrs {
+						if got == req {
+							found = true
+							continue
+						}
+					}
+					if !found {
+						t.Errorf("expected %v, got %v", required, got)
+					}
+				}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+			},
+		),
+	)
+
+	t.Run(
+		"JoinWithSeparatorsAndSelectors",
+		selectTest(
+			"name.join( ,last,first)",
+			original,
+			[]interface{}{
+				"Wright Tom",
+			},
+		),
+	)
+
+	t.Run(
+		"JoinInMap",
+		selectTest(
+			"mapOf(first,name.first,last,name.last,full,name.join( ,string(Mr),first,last))",
+			original,
+			[]interface{}{
+				map[string]interface{}{
+					"first": "Tom",
+					"full":  "Mr Tom Wright",
+					"last":  "Wright",
+				},
+			},
+		),
+	)
+
+	t.Run(
+		"JoinManyLists",
+		selectTestAssert(
+			"all().join(\\,,all())",
+			map[string]interface{}{
+				"x": []interface{}{1, 2, 3},
+				"y": []interface{}{4, 5, 6},
+				"z": []interface{}{7, 8, 9},
+			},
+			func(t *testing.T, got []any) {
+				required := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+				str, ok := got[0].(string)
+				if !ok {
+					t.Errorf("expected 1st result to be a string, got %T", got[0])
+					return
+				}
+
+				gotStrs := strings.Split(str, ",")
+				for _, req := range required {
+					found := false
+					for _, got := range gotStrs {
+						if got == req {
+							found = true
+							continue
+						}
+					}
+					if !found {
+						t.Errorf("expected %v, got %v", required, got)
+					}
+				}
+				if len(got) != 1 {
+					t.Errorf("expected 1 result, got %v", got)
+					return
+				}
+			},
+		),
+	)
+}
diff --git a/func_string_test.go b/func_string_test.go
new file mode 100644
index 0000000..f7bc43c
--- /dev/null
+++ b/func_string_test.go
@@ -0,0 +1,40 @@
+package dasel
+
+import (
+	"testing"
+)
+
+func TestStringFunc(t *testing.T) {
+	t.Run("Args", selectTestErr(
+		"string()",
+		map[string]interface{}{},
+		&ErrUnexpectedFunctionArgs{
+			Function: "string",
+			Args:     []string{},
+		}),
+	)
+
+	original := map[string]interface{}{}
+
+	t.Run(
+		"String",
+		selectTest(
+			"string(x)",
+			original,
+			[]interface{}{
+				"x",
+			},
+		),
+	)
+
+	t.Run(
+		"Comma",
+		selectTest(
+			"string(\\,)",
+			original,
+			[]interface{}{
+				",",
+			},
+		),
+	)
+}