1
0
mirror of https://github.com/taigrr/yq synced 2025-01-18 04:53:17 -08:00

Move implementation files to yqlib and test packages to allow for imports:

- Move data_navigator, json_converter, merge, and path_parser to pkg/yqlib
- Extract yamlToString from yq to pkg/yqlib/yaml_converter
- Move utils_test to test/utils
This commit is contained in:
Conor Nosal
2019-11-22 22:52:29 -05:00
committed by Mike Farah
parent ceafed30f9
commit 26a09e6ec0
15 changed files with 641 additions and 452 deletions

349
pkg/yqlib/data_navigator.go Normal file
View File

@@ -0,0 +1,349 @@
package yqlib
import (
"fmt"
"reflect"
"strconv"
"strings"
yaml "github.com/mikefarah/yaml/v2"
)
func matchesKey(key string, actual interface{}) bool {
var actualString = fmt.Sprintf("%v", actual)
var prefixMatch = strings.TrimSuffix(key, "*")
if prefixMatch != key {
return strings.HasPrefix(actualString, prefixMatch)
}
return actualString == key
}
func entriesInSlice(context yaml.MapSlice, key string) []*yaml.MapItem {
var matches = make([]*yaml.MapItem, 0)
for idx := range context {
var entry = &context[idx]
if matchesKey(key, entry.Key) {
matches = append(matches, entry)
}
}
return matches
}
func getMapSlice(context interface{}) yaml.MapSlice {
var mapSlice yaml.MapSlice
switch context := context.(type) {
case yaml.MapSlice:
mapSlice = context
default:
mapSlice = make(yaml.MapSlice, 0)
}
return mapSlice
}
func getArray(context interface{}) (array []interface{}, ok bool) {
switch context := context.(type) {
case []interface{}:
array = context
ok = true
default:
array = make([]interface{}, 0)
ok = false
}
return
}
func writeMap(context interface{}, paths []string, value interface{}) interface{} {
log.Debugf("writeMap with path %v for %v to set value %v\n", paths, context, value)
mapSlice := getMapSlice(context)
if len(paths) == 0 {
return context
}
children := entriesInSlice(mapSlice, paths[0])
if len(children) == 0 && paths[0] == "*" {
log.Debugf("\tNo matches, return map as is")
return context
}
if len(children) == 0 {
newChild := yaml.MapItem{Key: paths[0]}
mapSlice = append(mapSlice, newChild)
children = entriesInSlice(mapSlice, paths[0])
log.Debugf("\tAppended child at %v for mapSlice %v\n", paths[0], mapSlice)
}
remainingPaths := paths[1:]
for _, child := range children {
child.Value = UpdatedChildValue(child.Value, remainingPaths, value)
}
log.Debugf("\tReturning mapSlice %v\n", mapSlice)
return mapSlice
}
func UpdatedChildValue(child interface{}, remainingPaths []string, value interface{}) interface{} {
if len(remainingPaths) == 0 {
return value
}
log.Debugf("UpdatedChildValue for child %v with path %v to set value %v", child, remainingPaths, value)
log.Debugf("type of child is %v", reflect.TypeOf(child))
switch child := child.(type) {
case nil:
if remainingPaths[0] == "+" || remainingPaths[0] == "*" {
return writeArray(child, remainingPaths, value)
}
case []interface{}:
_, nextIndexErr := strconv.ParseInt(remainingPaths[0], 10, 64)
arrayCommand := nextIndexErr == nil || remainingPaths[0] == "+" || remainingPaths[0] == "*"
if arrayCommand {
return writeArray(child, remainingPaths, value)
}
}
return writeMap(child, remainingPaths, value)
}
func writeArray(context interface{}, paths []string, value interface{}) []interface{} {
log.Debugf("writeArray with path %v for %v to set value %v\n", paths, context, value)
array, _ := getArray(context)
if len(paths) == 0 {
return array
}
log.Debugf("\tarray %v\n", array)
rawIndex := paths[0]
remainingPaths := paths[1:]
var index int64
// the append array indicator
if rawIndex == "+" {
index = int64(len(array))
} else if rawIndex == "*" {
for index, oldChild := range array {
array[index] = UpdatedChildValue(oldChild, remainingPaths, value)
}
return array
} else {
index, _ = strconv.ParseInt(rawIndex, 10, 64) // nolint
// writeArray is only called by UpdatedChildValue which handles parsing the
// index, as such this renders this dead code.
}
for index >= int64(len(array)) {
array = append(array, nil)
}
currentChild := array[index]
log.Debugf("\tcurrentChild %v\n", currentChild)
array[index] = UpdatedChildValue(currentChild, remainingPaths, value)
log.Debugf("\tReturning array %v\n", array)
return array
}
func readMap(context yaml.MapSlice, head string, tail []string) (interface{}, error) {
log.Debugf("readingMap %v with key %v\n", context, head)
if head == "*" {
return readMapSplat(context, tail)
}
entries := entriesInSlice(context, head)
if len(entries) == 1 {
return calculateValue(entries[0].Value, tail)
} else if len(entries) == 0 {
return nil, nil
}
var errInIdx error
values := make([]interface{}, len(entries))
for idx, entry := range entries {
values[idx], errInIdx = calculateValue(entry.Value, tail)
if errInIdx != nil {
log.Errorf("Error updating index %v in %v", idx, context)
return nil, errInIdx
}
}
return values, nil
}
func readMapSplat(context yaml.MapSlice, tail []string) (interface{}, error) {
var newArray = make([]interface{}, len(context))
var i = 0
for _, entry := range context {
if len(tail) > 0 {
val, err := Recurse(entry.Value, tail[0], tail[1:])
if err != nil {
return nil, err
}
newArray[i] = val
} else {
newArray[i] = entry.Value
}
i++
}
return newArray, nil
}
func Recurse(value interface{}, head string, tail []string) (interface{}, error) {
switch value := value.(type) {
case []interface{}:
if head == "*" {
return readArraySplat(value, tail)
}
index, err := strconv.ParseInt(head, 10, 64)
if err != nil {
return nil, fmt.Errorf("error accessing array: %v", err)
}
return readArray(value, index, tail)
case yaml.MapSlice:
return readMap(value, head, tail)
default:
return nil, nil
}
}
func readArray(array []interface{}, head int64, tail []string) (interface{}, error) {
if head >= int64(len(array)) {
return nil, nil
}
value := array[head]
return calculateValue(value, tail)
}
func readArraySplat(array []interface{}, tail []string) (interface{}, error) {
var newArray = make([]interface{}, len(array))
for index, value := range array {
val, err := calculateValue(value, tail)
if err != nil {
return nil, err
}
newArray[index] = val
}
return newArray, nil
}
func calculateValue(value interface{}, tail []string) (interface{}, error) {
if len(tail) > 0 {
return Recurse(value, tail[0], tail[1:])
}
return value, nil
}
func deleteMap(context interface{}, paths []string) (yaml.MapSlice, error) {
log.Debugf("deleteMap for %v for %v\n", paths, context)
mapSlice := getMapSlice(context)
if len(paths) == 0 {
return mapSlice, nil
}
var index int
var child yaml.MapItem
for index, child = range mapSlice {
if matchesKey(paths[0], child.Key) {
log.Debugf("\tMatched [%v] with [%v] at index %v", paths[0], child.Key, index)
var badDelete error
mapSlice, badDelete = deleteEntryInMap(mapSlice, child, index, paths)
if badDelete != nil {
return nil, badDelete
}
}
}
return mapSlice, nil
}
func deleteEntryInMap(original yaml.MapSlice, child yaml.MapItem, index int, paths []string) (yaml.MapSlice, error) {
remainingPaths := paths[1:]
var newSlice yaml.MapSlice
if len(remainingPaths) > 0 {
newChild := yaml.MapItem{Key: child.Key}
var errorDeleting error
newChild.Value, errorDeleting = DeleteChildValue(child.Value, remainingPaths)
if errorDeleting != nil {
return nil, errorDeleting
}
newSlice = make(yaml.MapSlice, len(original))
for i := range original {
item := original[i]
if i == index {
item = newChild
}
newSlice[i] = item
}
} else {
// Delete item from slice at index
newSlice = append(original[:index], original[index+1:]...)
log.Debugf("\tDeleted item index %d from original", index)
}
log.Debugf("\tReturning original %v\n", original)
return newSlice, nil
}
func deleteArraySplat(array []interface{}, tail []string) (interface{}, error) {
log.Debugf("deleteArraySplat for %v for %v\n", tail, array)
var newArray = make([]interface{}, len(array))
for index, value := range array {
val, err := DeleteChildValue(value, tail)
if err != nil {
return nil, err
}
newArray[index] = val
}
return newArray, nil
}
func deleteArray(array []interface{}, paths []string, index int64) (interface{}, error) {
log.Debugf("deleteArray for %v for %v\n", paths, array)
if index >= int64(len(array)) {
return array, nil
}
remainingPaths := paths[1:]
if len(remainingPaths) > 0 {
// Recurse into the array element at index
var errorDeleting error
array[index], errorDeleting = deleteMap(array[index], remainingPaths)
if errorDeleting != nil {
return nil, errorDeleting
}
} else {
// Delete the array element at index
array = append(array[:index], array[index+1:]...)
log.Debugf("\tDeleted item index %d from array, leaving %v", index, array)
}
log.Debugf("\tReturning array: %v\n", array)
return array, nil
}
func DeleteChildValue(child interface{}, remainingPaths []string) (interface{}, error) {
log.Debugf("DeleteChildValue for %v for %v\n", remainingPaths, child)
var head = remainingPaths[0]
var tail = remainingPaths[1:]
switch child := child.(type) {
case yaml.MapSlice:
return deleteMap(child, remainingPaths)
case []interface{}:
if head == "*" {
return deleteArraySplat(child, tail)
}
index, err := strconv.ParseInt(head, 10, 64)
if err != nil {
return nil, fmt.Errorf("error accessing array: %v", err)
}
return deleteArray(child, remainingPaths, index)
}
return child, nil
}

View File

@@ -0,0 +1,400 @@
package yqlib
import (
"fmt"
"sort"
"testing"
"github.com/mikefarah/yq/test"
)
func TestReadMap_simple(t *testing.T) {
var data = test.ParseData(`
---
b:
c: 2
`)
got, _ := readMap(data, "b", []string{"c"})
test.AssertResult(t, 2, got)
}
func TestReadMap_numberKey(t *testing.T) {
var data = test.ParseData(`
---
200: things
`)
got, _ := readMap(data, "200", []string{})
test.AssertResult(t, "things", got)
}
func TestReadMap_splat(t *testing.T) {
var data = test.ParseData(`
---
mapSplat:
item1: things
item2: whatever
otherThing: cat
`)
res, _ := readMap(data, "mapSplat", []string{"*"})
test.AssertResult(t, "[things whatever cat]", fmt.Sprintf("%v", res))
}
func TestReadMap_prefixSplat(t *testing.T) {
var data = test.ParseData(`
---
mapSplat:
item1: things
item2: whatever
otherThing: cat
`)
res, _ := readMap(data, "mapSplat", []string{"item*"})
test.AssertResult(t, "[things whatever]", fmt.Sprintf("%v", res))
}
func TestReadMap_deep_splat(t *testing.T) {
var data = test.ParseData(`
---
mapSplatDeep:
item1:
cats: bananas
item2:
cats: apples
`)
res, _ := readMap(data, "mapSplatDeep", []string{"*", "cats"})
result := res.([]interface{})
var actual = []string{result[0].(string), result[1].(string)}
sort.Strings(actual)
test.AssertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual))
}
func TestReadMap_key_doesnt_exist(t *testing.T) {
var data = test.ParseData(`
---
b:
c: 2
`)
got, _ := readMap(data, "b.x.f", []string{"c"})
test.AssertResult(t, nil, got)
}
func TestReadMap_recurse_against_string(t *testing.T) {
var data = test.ParseData(`
---
a: cat
`)
got, _ := readMap(data, "a", []string{"b"})
test.AssertResult(t, nil, got)
}
func TestReadMap_with_array(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
- 3
- 4
`)
got, _ := readMap(data, "b", []string{"d", "1"})
test.AssertResult(t, 4, got)
}
func TestReadMap_with_array_and_bad_index(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
- 3
- 4
`)
_, err := readMap(data, "b", []string{"d", "x"})
if err == nil {
t.Fatal("Expected error due to invalid path")
}
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
test.AssertResult(t, expectedOutput, err.Error())
}
func TestReadMap_with_mapsplat_array_and_bad_index(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
e:
- 3
- 4
f:
- 1
- 2
`)
_, err := readMap(data, "b", []string{"d", "*", "x"})
if err == nil {
t.Fatal("Expected error due to invalid path")
}
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
test.AssertResult(t, expectedOutput, err.Error())
}
func TestReadMap_with_arraysplat_map_array_and_bad_index(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
- names:
- fred
- smith
- names:
- sam
- bo
`)
_, err := readMap(data, "b", []string{"d", "*", "names", "x"})
if err == nil {
t.Fatal("Expected error due to invalid path")
}
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
test.AssertResult(t, expectedOutput, err.Error())
}
func TestReadMap_with_array_out_of_bounds(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
- 3
- 4
`)
got, _ := readMap(data, "b", []string{"d", "3"})
test.AssertResult(t, nil, got)
}
func TestReadMap_with_array_out_of_bounds_by_1(t *testing.T) {
var data = test.ParseData(`
---
b:
d:
- 3
- 4
`)
got, _ := readMap(data, "b", []string{"d", "2"})
test.AssertResult(t, nil, got)
}
func TestReadMap_with_array_splat(t *testing.T) {
var data = test.ParseData(`
e:
-
name: Fred
thing: cat
-
name: Sam
thing: dog
`)
got, _ := readMap(data, "e", []string{"*", "name"})
test.AssertResult(t, "[Fred Sam]", fmt.Sprintf("%v", got))
}
func TestWrite_really_simple(t *testing.T) {
var data = test.ParseData(`
b: 2
`)
updated := writeMap(data, []string{"b"}, "4")
test.AssertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
}
func TestWrite_simple(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
updated := writeMap(data, []string{"b", "c"}, "4")
test.AssertResult(t, "[{b [{c 4}]}]", fmt.Sprintf("%v", updated))
}
func TestWrite_new(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
updated := writeMap(data, []string{"b", "d"}, "4")
test.AssertResult(t, "[{b [{c 2} {d 4}]}]", fmt.Sprintf("%v", updated))
}
func TestWrite_new_deep(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
updated := writeMap(data, []string{"b", "d", "f"}, "4")
test.AssertResult(t, "[{b [{c 2} {d [{f 4}]}]}]", fmt.Sprintf("%v", updated))
}
func TestWrite_array(t *testing.T) {
var data = test.ParseData(`
b:
- aa
`)
updated := writeMap(data, []string{"b", "0"}, "bb")
test.AssertResult(t, "[{b [bb]}]", fmt.Sprintf("%v", updated))
}
func TestWrite_new_array(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
updated := writeMap(data, []string{"b", "0"}, "4")
test.AssertResult(t, "[{b [{c 2} {0 4}]}]", fmt.Sprintf("%v", updated))
}
func TestWrite_new_array_deep(t *testing.T) {
var data = test.ParseData(`
a: apple
`)
var expected = `a: apple
b:
- c: "4"`
updated := writeMap(data, []string{"b", "+", "c"}, "4")
got, _ := YamlToString(updated, true)
test.AssertResult(t, expected, got)
}
func TestWrite_new_map_array_deep(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
var expected = `b:
c: 2
d:
- "4"`
updated := writeMap(data, []string{"b", "d", "+"}, "4")
got, _ := YamlToString(updated, true)
test.AssertResult(t, expected, got)
}
func TestWrite_add_to_array(t *testing.T) {
var data = test.ParseData(`
b:
- aa
`)
var expected = `b:
- aa
- bb`
updated := writeMap(data, []string{"b", "1"}, "bb")
got, _ := YamlToString(updated, true)
test.AssertResult(t, expected, got)
}
func TestWrite_with_no_tail(t *testing.T) {
var data = test.ParseData(`
b:
c: 2
`)
updated := writeMap(data, []string{"b"}, "4")
test.AssertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
}
func TestWriteMap_no_paths(t *testing.T) {
var data = test.ParseData(`
b: 5
`)
result := writeMap(data, []string{}, 4)
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestWriteArray_no_paths(t *testing.T) {
var data = make([]interface{}, 1)
data[0] = "mike"
result := writeArray(data, []string{}, 4)
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestDelete_MapItem(t *testing.T) {
var data = test.ParseData(`
a: 123
b: 456
`)
var expected = test.ParseData(`
b: 456
`)
result, _ := deleteMap(data, []string{"a"})
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
}
// Ensure deleting an index into a string does nothing
func TestDelete_index_to_string(t *testing.T) {
var data = test.ParseData(`
a: mystring
`)
result, _ := deleteMap(data, []string{"a", "0"})
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestDelete_list_index(t *testing.T) {
var data = test.ParseData(`
a: [3, 4]
`)
var expected = test.ParseData(`
a: [3]
`)
result, _ := deleteMap(data, []string{"a", "1"})
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
}
func TestDelete_list_index_beyond_bounds(t *testing.T) {
var data = test.ParseData(`
a: [3, 4]
`)
result, _ := deleteMap(data, []string{"a", "5"})
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestDelete_list_index_out_of_bounds_by_1(t *testing.T) {
var data = test.ParseData(`
a: [3, 4]
`)
result, _ := deleteMap(data, []string{"a", "2"})
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestDelete_no_paths(t *testing.T) {
var data = test.ParseData(`
a: [3, 4]
b:
- name: test
`)
result, _ := deleteMap(data, []string{})
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
}
func TestDelete_array_map_item(t *testing.T) {
var data = test.ParseData(`
b:
- name: fred
value: blah
- name: john
value: test
`)
var expected = test.ParseData(`
b:
- value: blah
- name: john
value: test
`)
result, _ := deleteMap(data, []string{"b", "0", "name"})
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
}

View File

@@ -0,0 +1,44 @@
package yqlib
import (
"encoding/json"
"fmt"
"strconv"
yaml "github.com/mikefarah/yaml/v2"
)
func JsonToString(context interface{}) (string, error) {
out, err := json.Marshal(toJSON(context))
if err != nil {
return "", fmt.Errorf("error printing yaml as json: %v", err)
}
return string(out), nil
}
func toJSON(context interface{}) interface{} {
switch context := context.(type) {
case []interface{}:
oldArray := context
newArray := make([]interface{}, len(oldArray))
for index, value := range oldArray {
newArray[index] = toJSON(value)
}
return newArray
case yaml.MapSlice:
oldMap := context
newMap := make(map[string]interface{})
for _, entry := range oldMap {
if str, ok := entry.Key.(string); ok {
newMap[str] = toJSON(entry.Value)
} else if i, ok := entry.Key.(int); ok {
newMap[strconv.Itoa(i)] = toJSON(entry.Value)
} else if b, ok := entry.Key.(bool); ok {
newMap[strconv.FormatBool(b)] = toJSON(entry.Value)
}
}
return newMap
default:
return context
}
}

View File

@@ -0,0 +1,47 @@
package yqlib
import (
"testing"
"github.com/mikefarah/yq/test"
)
func TestJsonToString(t *testing.T) {
var data = test.ParseData(`
---
b:
c: 2
`)
got, _ := JsonToString(data)
test.AssertResult(t, "{\"b\":{\"c\":2}}", got)
}
func TestJsonToString_withIntKey(t *testing.T) {
var data = test.ParseData(`
---
b:
2: c
`)
got, _ := JsonToString(data)
test.AssertResult(t, `{"b":{"2":"c"}}`, got)
}
func TestJsonToString_withBoolKey(t *testing.T) {
var data = test.ParseData(`
---
b:
false: c
`)
got, _ := JsonToString(data)
test.AssertResult(t, `{"b":{"false":"c"}}`, got)
}
func TestJsonToString_withArray(t *testing.T) {
var data = test.ParseData(`
---
b:
- item: one
- item: two
`)
got, _ := JsonToString(data)
test.AssertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", got)
}

54
pkg/yqlib/lib.go Normal file
View File

@@ -0,0 +1,54 @@
package yqlib
import (
mergo "gopkg.in/imdario/mergo.v0"
logging "gopkg.in/op/go-logging.v1"
)
var log = logging.MustGetLogger("yq")
func SetLogger(l *logging.Logger) {
log = l
}
func ReadPath(dataBucket interface{}, path string) (interface{}, error) {
var paths = ParsePath(path)
return Recurse(dataBucket, paths[0], paths[1:])
}
func WritePath(dataBucket interface{}, path string, value interface{}) (interface{}) {
var paths = ParsePath(path)
return UpdatedChildValue(dataBucket, paths, value)
}
func PrefixPath(dataBucket interface{}, prefix string) (interface{}) {
var paths = ParsePath(prefix)
// Inverse order
for i := len(paths)/2 - 1; i >= 0; i-- {
opp := len(paths) - 1 - i
paths[i], paths[opp] = paths[opp], paths[i]
}
var mapDataBucket = dataBucket
for _, key := range paths {
singlePath := []string{key}
mapDataBucket = UpdatedChildValue(nil, singlePath, mapDataBucket)
}
return mapDataBucket
}
func DeletePath(dataBucket interface{}, path string) (interface{}, error) {
var paths = ParsePath(path)
return DeleteChildValue(dataBucket, paths)
}
func Merge(dst interface{}, src interface{}, overwrite bool, append bool) error {
if overwrite {
return mergo.Merge(dst, src, mergo.WithOverride)
} else if append {
return mergo.Merge(dst, src, mergo.WithAppendSlice)
}
return mergo.Merge(dst, src)
}

148
pkg/yqlib/lib_test.go Normal file
View File

@@ -0,0 +1,148 @@
package yqlib
import (
"fmt"
"testing"
"github.com/mikefarah/yq/test"
)
func TestReadPath(t *testing.T) {
var data = test.ParseData(`
---
b:
2: c
`)
got, _ := ReadPath(data, "b.2")
test.AssertResult(t, `c`, got)
}
func TestReadPath_WithError(t *testing.T) {
var data = test.ParseData(`
---
b:
- c
`)
_, err := ReadPath(data, "b.[a]")
if err == nil {
t.Fatal("Expected error due to invalid path")
}
}
func TestWritePath(t *testing.T) {
var data = test.ParseData(`
---
b:
2: c
`)
got := WritePath(data, "b.3", "a")
test.AssertResult(t, `[{b [{2 c} {3 a}]}]`, fmt.Sprintf("%v", got))
}
func TestPrefixPath(t *testing.T) {
var data = test.ParseData(`
---
b:
2: c
`)
got := PrefixPath(data, "d")
test.AssertResult(t, `[{d [{b [{2 c}]}]}]`, fmt.Sprintf("%v", got))
}
func TestDeletePath(t *testing.T) {
var data = test.ParseData(`
---
b:
2: c
3: a
`)
got, _ := DeletePath(data, "b.2")
test.AssertResult(t, `[{b [{3 a}]}]`, fmt.Sprintf("%v", got))
}
func TestDeletePath_WithError(t *testing.T) {
var data = test.ParseData(`
---
b:
- c
`)
_, err := DeletePath(data, "b.[a]")
if err == nil {
t.Fatal("Expected error due to invalid path")
}
}
func TestMerge(t *testing.T) {
var dst = test.ParseData(`
---
a: b
c: d
`)
var src = test.ParseData(`
---
a: 1
b: 2
`)
var mergedData = make(map[interface{}]interface{})
mergedData["root"] = dst
var mapDataBucket = make(map[interface{}]interface{})
mapDataBucket["root"] = src
Merge(&mergedData, mapDataBucket, false, false)
test.AssertResult(t, `[{a b} {c d}]`, fmt.Sprintf("%v", mergedData["root"]))
}
func TestMerge_WithOverwrite(t *testing.T) {
var dst = test.ParseData(`
---
a: b
c: d
`)
var src = test.ParseData(`
---
a: 1
b: 2
`)
var mergedData = make(map[interface{}]interface{})
mergedData["root"] = dst
var mapDataBucket = make(map[interface{}]interface{})
mapDataBucket["root"] = src
Merge(&mergedData, mapDataBucket, true, false)
test.AssertResult(t, `[{a 1} {b 2}]`, fmt.Sprintf("%v", mergedData["root"]))
}
func TestMerge_WithAppend(t *testing.T) {
var dst = test.ParseData(`
---
a: b
c: d
`)
var src = test.ParseData(`
---
a: 1
b: 2
`)
var mergedData = make(map[interface{}]interface{})
mergedData["root"] = dst
var mapDataBucket = make(map[interface{}]interface{})
mapDataBucket["root"] = src
Merge(&mergedData, mapDataBucket, false, true)
test.AssertResult(t, `[{a b} {c d} {a 1} {b 2}]`, fmt.Sprintf("%v", mergedData["root"]))
}
func TestMerge_WithError(t *testing.T) {
err := Merge(nil, nil, false, false)
if err == nil {
t.Fatal("Expected error due to nil")
}
}

55
pkg/yqlib/path_parser.go Normal file
View File

@@ -0,0 +1,55 @@
package yqlib
func ParsePath(path string) []string {
return parsePathAccum([]string{}, path)
}
func parsePathAccum(paths []string, remaining string) []string {
head, tail := nextYamlPath(remaining)
if tail == "" {
return append(paths, head)
}
return parsePathAccum(append(paths, head), tail)
}
func nextYamlPath(path string) (pathElement string, remaining string) {
switch path[0] {
case '[':
// e.g [0].blah.cat -> we need to return "0" and "blah.cat"
return search(path[1:], []uint8{']'}, true)
case '"':
// e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat"
return search(path[1:], []uint8{'"'}, true)
default:
// e.g "a.blah.cat" -> return "a" and "blah.cat"
return search(path[0:], []uint8{'.', '['}, false)
}
}
func search(path string, matchingChars []uint8, skipNext bool) (pathElement string, remaining string) {
for i := 0; i < len(path); i++ {
var char = path[i]
if contains(matchingChars, char) {
var remainingStart = i + 1
if skipNext {
remainingStart = remainingStart + 1
} else if !skipNext && char != '.' {
remainingStart = i
}
if remainingStart > len(path) {
remainingStart = len(path)
}
return path[0:i], path[remainingStart:]
}
}
return path, ""
}
func contains(matchingChars []uint8, candidate uint8) bool {
for _, a := range matchingChars {
if a == candidate {
return true
}
}
return false
}

View File

@@ -0,0 +1,44 @@
package yqlib
import (
"testing"
"github.com/mikefarah/yq/test"
)
var parsePathsTests = []struct {
path string
expectedPaths []string
}{
{"a.b", []string{"a", "b"}},
{"a.b[0]", []string{"a", "b", "0"}},
{"a.b.d[+]", []string{"a", "b", "d", "+"}},
}
func TestParsePath(t *testing.T) {
for _, tt := range parsePathsTests {
test.AssertResultComplex(t, tt.expectedPaths, ParsePath(tt.path))
}
}
var nextYamlPathTests = []struct {
path string
expectedElement string
expectedRemaining string
}{
{"a.b", "a", "b"},
{"a", "a", ""},
{"a.b.c", "a", "b.c"},
{"\"a.b\".c", "a.b", "c"},
{"a.\"b.c\".d", "a", "\"b.c\".d"},
{"[1].a.d", "1", "a.d"},
{"a[0].c", "a", "[0].c"},
{"[0]", "0", ""},
}
func TestNextYamlPath(t *testing.T) {
for _, tt := range nextYamlPathTests {
var element, remaining = nextYamlPath(tt.path)
test.AssertResultWithContext(t, tt.expectedElement, element, tt)
test.AssertResultWithContext(t, tt.expectedRemaining, remaining, tt)
}
}

View File

@@ -0,0 +1,32 @@
package yqlib
import (
yaml "github.com/mikefarah/yaml/v2"
errors "github.com/pkg/errors"
"strings"
)
func YamlToString(context interface{}, trimOutput bool) (string, error) {
switch context := context.(type) {
case string:
return context, nil
default:
return marshalContext(context, trimOutput)
}
}
func marshalContext(context interface{}, trimOutput bool) (string, error) {
out, err := yaml.Marshal(context)
if err != nil {
return "", errors.Wrap(err, "error printing yaml")
}
outStr := string(out)
// trim the trailing new line as it's easier for a script to add
// it in if required than to remove it
if trimOutput {
return strings.Trim(outStr, "\n "), nil
}
return outStr, nil
}