From 357a0e07a6ec94a9c7206d2efaabfdc4a84086df Mon Sep 17 00:00:00 2001 From: Matthias Diester Date: Sat, 28 Dec 2019 23:31:47 +0100 Subject: [PATCH] Migrate to Go YAML v3 Introduce consistent usage of Go YAML v3 type `Node` in all functions. Remove all old Go YAML v2 map item and slice references. Rewrite the restructure code based on the previous style and ideas. Rework test case setup to introduce YAML Node type. Drop version directory in main library package path. --- .gitignore | 1 + Makefile | 2 +- go.mod | 4 +- go.sum | 4 +- internal/cmd/compare.go | 2 +- internal/cmd/get.go | 2 +- internal/cmd/paths.go | 2 +- internal/cmd/restructure.go | 19 ++-- internal/cmd/root.go | 2 +- pkg/v1/ytbx/convert.go | 114 -------------------- pkg/v1/ytbx/getting.go | 93 ---------------- pkg/v1/ytbx/list_functions.go | 118 --------------------- pkg/{v1 => }/ytbx/common.go | 74 ++++--------- pkg/ytbx/convert.go | 39 +++++++ pkg/{v1 => }/ytbx/errors.go | 0 pkg/ytbx/getting.go | 123 +++++++++++++++++++++ pkg/{v1 => }/ytbx/getting_test.go | 34 +++--- pkg/{v1 => }/ytbx/input.go | 147 ++++++++------------------ pkg/{v1 => }/ytbx/input_test.go | 22 ++-- pkg/ytbx/list_functions.go | 99 +++++++++++++++++ pkg/{v1 => }/ytbx/map_functions.go | 45 ++++---- pkg/{v1 => }/ytbx/path.go | 102 ++++++++++++------ pkg/{v1 => }/ytbx/path_test.go | 12 ++- pkg/{v1 => }/ytbx/restructure.go | 127 ++++++++++++---------- pkg/{v1 => }/ytbx/restructure_test.go | 38 +++---- pkg/{v1 => }/ytbx/ytbx_suite_test.go | 135 ++++++++++++++++------- 26 files changed, 649 insertions(+), 711 deletions(-) delete mode 100644 pkg/v1/ytbx/convert.go delete mode 100644 pkg/v1/ytbx/getting.go delete mode 100644 pkg/v1/ytbx/list_functions.go rename pkg/{v1 => }/ytbx/common.go (55%) create mode 100644 pkg/ytbx/convert.go rename pkg/{v1 => }/ytbx/errors.go (100%) create mode 100644 pkg/ytbx/getting.go rename pkg/{v1 => }/ytbx/getting_test.go (76%) rename pkg/{v1 => }/ytbx/input.go (70%) rename pkg/{v1 => }/ytbx/input_test.go (84%) create mode 100644 pkg/ytbx/list_functions.go rename pkg/{v1 => }/ytbx/map_functions.go (55%) rename pkg/{v1 => }/ytbx/path.go (83%) rename pkg/{v1 => }/ytbx/path_test.go (97%) rename pkg/{v1 => }/ytbx/restructure.go (57%) rename pkg/{v1 => }/ytbx/restructure_test.go (67%) rename pkg/{v1 => }/ytbx/ytbx_suite_test.go (61%) diff --git a/.gitignore b/.gitignore index e935fd3..33bb546 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ binaries +**/*.coverprofile diff --git a/Makefile b/Makefile index 36e7ed1..6116534 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ .PHONY: all clean test verify build version := $(shell git describe --tags --abbrev=0 2>/dev/null || (git rev-parse HEAD | cut -c-8)) -sources := $(wildcard cmd/ytbx/*.go internal/cmd/*.go pkg/v1/ytbx/*.go) +sources := $(wildcard cmd/ytbx/*.go internal/cmd/*.go pkg/ytbx/*.go) all: clean verify test build diff --git a/go.mod b/go.mod index f95fa97..053f3e6 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.12 require ( github.com/BurntSushi/toml v0.3.1 github.com/gonvenience/bunt v1.1.1 - github.com/gonvenience/neat v1.1.0 + github.com/gonvenience/neat v1.1.1 github.com/gonvenience/wrap v1.1.0 github.com/gorilla/mux v1.7.3 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 github.com/spf13/cobra v0.0.5 github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 - gopkg.in/yaml.v2 v2.2.7 + gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 ) diff --git a/go.sum b/go.sum index 37c7221..f45d020 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gonvenience/bunt v1.1.1 h1:isYxOpDqbRMOSRhZtoux1tYvhhQ/AIbVDFrs24l6t0M= github.com/gonvenience/bunt v1.1.1/go.mod h1:lsyhkmNpSAzhVx059BD0fQy5F29rWcS6AHb7UWNlT/s= -github.com/gonvenience/neat v1.1.0 h1:xuEH2rYPedbIwuaYBJAtGwCJHwRlo8jF5PScLizuI5Y= -github.com/gonvenience/neat v1.1.0/go.mod h1:Yb+9Jlr04pbtcRU8EGosVheOEBs//Lw/OXvgDyQfLTQ= +github.com/gonvenience/neat v1.1.1 h1:uX5uxp3/KVMNgvfFceA5gkFeSofaqREIhOWepZafYlE= +github.com/gonvenience/neat v1.1.1/go.mod h1:Yb+9Jlr04pbtcRU8EGosVheOEBs//Lw/OXvgDyQfLTQ= github.com/gonvenience/term v1.0.0 h1:joCB/j0Ngmdakd3muuLgAGPMf7DNKdoe708c1I6RiBs= github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= github.com/gonvenience/wrap v1.1.0 h1:d8gEZrXS/zg4BC1q0U4nHpPIh5k6muKpQ1+rQFBwpYc= diff --git a/internal/cmd/compare.go b/internal/cmd/compare.go index a7d4451..c28962b 100644 --- a/internal/cmd/compare.go +++ b/internal/cmd/compare.go @@ -24,7 +24,7 @@ import ( "fmt" "github.com/gonvenience/wrap" - "github.com/homeport/ytbx/pkg/v1/ytbx" + "github.com/homeport/ytbx/pkg/ytbx" "github.com/spf13/cobra" ) diff --git a/internal/cmd/get.go b/internal/cmd/get.go index a217053..bbd8b0f 100644 --- a/internal/cmd/get.go +++ b/internal/cmd/get.go @@ -25,7 +25,7 @@ import ( "github.com/gonvenience/neat" "github.com/gonvenience/wrap" - "github.com/homeport/ytbx/pkg/v1/ytbx" + "github.com/homeport/ytbx/pkg/ytbx" "github.com/spf13/cobra" ) diff --git a/internal/cmd/paths.go b/internal/cmd/paths.go index 9a23f6c..af9df3f 100644 --- a/internal/cmd/paths.go +++ b/internal/cmd/paths.go @@ -24,7 +24,7 @@ import ( "fmt" "github.com/gonvenience/wrap" - "github.com/homeport/ytbx/pkg/v1/ytbx" + "github.com/homeport/ytbx/pkg/ytbx" "github.com/spf13/cobra" ) diff --git a/internal/cmd/restructure.go b/internal/cmd/restructure.go index e48eb67..72432f0 100644 --- a/internal/cmd/restructure.go +++ b/internal/cmd/restructure.go @@ -29,9 +29,9 @@ import ( "github.com/gonvenience/bunt" "github.com/gonvenience/neat" - "github.com/homeport/ytbx/pkg/v1/ytbx" + "github.com/homeport/ytbx/pkg/ytbx" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) var inplace bool @@ -53,7 +53,7 @@ var restructureCmd = &cobra.Command{ } for i := range input.Documents { - input.Documents[i] = ytbx.RestructureObject(input.Documents[i]) + ytbx.RestructureObject(input.Documents[i]) } if inplace { @@ -65,12 +65,12 @@ var restructureCmd = &cobra.Command{ var buf bytes.Buffer writer := bufio.NewWriter(&buf) for _, document := range input.Documents { - out, err := yaml.Marshal(document) + out, err := yamlv3.Marshal(document) if err != nil { return err } - fmt.Fprint(writer, "---\n", string(out)) + fmt.Fprint(writer, string(out)) } writer.Flush() @@ -83,7 +83,6 @@ var restructureCmd = &cobra.Command{ return err } - bunt.Println("DimGray{*---*}") fmt.Print(out) fmt.Println() } @@ -104,8 +103,8 @@ func init() { } func renderLongDescription() string { - var data yaml.MapSlice - yaml.Unmarshal([]byte(`--- + var data yamlv3.Node + yamlv3.Unmarshal([]byte(`--- releases: - sha1: 5ab3b7e685ca18a47d0b4a16d0e3b60832b0a393 name: binary-buildpack @@ -114,7 +113,9 @@ releases: `), &data) before, _ := neat.ToYAMLString(data) - after, _ := neat.ToYAMLString(ytbx.RestructureObject(data)) + + ytbx.RestructureObject(&data) + after, _ := neat.ToYAMLString(data) return bunt.Sprintf(`Restructure the order of keys in YAML maps diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 19887a8..7afcb08 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -27,7 +27,7 @@ import ( "github.com/gonvenience/bunt" "github.com/gonvenience/neat" "github.com/gonvenience/wrap" - "github.com/homeport/ytbx/pkg/v1/ytbx" + "github.com/homeport/ytbx/pkg/ytbx" "github.com/spf13/cobra" ) diff --git a/pkg/v1/ytbx/convert.go b/pkg/v1/ytbx/convert.go deleted file mode 100644 index 77432c5..0000000 --- a/pkg/v1/ytbx/convert.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright © 2018 The Homeport Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package ytbx - -import ( - "sort" - - ordered "github.com/virtuald/go-ordered-json" - yaml "gopkg.in/yaml.v2" -) - -// mapSlicify makes sure that each occurrence of a map in the provided structure -// is changed to a YAML MapSlice. -// -// Please note: In case the input data were decoded by the default standard JSON -// parser, there will be no preservation of the order of keys, because JSON does -// not support such thing as an order of keys. Therfore, the keys are sorted to -// have a consistent and testable output structure. -// -// This function supports `OrderedObjects` from the JSON library fork -// `github.com/virtuald/go-ordered-json` and will translate this structure into -// the compatible YAML structure. -func mapSlicify(obj interface{}) interface{} { - switch tobj := obj.(type) { - case ordered.OrderedObject: - result := make(yaml.MapSlice, 0, len(tobj)) - for _, member := range tobj { - result = append(result, yaml.MapItem{Key: member.Key, Value: mapSlicify(member.Value)}) - } - - return result - - case map[string]interface{}: - return mapToYamlSlice(tobj) - - case []interface{}: - result := make([]interface{}, len(tobj)) - for idx, entry := range tobj { - result[idx] = mapSlicify(entry) - } - - return result - - case []map[string]interface{}: - result := make([]yaml.MapSlice, len(tobj)) - for idx, entry := range tobj { - result[idx] = mapToYamlSlice(entry) - } - - return result - - default: - return obj - } -} - -func mapToYamlSlice(input map[string]interface{}) yaml.MapSlice { - keys := make([]string, 0, len(input)) - for key := range input { - keys = append(keys, key) - } - - sort.Strings(keys) - - result := make(yaml.MapSlice, 0, len(input)) - for _, key := range keys { - result = append(result, yaml.MapItem{Key: key, Value: mapSlicify(input[key])}) - } - - return result -} - -func castAsComplexList(obj interface{}) ([]yaml.MapSlice, bool) { - switch tobj := obj.(type) { - case []yaml.MapSlice: - return tobj, true - - case []interface{}: - if IsComplexSlice(tobj) { - result := make([]yaml.MapSlice, len(tobj)) - for idx, entry := range tobj { - switch x := entry.(type) { - case yaml.MapSlice: - result[idx] = x - - case map[string]interface{}: - result[idx] = mapToYamlSlice(x) - } - } - - return result, true - } - } - - return nil, false -} diff --git a/pkg/v1/ytbx/getting.go b/pkg/v1/ytbx/getting.go deleted file mode 100644 index 1d98631..0000000 --- a/pkg/v1/ytbx/getting.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright © 2018 The Homeport Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package ytbx - -import ( - "fmt" - - yaml "gopkg.in/yaml.v2" -) - -// Grab get the value from the provided YAML tree using a path to traverse through the tree structure -func Grab(obj interface{}, pathString string) (interface{}, error) { - path, err := ParsePathString(pathString, obj) - if err != nil { - return nil, err - } - - return grabByPath(obj, path) -} - -func grabByPath(obj interface{}, path Path) (interface{}, error) { - pointer, pointerPath := obj, Path{DocumentIdx: path.DocumentIdx} - - for _, element := range path.PathElements { - switch { - // Key/Value Map, where the element name is the key for the map - case element.isMapElement(): - if !isMapSlice(pointer) { - return nil, fmt.Errorf("failed to traverse tree, expected a %s but found type %s at %s", typeMap, GetType(pointer), pointerPath.ToGoPatchStyle()) - } - - entry, err := getValueByKey(pointer.(yaml.MapSlice), element.Name) - if err != nil { - return nil, err - } - - pointer = entry - - // Complex List, where each list entry is a Key/Value map and the entry is identified by name using an indentifier (e.g. name, key, or id) - case element.isComplexListElement(): - complexList, ok := castAsComplexList(pointer) - if !ok { - return nil, fmt.Errorf("failed to traverse tree, expected a %s but found type %s at %s", typeComplexList, GetType(pointer), pointerPath.ToGoPatchStyle()) - } - - entry, err := getEntryByIdentifierAndName(complexList, element.Key, element.Name) - if err != nil { - return nil, err - } - - pointer = entry - - // Simple List (identified by index) - case element.isSimpleListElement(): - if !isList(pointer) { - return nil, fmt.Errorf("failed to traverse tree, expected a %s but found type %s at %s", typeSimpleList, GetType(pointer), pointerPath.ToGoPatchStyle()) - } - - list := pointer.([]interface{}) - if element.Idx < 0 || element.Idx >= len(list) { - return nil, fmt.Errorf("failed to traverse tree, provided %s index %d is not in range: 0..%d", typeSimpleList, element.Idx, len(list)-1) - } - - pointer = list[element.Idx] - - default: - return nil, fmt.Errorf("failed to traverse tree, the provided path %s seems to be invalid", path) - } - - // Update the path that the current pointer has (only used in error case to point to the right position) - pointerPath.PathElements = append(pointerPath.PathElements, element) - } - - return pointer, nil -} diff --git a/pkg/v1/ytbx/list_functions.go b/pkg/v1/ytbx/list_functions.go deleted file mode 100644 index 3782197..0000000 --- a/pkg/v1/ytbx/list_functions.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright © 2018 The Homeport Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package ytbx - -import ( - "fmt" - - yaml "gopkg.in/yaml.v2" -) - -// GetIdentifierFromNamedList returns the identifier key used in the provided list, or an empty string if there is none. The identifier key is either 'name', 'key', or 'id'. -func GetIdentifierFromNamedList(list []interface{}) string { - counters := map[interface{}]int{} - - for _, sliceEntry := range list { - switch mapslice := sliceEntry.(type) { - case yaml.MapSlice: - for _, mapSliceEntry := range mapslice { - if _, ok := counters[mapSliceEntry.Key]; !ok { - counters[mapSliceEntry.Key] = 0 - } - - counters[mapSliceEntry.Key]++ - } - } - } - - sliceLength := len(list) - for _, identifier := range []string{"name", "key", "id"} { - if count, ok := counters[identifier]; ok && count == sliceLength { - return identifier - } - } - - return "" -} - -// ListStringKeys returns a list of the keys of the YAML MapSlice (map). Only string keys are supported. Other types will result in an error. -func ListStringKeys(mapslice yaml.MapSlice) ([]string, error) { - keys := make([]string, len(mapslice)) - for i, mapitem := range mapslice { - switch mapitem.Key.(type) { - case string: - keys[i] = mapitem.Key.(string) - - default: - return nil, fmt.Errorf("provided mapslice mapitem contains non-string key: %#v", mapitem.Key) - } - } - - return keys, nil -} - -// getEntryFromNamedList returns the entry that is identified by the identifier key and a name, for example: `name: one` where name is the identifier key and one the name. Function will return nil with bool false if there is no such entry. -func getEntryFromNamedList(list []interface{}, identifier string, name interface{}) (interface{}, bool) { - for _, listEntry := range list { - mapslice := listEntry.(yaml.MapSlice) - - for _, element := range mapslice { - if element.Key == identifier && element.Value == name { - return mapslice, true - } - } - } - - return nil, false -} - -func listNamesOfNamedList(list []interface{}, identifier string) ([]string, error) { - result := make([]string, len(list)) - for i, entry := range list { - switch entry.(type) { - case yaml.MapSlice: - value, err := getValueByKey(entry.(yaml.MapSlice), identifier) - if err != nil { - return nil, err - } - - result[i] = value.(string) - - default: - return nil, &NoNamedEntryListError{} - } - } - - return result, nil -} - -func splitEntryIntoNameAndData(mapslice yaml.MapSlice, identifier string) (name interface{}, result yaml.MapSlice) { - for _, mapitem := range mapslice { - if key, ok := mapitem.Key.(string); ok && key == identifier { - name = mapitem.Value - - } else { - result = append(result, mapitem) - } - } - - return name, result -} diff --git a/pkg/v1/ytbx/common.go b/pkg/ytbx/common.go similarity index 55% rename from pkg/v1/ytbx/common.go rename to pkg/ytbx/common.go index c1b6bc3..7a92cb3 100644 --- a/pkg/v1/ytbx/common.go +++ b/pkg/ytbx/common.go @@ -23,7 +23,7 @@ package ytbx import ( "reflect" - yaml "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) // Internal string constants for type names and type decisions @@ -37,73 +37,35 @@ const ( // GetType returns the type of the input value with a YAML specific view func GetType(value interface{}) string { switch tobj := value.(type) { + case *yamlv3.Node: + switch tobj.Kind { + case yamlv3.MappingNode: + return typeMap - case yaml.MapSlice, nil: - return typeMap + case yamlv3.SequenceNode: + if hasMappingNodes(tobj) { + return typeComplexList + } - case []interface{}: - if IsComplexSlice(tobj) { - return typeComplexList - } - - return typeSimpleList - - case []yaml.MapSlice: - return typeComplexList + return typeSimpleList - case string: - return typeString + default: + return reflect.TypeOf(tobj.Value).Kind().String() + } default: return reflect.TypeOf(value).Kind().String() } } -// IsComplexSlice returns whether the slice contains (hash)map entries, otherwise the slice is called a simple list. -func IsComplexSlice(slice []interface{}) bool { - // This is kind of a weird case, but by definition an empty list is a simple slice - if len(slice) == 0 { - return false - } - - // Count the number of entries which are maps or YAML MapSlices +func hasMappingNodes(sequenceNode *yamlv3.Node) bool { counter := 0 - for _, entry := range slice { - switch entry.(type) { - case map[string]interface{}, map[interface{}]interface{}, yaml.MapSlice: + + for _, entry := range sequenceNode.Content { + if entry.Kind == yamlv3.MappingNode { counter++ } } - return counter == len(slice) -} - -// SimplifyList will cast a slice of YAML MapSlices into a slice of interfaces. -func SimplifyList(input []yaml.MapSlice) []interface{} { - result := make([]interface{}, len(input)) - for i := range input { - result[i] = input[i] - } - - return result -} - -func isList(obj interface{}) bool { - switch obj.(type) { - case []interface{}: - return true - - default: - return false - } -} - -func isMapSlice(obj interface{}) bool { - switch obj.(type) { - case yaml.MapSlice: - return true - - default: - return false - } + return counter == len(sequenceNode.Content) } diff --git a/pkg/ytbx/convert.go b/pkg/ytbx/convert.go new file mode 100644 index 0000000..dd2ce3b --- /dev/null +++ b/pkg/ytbx/convert.go @@ -0,0 +1,39 @@ +// Copyright © 2018 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ytbx + +import ( + yamlv3 "gopkg.in/yaml.v3" +) + +func asYAMLNode(obj interface{}) (*yamlv3.Node, error) { + data, err := yamlv3.Marshal(obj) + if err != nil { + return nil, err + } + + var node yamlv3.Node + if err := yamlv3.Unmarshal(data, &node); err != nil { + return nil, err + } + + return &node, nil +} diff --git a/pkg/v1/ytbx/errors.go b/pkg/ytbx/errors.go similarity index 100% rename from pkg/v1/ytbx/errors.go rename to pkg/ytbx/errors.go diff --git a/pkg/ytbx/getting.go b/pkg/ytbx/getting.go new file mode 100644 index 0000000..6fac453 --- /dev/null +++ b/pkg/ytbx/getting.go @@ -0,0 +1,123 @@ +// Copyright © 2018 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ytbx + +import ( + "fmt" + + yamlv3 "gopkg.in/yaml.v3" +) + +// Grab gets the value from the provided YAML tree using a path to traverse +// through the tree structure +func Grab(node *yamlv3.Node, pathString string) (*yamlv3.Node, error) { + path, err := ParsePathString(pathString, node) + if err != nil { + return nil, err + } + + if node.Kind == yamlv3.DocumentNode { + if len(node.Content) != 1 { + panic("unsure of implementation detail document node with multiple nodes") + } + + entry := node.Content[0] + return grabByPath(entry, path) + } + + return grabByPath(node, path) +} + +func grabByPath(node *yamlv3.Node, path Path) (*yamlv3.Node, error) { + pointer := node + pointerPath := Path{DocumentIdx: path.DocumentIdx} + + for _, element := range path.PathElements { + switch { + // Key/Value Map, where the element name is the key for the map + case element.isMapElement(): + if pointer.Kind != yamlv3.MappingNode { + return nil, + fmt.Errorf("failed to traverse tree, expected %s but found type %s at %s", + typeMap, + GetType(pointer), + pointerPath.ToGoPatchStyle(), + ) + } + + entry, err := getValueByKey(pointer, element.Name) + if err != nil { + return nil, err + } + + pointer = entry + + // Complex List, where each list entry is a Key/Value map and the entry is + // identified by name using an indentifier (e.g. name, key, or id) + case element.isComplexListElement(): + if pointer.Kind != yamlv3.SequenceNode { + return nil, + fmt.Errorf("failed to traverse tree, expected %s but found type %s at %s", + typeComplexList, + GetType(pointer), + pointerPath.ToGoPatchStyle(), + ) + } + + entry, err := getEntryByIdentifierAndName(pointer, element.Key, element.Name) + if err != nil { + return nil, err + } + + pointer = entry + + // Simple List (identified by index) + case element.isSimpleListElement(): + if pointer.Kind != yamlv3.SequenceNode { + return nil, + fmt.Errorf("failed to traverse tree, expected %s but found type %s at %s", + typeSimpleList, + GetType(pointer), + pointerPath.ToGoPatchStyle(), + ) + } + + if element.Idx < 0 || element.Idx >= len(pointer.Content) { + return nil, + fmt.Errorf("failed to traverse tree, provided %s index %d is not in range: 0..%d", + typeSimpleList, + element.Idx, + len(pointer.Content)-1, + ) + } + + pointer = pointer.Content[element.Idx] + + default: + return nil, fmt.Errorf("failed to traverse tree, the provided path %s seems to be invalid", path) + } + + // Update the path that the current pointer to keep track of the traversing + pointerPath.PathElements = append(pointerPath.PathElements, element) + } + + return pointer, nil +} diff --git a/pkg/v1/ytbx/getting_test.go b/pkg/ytbx/getting_test.go similarity index 76% rename from pkg/v1/ytbx/getting_test.go rename to pkg/ytbx/getting_test.go index c26c5fd..6f3b2b8 100644 --- a/pkg/v1/ytbx/getting_test.go +++ b/pkg/ytbx/getting_test.go @@ -23,6 +23,8 @@ package ytbx_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/homeport/ytbx/pkg/ytbx" ) var _ = Describe("getting stuff test cases", func() { @@ -31,38 +33,44 @@ var _ = Describe("getting stuff test cases", func() { example := yml(assets("examples", "types.yml")) Expect(grab(example, "/yaml/map/before")).To(BeEquivalentTo("after")) Expect(grab(example, "/yaml/map/intA")).To(BeEquivalentTo(42)) - Expect(grab(example, "/yaml/map/mapA")).To(BeEquivalentTo(yml(`{ key0: A, key1: A }`))) - Expect(grab(example, "/yaml/map/listA")).To(BeEquivalentTo(list(`[ A, A, A ]`))) - Expect(grab(example, "/yaml/named-entry-list-using-name/name=B")).To(BeEquivalentTo(yml(`{ name: B }`))) - Expect(grab(example, "/yaml/named-entry-list-using-key/key=B")).To(BeEquivalentTo(yml(`{ key: B }`))) - Expect(grab(example, "/yaml/named-entry-list-using-id/id=B")).To(BeEquivalentTo(yml(`{ id: B }`))) + Expect(grab(example, "/yaml/map/mapA")).To(BeAsNode(yml(`{ key0: A, key1: A }`))) + Expect(grab(example, "/yaml/map/listA")).To(BeAsNode(list(`[ A, A, A ]`))) + Expect(grab(example, "/yaml/named-entry-list-using-name/name=B")).To(BeAsNode(yml(`{ name: B }`))) + Expect(grab(example, "/yaml/named-entry-list-using-key/key=B")).To(BeAsNode(yml(`{ key: B }`))) + Expect(grab(example, "/yaml/named-entry-list-using-id/id=B")).To(BeAsNode(yml(`{ id: B }`))) Expect(grab(example, "/yaml/simple-list/1")).To(BeEquivalentTo("B")) - Expect(grab(example, "/yaml/named-entry-list-using-key/3")).To(BeEquivalentTo(yml(`{ key: X }`))) + Expect(grab(example, "/yaml/named-entry-list-using-key/3")).To(BeAsNode(yml(`{ key: X }`))) example = yml(assets("bosh-yaml", "manifest.yml")) Expect(grab(example, "/instance_groups/name=web/networks/name=concourse/static_ips/0")).To(BeEquivalentTo("XX.XX.XX.XX")) - Expect(grab(example, "/instance_groups/name=worker/jobs/name=baggageclaim/properties")).To(BeEquivalentTo(yml(`{}`))) + Expect(grab(example, "/instance_groups/name=worker/jobs/name=baggageclaim/properties")).To(BeAsNode(yml(`{}`))) }) It("should return the whole tree if root is referenced", func() { - example := yml(assets("examples", "types.yml")) - Expect(grab(example, "/")).To(BeEquivalentTo(example)) + file, err := ytbx.LoadFile(assets("examples", "types.yml")) + Expect(err).ToNot(HaveOccurred()) + + document := file.Documents[0] + Expect(grab(document, "/")).To(BeAsNode(document.Content[0])) }) It("should return useful error messages", func() { example := yml(assets("examples", "types.yml")) Expect(grabError(example, "/yaml/simple-list/-1")).To(BeEquivalentTo("failed to traverse tree, provided list index -1 is not in range: 0..4")) Expect(grabError(example, "/yaml/does-not-exist")).To(BeEquivalentTo("no key 'does-not-exist' found in map, available keys: map, simple-list, named-entry-list-using-name, named-entry-list-using-key, named-entry-list-using-id")) - Expect(grabError(example, "/yaml/0")).To(BeEquivalentTo("failed to traverse tree, expected a list but found type map at /yaml")) - Expect(grabError(example, "/yaml/simple-list/foobar")).To(BeEquivalentTo("failed to traverse tree, expected a map but found type list at /yaml/simple-list")) - Expect(grabError(example, "/yaml/map/foobar=0")).To(BeEquivalentTo("failed to traverse tree, expected a complex-list but found type map at /yaml/map")) + Expect(grabError(example, "/yaml/0")).To(BeEquivalentTo("failed to traverse tree, expected list but found type map at /yaml")) + Expect(grabError(example, "/yaml/simple-list/foobar")).To(BeEquivalentTo("failed to traverse tree, expected map but found type list at /yaml/simple-list")) + Expect(grabError(example, "/yaml/map/foobar=0")).To(BeEquivalentTo("failed to traverse tree, expected complex-list but found type map at /yaml/map")) Expect(grabError(example, "/yaml/named-entry-list-using-id/id=0")).To(BeEquivalentTo("there is no entry id=0 in the list")) }) }) + Context("Trying to get values by path in an empty file", func() { It("should return a not found key error", func() { emptyFile := yml(assets("examples", "empty.yml")) - Expect(grabError(emptyFile, "does-not-exist")).To(BeEquivalentTo("no key 'does-not-exist' found in map, available keys: ")) + Expect(grabError(emptyFile, "/does-not-exist")).To( + BeEquivalentTo("failed to traverse tree, expected map but found type string at /"), + ) }) }) }) diff --git a/pkg/v1/ytbx/input.go b/pkg/ytbx/input.go similarity index 70% rename from pkg/v1/ytbx/input.go rename to pkg/ytbx/input.go index 34cd42d..a32a633 100644 --- a/pkg/v1/ytbx/input.go +++ b/pkg/ytbx/input.go @@ -36,7 +36,7 @@ import ( "github.com/gonvenience/bunt" "github.com/gonvenience/wrap" ordered "github.com/virtuald/go-ordered-json" - yaml "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) // PreserveKeyOrderInJSON specifies whether a special library is used to decode @@ -55,7 +55,7 @@ type DecoderProxy struct { type InputFile struct { Location string Note string - Documents []interface{} + Documents []*yamlv3.Node } // NewDecoderProxy creates a new decoder proxy which either works in ordered @@ -102,7 +102,7 @@ func HumanReadableLocationInformation(inputFile InputFile) string { } buf.WriteString(", ") - buf.WriteString(bunt.Sprintf("Aquamarine{*%s*}", str) ) + buf.WriteString(bunt.Sprintf("Aquamarine{*%s*}", str)) } return buf.String() @@ -166,29 +166,30 @@ func LoadFiles(locationA string, locationB string) (InputFile, InputFile, error) // supported document formats, or plain text if nothing else works. func LoadFile(location string) (InputFile, error) { var ( - documents []interface{} + documents []*yamlv3.Node data []byte err error ) if data, err = getBytesFromLocation(location); err != nil { - return InputFile{}, - wrap.Error(err, fmt.Sprintf("unable to load data from %s", location)) + return InputFile{}, wrap.Errorf(err, "unable to load data from %s", location) } if documents, err = LoadDocuments(data); err != nil { - return InputFile{}, - wrap.Error(err, fmt.Sprintf("unable to parse data from %s", location)) + return InputFile{}, wrap.Errorf(err, "unable to parse data from %s", location) } - return InputFile{Location: location, Documents: documents}, nil + return InputFile{ + Location: location, + Documents: documents, + }, nil } // LoadDocuments reads the provided input data slice as a YAML, JSON, or TOML // file with potential multiple documents. It only acts as a dispatcher and // depending on the input will either use `LoadTOMLDocuments`, // `LoadJSONDocuments`, or `LoadYAMLDocuments`. -func LoadDocuments(input []byte) ([]interface{}, error) { +func LoadDocuments(input []byte) ([]*yamlv3.Node, error) { // There is no easy check whether the input data is TOML format, this is // why there is currently no other option than simply trying to parse it. if toml, err := LoadTOMLDocuments(input); err == nil { @@ -207,58 +208,30 @@ func LoadDocuments(input []byte) ([]interface{}, error) { } } -// LoadJSONDocuments reads the provided input data slice as a YAML file with +// LoadJSONDocuments reads the provided input data slice as a JSON file with // potential multiple documents. Each document in the JSON stream results in an -// entry of the result slice. This function performs two decoding passes over -// the input data slice, the first one to detect the respective types in use. -// And a second one to properly unmarshal the data in the most suitable Go types -// available. JSON does not support key orders in maps. -func LoadJSONDocuments(input []byte) ([]interface{}, error) { - var ( - types []string - values []interface{} - decoder *DecoderProxy - ) +// entry of the result slice. +func LoadJSONDocuments(input []byte) ([]*yamlv3.Node, error) { + values := []*yamlv3.Node{} - // First pass: decode all documents and save the actual types - types = make([]string, 0) - decoder = NewDecoderProxy(false, bytes.NewReader(input)) + decoder := NewDecoderProxy(PreserveKeyOrderInJSON, bytes.NewReader(input)) for { var value interface{} - - if err := decoder.Decode(&value); err == io.EOF { + err := decoder.Decode(&value) + if err == io.EOF { break + } - } else if err != nil { + if err != nil { return nil, err } - types = append(types, GetType(value)) - } - - // Second pass: Based on the types, initialise a proper variable to unmarshal data into - values = make([]interface{}, len(types)) - decoder = NewDecoderProxy(PreserveKeyOrderInJSON, bytes.NewReader(input)) - for i := 0; i < len(types); i++ { - switch types[i] { - case typeMap: - var value interface{} - decoder.Decode(&value) - values[i] = mapSlicify(value) - - case typeSimpleList, typeComplexList: - var value []interface{} - decoder.Decode(&value) - values[i] = mapSlicify(value) - - case typeString: - var value string - decoder.Decode(&value) - values[i] = value - - default: - return nil, fmt.Errorf("unsupported type %s in load document function", types[i]) + node, err := asYAMLNode(value) + if err != nil { + return nil, err } + + values = append(values, node) } return values, nil @@ -266,79 +239,47 @@ func LoadJSONDocuments(input []byte) ([]interface{}, error) { // LoadYAMLDocuments reads the provided input data slice as a YAML file with // potential multiple documents. Each document in the YAML stream results in an -// entry of the result slice. This function performs two decoding passes over -// the input data slice, the first one to detect the respective types in use. -// And a second one to properly unmarshal the data in the most suitable Go types -// available so that key orders in hashes are preserved. -func LoadYAMLDocuments(input []byte) ([]interface{}, error) { - var ( - types []string - values []interface{} - decoder *yaml.Decoder - ) +// entry of the result slice. +func LoadYAMLDocuments(input []byte) ([]*yamlv3.Node, error) { + documents := []*yamlv3.Node{} - // First pass: decode all documents and save the actual types - types = make([]string, 0) - decoder = yaml.NewDecoder(bytes.NewReader(input)) + decoder := yamlv3.NewDecoder(bytes.NewReader(input)) for { - var value interface{} + var node yamlv3.Node - if err := decoder.Decode(&value); err == io.EOF { + err := decoder.Decode(&node) + if err == io.EOF { break + } - } else if err != nil { + if err != nil { return nil, err } - types = append(types, GetType(value)) + documents = append(documents, &node) } - // Second pass: Based on the types, initialise a proper variable to unmarshal data into - values = make([]interface{}, len(types)) - decoder = yaml.NewDecoder(bytes.NewReader(input)) - for i := 0; i < len(types); i++ { - switch types[i] { - case typeMap: - var value yaml.MapSlice - decoder.Decode(&value) - values[i] = value - - case typeSimpleList: - var value []interface{} - decoder.Decode(&value) - values[i] = value - - case typeComplexList: - var value []yaml.MapSlice - decoder.Decode(&value) - values[i] = value - - case typeString: - var value string - decoder.Decode(&value) - values[i] = value - - default: - return nil, fmt.Errorf("unsupported type %s in load document function", types[i]) - } - } - - return values, nil + return documents, nil } // LoadTOMLDocuments reads the provided input data slice as a TOML file, which // can only have one document. For the sake of having similar sounding // functions and the same signatures, the function uses the plural in its name // and returns a list of results even though it will only contain one entry. -// All map entries inside the result document are converted into YAML MapSlice +// All map entries inside the result document are converted into Go-YAMLv3 Node // types to make it compatible with the rest of the package. -func LoadTOMLDocuments(input []byte) ([]interface{}, error) { +func LoadTOMLDocuments(input []byte) ([]*yamlv3.Node, error) { var data interface{} if err := toml.Unmarshal(input, &data); err != nil { return nil, err } - return []interface{}{mapSlicify(data)}, nil + node, err := asYAMLNode(data) + if err != nil { + return nil, err + } + + return []*yamlv3.Node{node}, nil } func getBytesFromLocation(location string) ([]byte, error) { diff --git a/pkg/v1/ytbx/input_test.go b/pkg/ytbx/input_test.go similarity index 84% rename from pkg/v1/ytbx/input_test.go rename to pkg/ytbx/input_test.go index 16f1e8b..026c992 100644 --- a/pkg/v1/ytbx/input_test.go +++ b/pkg/ytbx/input_test.go @@ -31,8 +31,7 @@ import ( . "github.com/onsi/gomega" . "github.com/gorilla/mux" - . "github.com/homeport/ytbx/pkg/v1/ytbx" - yaml "gopkg.in/yaml.v2" + . "github.com/homeport/ytbx/pkg/ytbx" ) var _ = Describe("Input test cases", func() { @@ -41,10 +40,10 @@ var _ = Describe("Input test cases", func() { doc0, doc1 := `{ "key": "value" }`, `[ { "foo": "bar" } ]` documents, err := LoadDocuments([]byte(doc0 + "\n" + doc1)) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(len(documents)).To(BeEquivalentTo(2)) - Expect(documents[0]).To(BeEquivalentTo(yml(doc0))) - Expect(documents[1]).To(BeEquivalentTo(list(doc1))) + Expect(documents[0].Content[0]).To(BeAsNode(yml(doc0))) + Expect(documents[1].Content[0]).To(BeAsNode(list(doc1))) }) }) @@ -102,16 +101,9 @@ var _ = Describe("Input test cases", func() { Expect(err).ToNot(HaveOccurred()) Expect(len(documents)).To(BeEquivalentTo(1)) - document := documents[0] - - Expect(document).To(BeAssignableToTypeOf(yaml.MapSlice{})) - root := documents[0].(yaml.MapSlice) - - Expect(root[0].Key).To(BeEquivalentTo("constraint")) - Expect(root[0].Value).To(BeAssignableToTypeOf([]yaml.MapSlice{})) - - Expect(root[1].Key).To(BeEquivalentTo("override")) - Expect(root[1].Value).To(BeAssignableToTypeOf([]yaml.MapSlice{})) + rootMap := documents[0].Content[0] + Expect(rootMap.Content[0].Value).To(BeEquivalentTo("constraint")) + Expect(rootMap.Content[2].Value).To(BeEquivalentTo("override")) }) }) }) diff --git a/pkg/ytbx/list_functions.go b/pkg/ytbx/list_functions.go new file mode 100644 index 0000000..01ec02e --- /dev/null +++ b/pkg/ytbx/list_functions.go @@ -0,0 +1,99 @@ +// Copyright © 2018 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ytbx + +import ( + "fmt" + + yamlv3 "gopkg.in/yaml.v3" +) + +// GetIdentifierFromNamedList returns the identifier key used in the provided +// list, or an empty string if there is none. +// The identifier key is either 'name', 'key', or 'id'. +func GetIdentifierFromNamedList(sequenceNode *yamlv3.Node) string { + counters := map[string]int{} + + for _, mappingNode := range sequenceNode.Content { + for i := 0; i < len(mappingNode.Content); i += 2 { + k := mappingNode.Content[i] + + if _, ok := counters[k.Value]; !ok { + counters[k.Value] = 0 + } + + counters[k.Value]++ + } + } + + listLength := len(sequenceNode.Content) + for _, identifier := range []string{"name", "key", "id"} { + if count, ok := counters[identifier]; ok && count == listLength { + return identifier + } + } + + return "" +} + +// getEntryFromNamedList returns the entry that is identified by the identifier +// key and a name, for example: `name: one` where name is the identifier key and +// one the name. Function will return nil with bool false if there is no entry. +func getEntryFromNamedList(sequenceNode *yamlv3.Node, identifier string, name string) (*yamlv3.Node, bool) { + node, err := getEntryByIdentifierAndName(sequenceNode, identifier, name) + return node, err == nil +} + +func getEntryByIdentifierAndName(sequenceNode *yamlv3.Node, identifier string, name string) (*yamlv3.Node, error) { + for _, mappingNode := range sequenceNode.Content { + for i := 0; i < len(mappingNode.Content); i += 2 { + k, v := mappingNode.Content[i], mappingNode.Content[i+1] + if k.Value == identifier && v.Value == name { + return mappingNode, nil + } + } + } + + return nil, + fmt.Errorf("there is no entry %s=%v in the list", + identifier, + name, + ) +} + +func listNamesOfNamedList(sequenceNode *yamlv3.Node, identifier string) ([]string, error) { + result := make([]string, len(sequenceNode.Content)) + + for i, mappingNode := range sequenceNode.Content { + if mappingNode.Kind != yamlv3.MappingNode { + return nil, &NoNamedEntryListError{} + } + + v, err := getValueByKey(mappingNode, identifier) + if err != nil { + return nil, err + } + + result[i] = v.Value + } + + return result, nil +} diff --git a/pkg/v1/ytbx/map_functions.go b/pkg/ytbx/map_functions.go similarity index 55% rename from pkg/v1/ytbx/map_functions.go rename to pkg/ytbx/map_functions.go index 7de37e3..5b50c15 100644 --- a/pkg/v1/ytbx/map_functions.go +++ b/pkg/ytbx/map_functions.go @@ -21,40 +21,37 @@ package ytbx import ( - "fmt" - - yaml "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) -// listKeys returns a list of the keys of the YAML MapSlice (map). -func listKeys(mapslice yaml.MapSlice) []string { - keys := make([]string, len(mapslice)) - for i, mapitem := range mapslice { - keys[i] = fmt.Sprintf("%v", mapitem.Key) +// listKeys returns a list of the keys of a Go-YAML v3 MappingNode (map) +func listKeys(mappingNode *yamlv3.Node) []string { + keys := []string{} + for i := 0; i < len(mappingNode.Content); i += 2 { + keys = append(keys, mappingNode.Content[i].Value) } return keys } -// getValueByKey returns the value for a given key in a provided MapSlice, or nil with an error if there is no such entry. This is comparable to getting a value from a map with `foobar[key]`. -func getValueByKey(mapslice yaml.MapSlice, key string) (interface{}, error) { - for _, element := range mapslice { - if element.Key == key { - return element.Value, nil - } - } - - return nil, &KeyNotFoundInMapError{MissingKey: key, AvailableKeys: listKeys(mapslice)} +// ListStringKeys lists the keys in a MappingNode +func ListStringKeys(mappingNode *yamlv3.Node) ([]string, error) { + return listKeys(mappingNode), nil } -func getEntryByIdentifierAndName(list []yaml.MapSlice, identifier string, name interface{}) (yaml.MapSlice, error) { - for _, mapslice := range list { - for _, element := range mapslice { - if element.Key == identifier && element.Value == name { - return mapslice, nil - } +// getValueByKey returns the value for a given key in a provided mapping node, +// or nil with an error if there is no such entry. This is comparable to getting +// a value from a map with `foobar[key]`. +func getValueByKey(mappingNode *yamlv3.Node, key string) (*yamlv3.Node, error) { + for i := 0; i < len(mappingNode.Content); i += 2 { + k, v := mappingNode.Content[i], mappingNode.Content[i+1] + if k.Value == key { + return v, nil } } - return nil, fmt.Errorf("there is no entry %s=%v in the list", identifier, name) + return nil, &KeyNotFoundInMapError{ + MissingKey: key, + AvailableKeys: listKeys(mappingNode), + } } diff --git a/pkg/v1/ytbx/path.go b/pkg/ytbx/path.go similarity index 83% rename from pkg/v1/ytbx/path.go rename to pkg/ytbx/path.go index 1085ae8..601169a 100644 --- a/pkg/v1/ytbx/path.go +++ b/pkg/ytbx/path.go @@ -26,7 +26,7 @@ import ( "strconv" "strings" - yaml "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) // PathStyle is a custom type for supported path styles @@ -68,8 +68,11 @@ func (path Path) String() string { // ToGoPatchStyle returns the path as a GoPatch style string. func (path *Path) ToGoPatchStyle() string { - sections := []string{""} + if len(path.PathElements) == 0 { + return "/" + } + sections := []string{""} for _, element := range path.PathElements { switch { case element.Name != "" && element.Key == "": @@ -218,7 +221,7 @@ func ListPaths(location string, style PathStyle) ([]Path, error) { for idx, document := range inputfile.Documents { root := Path{DocumentIdx: idx} - traverseTree(root, document, func(path Path, _ interface{}) { + traverseTree(root, document, func(path Path, _ *yamlv3.Node) { paths = append(paths, path) }) } @@ -226,28 +229,56 @@ func ListPaths(location string, style PathStyle) ([]Path, error) { return paths, nil } -func traverseTree(path Path, obj interface{}, leafFunc func(path Path, value interface{})) { - switch tobj := obj.(type) { - case []interface{}: - if identifier := GetIdentifierFromNamedList(tobj); identifier != "" { - for _, entry := range tobj { - name, data := splitEntryIntoNameAndData(entry.(yaml.MapSlice), identifier) - traverseTree(NewPathWithNamedListElement(path, identifier, name), data, leafFunc) +func traverseTree(path Path, node *yamlv3.Node, leafFunc func(p Path, n *yamlv3.Node)) { + switch node.Kind { + case yamlv3.DocumentNode: + traverseTree( + path, + node.Content[0], + leafFunc, + ) + + case yamlv3.SequenceNode: + if identifier := GetIdentifierFromNamedList(node); identifier != "" { + for _, mappingNode := range node.Content { + name, _ := getValueByKey(mappingNode, identifier) + tmpPath := NewPathWithNamedListElement(path, identifier, name.Value) + for i := 0; i < len(mappingNode.Content); i += 2 { + k, v := mappingNode.Content[i], mappingNode.Content[i+1] + if k.Value == identifier { // skip the identifier mapping entry + continue + } + + traverseTree( + NewPathWithNamedElement(tmpPath, k.Value), + v, + leafFunc, + ) + } } } else { - for idx, entry := range tobj { - traverseTree(NewPathWithIndexedListElement(path, idx), entry, leafFunc) + for idx, entry := range node.Content { + traverseTree( + NewPathWithIndexedListElement(path, idx), + entry, + leafFunc, + ) } } - case yaml.MapSlice: - for _, mapitem := range tobj { - traverseTree(NewPathWithNamedElement(path, mapitem.Key), mapitem.Value, leafFunc) + case yamlv3.MappingNode: + for i := 0; i < len(node.Content); i += 2 { + k, v := node.Content[i], node.Content[i+1] + traverseTree( + NewPathWithNamedElement(path, k.Value), + v, + leafFunc, + ) } default: - leafFunc(path, obj) + leafFunc(path, node) } } @@ -304,15 +335,24 @@ func ParseGoPatchStylePathString(path string) (Path, error) { // ParseDotStylePathString returns a path by parsing a string representation // which is assumed to be a Dot-Style path. -func ParseDotStylePathString(path string, obj interface{}) (Path, error) { +func ParseDotStylePathString(path string, node *yamlv3.Node) (Path, error) { + if node.Kind != yamlv3.DocumentNode { + return Path{}, fmt.Errorf("node has to be of kind DocumentNode for parsing a document path") + } + elements := make([]PathElement, 0) + pointer := node.Content[0] - pointer := obj for _, section := range strings.Split(path, ".") { switch { - case isMapSlice(pointer): - mapslice := pointer.(yaml.MapSlice) - if value, err := getValueByKey(mapslice, section); err == nil { + case pointer == nil: + // If the pointer is nil, it means that the previous section of the path + // string could not be found in the data structure and that all remaining + // sections are assumed to be of type map. + elements = append(elements, PathElement{Idx: -1, Name: section}) + + case pointer.Kind == yamlv3.MappingNode: + if value, err := getValueByKey(pointer, section); err == nil { pointer = value elements = append(elements, PathElement{Idx: -1, Name: section}) @@ -321,8 +361,8 @@ func ParseDotStylePathString(path string, obj interface{}) (Path, error) { elements = append(elements, PathElement{Idx: -1, Name: section}) } - case isList(pointer): - list := pointer.([]interface{}) + case pointer.Kind == yamlv3.SequenceNode: + list := pointer.Content if id, err := strconv.Atoi(section); err == nil { if id < 0 || id >= len(list) { return Path{}, &InvalidPathString{ @@ -336,10 +376,10 @@ func ParseDotStylePathString(path string, obj interface{}) (Path, error) { elements = append(elements, PathElement{Idx: id}) } else { - identifier := GetIdentifierFromNamedList(list) - value, ok := getEntryFromNamedList(list, identifier, section) + identifier := GetIdentifierFromNamedList(pointer) + value, ok := getEntryFromNamedList(pointer, identifier, section) if !ok { - names, err := listNamesOfNamedList(list, identifier) + names, err := listNamesOfNamedList(pointer, identifier) if err != nil { return Path{}, &InvalidPathString{ Style: DotStyle, @@ -358,12 +398,6 @@ func ParseDotStylePathString(path string, obj interface{}) (Path, error) { pointer = value elements = append(elements, PathElement{Idx: -1, Key: identifier, Name: section}) } - - case pointer == nil: - // If the pointer is nil, it means that the previous section of the path - // string could not be found in the data structure and that all remaining - // sections are assumed to be of type map. - elements = append(elements, PathElement{Idx: -1, Name: section}) } } @@ -372,12 +406,12 @@ func ParseDotStylePathString(path string, obj interface{}) (Path, error) { // ParsePathString returns a path by parsing a string representation // of a path, which can be one of the supported types. -func ParsePathString(pathString string, obj interface{}) (Path, error) { +func ParsePathString(pathString string, node *yamlv3.Node) (Path, error) { if strings.HasPrefix(pathString, "/") { return ParseGoPatchStylePathString(pathString) } - return ParseDotStylePathString(pathString, obj) + return ParseDotStylePathString(pathString, node) } func (element PathElement) isMapElement() bool { diff --git a/pkg/v1/ytbx/path_test.go b/pkg/ytbx/path_test.go similarity index 97% rename from pkg/v1/ytbx/path_test.go rename to pkg/ytbx/path_test.go index 9f6d412..81c2f8b 100644 --- a/pkg/v1/ytbx/path_test.go +++ b/pkg/ytbx/path_test.go @@ -24,12 +24,13 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - . "github.com/homeport/ytbx/pkg/v1/ytbx" + . "github.com/homeport/ytbx/pkg/ytbx" + yamlv3 "gopkg.in/yaml.v3" ) -func getExampleDocument() interface{} { +func getExampleDocument() *yamlv3.Node { input, err := LoadFile(assets("testbed", "example.yml")) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(len(input.Documents)).To(BeIdenticalTo(1)) return input.Documents[0] @@ -68,7 +69,7 @@ var _ = Describe("path tests", func() { It("should parse string with non-existing map elements", func() { path, err := ParseDotStylePathString("yaml.update.newkey", getExampleDocument()) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(path).To(BeEquivalentTo(Path{DocumentIdx: 0, PathElements: []PathElement{ {Idx: -1, Key: "", Name: "yaml"}, {Idx: -1, Key: "", Name: "update"}, @@ -189,7 +190,8 @@ var _ = Describe("path tests", func() { {Idx: -1, Key: "", Name: "yaml"}, {Idx: -1, Key: "", Name: "structure"}, {Idx: -1, Key: "", Name: "dot"}, - }}, + }, + }, { DocumentIdx: 0, PathElements: []PathElement{ {Idx: -1, Key: "", Name: "list"}, diff --git a/pkg/v1/ytbx/restructure.go b/pkg/ytbx/restructure.go similarity index 57% rename from pkg/v1/ytbx/restructure.go rename to pkg/ytbx/restructure.go index b729bd2..f9873b3 100644 --- a/pkg/v1/ytbx/restructure.go +++ b/pkg/ytbx/restructure.go @@ -21,15 +21,13 @@ package ytbx import ( - "encoding/json" "sort" - yaml "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" ) -// DisableRemainingKeySort disables that that during restructuring of map keys, -// all unknown keys are also sorted in such a way that it ideally improves the -// readability. +// DisableRemainingKeySort disables that during restructuring of map keys, all +// unknown keys are also sorted in such a way that it improves the readability. var DisableRemainingKeySort = false var knownKeyOrders = [][]string{ @@ -66,6 +64,30 @@ func lookupMap(list []string) map[string]int { return result } +func lookupMapOfContentList(list []*yamlv3.Node) map[string]int { + lookup := make(map[string]int, len(list)) + for i := 0; i < len(list); i += 2 { + lookup[list[i].Value] = i + } + + return lookup +} + +func maxDepth(node *yamlv3.Node) (max int) { + rootPath, _ := ParseGoPatchStylePathString("/") + traverseTree( + rootPath, + node, + func(p Path, _ *yamlv3.Node) { + if depth := len(p.PathElements); depth > max { + max = depth + } + }, + ) + + return max +} + func countCommonKeys(keys []string, list []string) int { counter, lookup := 0, lookupMap(keys) for _, key := range list { @@ -88,45 +110,41 @@ func commonKeys(setA []string, setB []string) []string { return result } -func reorderMapsliceKeys(input yaml.MapSlice, keys []string) yaml.MapSlice { - // Create list with all remaining keys: those that are part of the input - // YAML MapSlice, but not listed in the keys list - remainingKeys, lookup := []string{}, lookupMap(keys) - for _, mapitem := range input { - key := mapitem.Key.(string) - if _, ok := lookup[key]; !ok { +func reorderKeyValuePairsInMappingNodeContent(mappingNode *yamlv3.Node, keys []string) { + // Create list with all keys, that are not part of the provided list of keys + remainingKeys, keysLookup := []string{}, lookupMap(keys) + for i := 0; i < len(mappingNode.Content); i += 2 { + key := mappingNode.Content[i].Value + if _, ok := keysLookup[key]; !ok { remainingKeys = append(remainingKeys, key) } } // Sort remaining keys by sorting long and possibly hard to read structure - // to the end of the map + // to the end of the mapping if !DisableRemainingKeySort { sort.Slice(remainingKeys, func(i, j int) bool { - valI, _ := getValueByKey(input, remainingKeys[i]) - valJ, _ := getValueByKey(input, remainingKeys[j]) - marI, _ := json.Marshal(valI) - marJ, _ := json.Marshal(valJ) - return len(marI) < len(marJ) + valI, _ := getValueByKey(mappingNode, remainingKeys[i]) + valJ, _ := getValueByKey(mappingNode, remainingKeys[j]) + return maxDepth(valI) < maxDepth(valJ) }) } - // Rebuild a new YAML MapSlice key by key by using first the keys from the - // reorder list and then all remaining keys - result := yaml.MapSlice{} + // Rebuild a new YAML Node list (content) key by key by using first the keys + // from the reorder list and then all remaining keys + content, contentLookup := []*yamlv3.Node{}, lookupMapOfContentList(mappingNode.Content) for _, key := range append(keys, remainingKeys...) { - // Ignore the error field here since we know what keys there are - value, _ := getValueByKey(input, key) - result = append(result, yaml.MapItem{ - Key: key, - Value: value, - }) + idx := contentLookup[key] + content = append(content, + mappingNode.Content[idx], + mappingNode.Content[idx+1], + ) } - return result + mappingNode.Content = content } -func getSuitableReorderFunction(keys []string) func(yaml.MapSlice) yaml.MapSlice { +func getSuitableReorderFunction(keys []string) func(*yamlv3.Node) { topCandidateIdx, topCandidateHits := -1, -1 for idx, candidate := range knownKeyOrders { if count := countCommonKeys(keys, candidate); count > topCandidateHits { @@ -136,45 +154,40 @@ func getSuitableReorderFunction(keys []string) func(yaml.MapSlice) yaml.MapSlice } if topCandidateIdx >= 0 { - return func(input yaml.MapSlice) yaml.MapSlice { - return reorderMapsliceKeys(input, commonKeys(knownKeyOrders[topCandidateIdx], keys)) + return func(input *yamlv3.Node) { + reorderKeyValuePairsInMappingNodeContent( + input, + commonKeys(knownKeyOrders[topCandidateIdx], keys), + ) } } return nil } -// RestructureObject takes an object and traverses down any sub elements such as list entries or map values to recursively call restructure itself. On YAML MapSlices (maps), it will use a look-up mechanism to decide if the order of key in that map needs to be rearranged to meet some known established human order. -func RestructureObject(obj interface{}) interface{} { - switch val := obj.(type) { - case yaml.MapSlice: - // Restructure the YAML MapSlice keys - if keys, err := ListStringKeys(val); err == nil { - if fn := getSuitableReorderFunction(keys); fn != nil { - val = fn(val) - } +// RestructureObject takes an object and traverses down any sub elements such as +// list entries or map values to recursively call restructure itself. On YAML +// MappingNodes, it will use a look-up mechanism to decide if the order of key +// in that map need to be rearranged to meet some known established human order. +func RestructureObject(node *yamlv3.Node) { + switch node.Kind { + case yamlv3.DocumentNode: + RestructureObject(node.Content[0]) + + case yamlv3.MappingNode: + keys := listKeys(node) + if fn := getSuitableReorderFunction(keys); fn != nil { + fn(node) } // Restructure the values of the respective keys of this YAML MapSlice - for idx := range val { - val[idx].Value = RestructureObject(val[idx].Value) + for i := 0; i < len(node.Content); i += 2 { + RestructureObject(node.Content[i+1]) } - return val - - case []interface{}: - for i := range val { - val[i] = RestructureObject(val[i]) + case yamlv3.SequenceNode: + for i := range node.Content { + RestructureObject(node.Content[i]) } - return val - - case []yaml.MapSlice: - for i := range val { - val[i] = RestructureObject(val[i]).(yaml.MapSlice) - } - return val - - default: - return obj } } diff --git a/pkg/v1/ytbx/restructure_test.go b/pkg/ytbx/restructure_test.go similarity index 67% rename from pkg/v1/ytbx/restructure_test.go rename to pkg/ytbx/restructure_test.go index 79eb0a8..0bc6c85 100644 --- a/pkg/v1/ytbx/restructure_test.go +++ b/pkg/ytbx/restructure_test.go @@ -24,52 +24,48 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - . "github.com/homeport/ytbx/pkg/v1/ytbx" - yaml "gopkg.in/yaml.v2" + . "github.com/homeport/ytbx/pkg/ytbx" ) var _ = Describe("Restructure order of map keys", func() { Context("YAML MapSlice key reorderings of the MapSlice itself", func() { It("should restructure Concourse root level keys", func() { - input := yml("{ groups: [], jobs: [], resources: [], resource_types: [] }") - output := RestructureObject(input).(yaml.MapSlice) + example := yml("{ groups: [], jobs: [], resources: [], resource_types: [] }") + RestructureObject(example) - keys, err := ListStringKeys(output) - Expect(err).To(BeNil()) + keys, err := ListStringKeys(example) + Expect(err).ToNot(HaveOccurred()) Expect(keys).To(BeEquivalentTo([]string{"jobs", "resources", "resource_types", "groups"})) }) It("should restructure Concourse resource and resource_type keys", func() { - input := yml("{ source: {}, name: {}, type: {}, privileged: {} }") - output := RestructureObject(input).(yaml.MapSlice) + example := yml("{ source: {}, name: {}, type: {}, privileged: {} }") + RestructureObject(example) - keys, err := ListStringKeys(output) - Expect(err).To(BeNil()) + keys, err := ListStringKeys(example) + Expect(err).ToNot(HaveOccurred()) Expect(keys).To(BeEquivalentTo([]string{"name", "type", "source", "privileged"})) }) }) Context("YAML MapSlice key reorderings of the MapSlice values", func() { It("should restructure Concourse resource keys as part as part of a MapSlice value", func() { - input := yml("{ resources: [ { privileged: false, source: { branch: foo, paths: [] }, name: myname, type: mytype } ] }") - output := RestructureObject(input).(yaml.MapSlice) + example := yml("{ resources: [ { privileged: false, source: { branch: foo, paths: [] }, name: myname, type: mytype } ] }") + RestructureObject(example) - value := output[0].Value.([]interface{}) - obj := value[0].(yaml.MapSlice) - - keys, err := ListStringKeys(obj) - Expect(err).To(BeNil()) + keys, err := ListStringKeys(example.Content[1].Content[0]) + Expect(err).ToNot(HaveOccurred()) Expect(keys).To(BeEquivalentTo([]string{"name", "type", "source", "privileged"})) }) }) Context("Restructure code tries to rearrange even unknown keys", func() { It("should reorder map keys in a somehow more readable way", func() { - input := yml(`{"list":["one","two","three"], "some":{"deep":{"structure":{"where":{"you":{"loose":{"focus":{"one":1,"two":2}}}}}}}, "name":"here", "release":"this"}`) - output := RestructureObject(input).(yaml.MapSlice) + example := yml(`{"list":["one","two","three"], "some":{"deep":{"structure":{"where":{"you":{"loose":{"focus":{"one":1,"two":2}}}}}}}, "name":"here", "release":"this"}`) + RestructureObject(example) - keys, err := ListStringKeys(output) - Expect(err).To(BeNil()) + keys, err := ListStringKeys(example) + Expect(err).ToNot(HaveOccurred()) Expect(keys).To(BeEquivalentTo([]string{"name", "release", "list", "some"})) }) }) diff --git a/pkg/v1/ytbx/ytbx_suite_test.go b/pkg/ytbx/ytbx_suite_test.go similarity index 61% rename from pkg/v1/ytbx/ytbx_suite_test.go rename to pkg/ytbx/ytbx_suite_test.go index 51bbdcb..277f998 100644 --- a/pkg/v1/ytbx/ytbx_suite_test.go +++ b/pkg/ytbx/ytbx_suite_test.go @@ -24,16 +24,17 @@ import ( "fmt" "os" "path/filepath" - "reflect" + "strconv" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/types" "github.com/gonvenience/bunt" "github.com/gonvenience/neat" - "github.com/homeport/ytbx/pkg/v1/ytbx" - yaml "gopkg.in/yaml.v2" + "github.com/homeport/ytbx/pkg/ytbx" + yamlv3 "gopkg.in/yaml.v3" ) var exampleTOML = ` @@ -87,7 +88,7 @@ var _ = AfterSuite(func() { func assets(pathElement ...string) string { targetPath := filepath.Join(append( - []string{"..", "..", "..", "assets"}, + []string{"..", "..", "assets"}, pathElement..., )...) @@ -97,8 +98,8 @@ func assets(pathElement ...string) string { return abs } -func yml(input string) yaml.MapSlice { - // If input is a file loacation, load this as YAML +func yml(input string) *yamlv3.Node { + // If input is a file location, load this as YAML if _, err := os.Open(input); err == nil { var content ytbx.InputFile var err error @@ -110,41 +111,20 @@ func yml(input string) yaml.MapSlice { Fail(fmt.Sprintf("Failed to load YAML MapSlice from '%s': Provided file contains more than one document", input)) } - switch content.Documents[0].(type) { - case yaml.MapSlice: - return content.Documents[0].(yaml.MapSlice) - } - - Fail(fmt.Sprintf("Failed to load YAML MapSlice from '%s': Document #0 in YAML is not of type MapSlice, but is %s", input, reflect.TypeOf(content.Documents[0]))) + return content.Documents[0] } // Load YAML by parsing the actual string as YAML if it was not a file location - doc := singleDoc(input) - switch mapslice := doc.(type) { - case yaml.MapSlice: - return mapslice - } - - Fail(fmt.Sprintf("Failed to use YAML, parsed data is not a YAML MapSlice:\n%s\n", input)) - return nil + document := singleDoc(input) + return document.Content[0] } -func list(input string) []interface{} { - doc := singleDoc(input) - - switch tobj := doc.(type) { - case []interface{}: - return tobj - - case []yaml.MapSlice: - return ytbx.SimplifyList(tobj) - } - - Fail(fmt.Sprintf("Failed to use YAML, parsed data is not a slice of any kind:\n%s\nIt was parsed as: %#v", input, doc)) - return nil +func list(input string) *yamlv3.Node { + document := singleDoc(input) + return document.Content[0] } -func singleDoc(input string) interface{} { +func singleDoc(input string) *yamlv3.Node { docs, err := ytbx.LoadYAMLDocuments([]byte(input)) if err != nil { Fail(fmt.Sprintf("Failed to parse as YAML:\n%s\n\n%v", input, err)) @@ -157,18 +137,93 @@ func singleDoc(input string) interface{} { return docs[0] } -func grab(obj interface{}, path string) interface{} { - value, err := ytbx.Grab(obj, path) +func grab(node *yamlv3.Node, path string) interface{} { + v, err := ytbx.Grab(node, path) if err != nil { - out, _ := neat.ToYAMLString(obj) + out, _ := neat.ToYAMLString(node) Fail(fmt.Sprintf("Failed to grab by path %s from %s", path, out)) } - return value + switch v.Tag { + case "!!str": + return v.Value + + case "!!int": + i, _ := strconv.Atoi(v.Value) + return i + } + + return v } -func grabError(obj interface{}, path string) string { - value, err := ytbx.Grab(obj, path) +func grabError(node *yamlv3.Node, path string) string { + value, err := ytbx.Grab(node, path) Expect(value).To(BeNil()) return err.Error() } + +func BeAsNode(expected *yamlv3.Node) GomegaMatcher { + return &nodeMatcher{ + expected: expected, + } +} + +type nodeMatcher struct { + expected *yamlv3.Node +} + +func (matcher *nodeMatcher) Match(actual interface{}) (success bool, err error) { + actualNodePtr, ok := actual.(*yamlv3.Node) + if !ok { + return false, fmt.Errorf("BeAsNode matcher expected a Go YAML v3 Node, not %T", actual) + } + + return isSameNode(actualNodePtr, matcher.expected) +} + +func (matcher *nodeMatcher) FailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected\n\t%#v\nto be same as\n\t%#v", + actual, + matcher.expected) +} + +func (matcher *nodeMatcher) NegatedFailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected\n\t%#v\nnot to be same as\n\t%#v", + actual, + matcher.expected, + ) +} + +func isSameNode(a, b *yamlv3.Node) (bool, error) { + if a == nil && b == nil { + return true, nil + } + + if (a == nil && b != nil) || (a != nil && b == nil) { + return false, nil + } + + if a.Kind != b.Kind { + return false, nil + } + + if a.Tag != b.Tag { + return false, nil + } + + if a.Value != b.Value { + return false, nil + } + + if len(a.Content) != len(b.Content) { + return false, nil + } + + for i := range a.Content { + if same, err := isSameNode(a.Content[i], b.Content[i]); !same { + return same, err + } + } + + return true, nil +}