From 30b419e56b3275892219469b8da787543f7f07f9 Mon Sep 17 00:00:00 2001 From: Luc DUZAN Date: Thu, 20 Jun 2024 17:42:04 +0200 Subject: [PATCH] keep order from api response --- client/client.go | 18 ++++++-- orderedjson/orderedjson.go | 60 ++++++++++++++++++++++++ orderedjson/orderingjson_test.go | 79 ++++++++++++++++++++++++++++++++ printutils/printYaml.go | 53 ++++++++++++++++----- printutils/printYaml_test.go | 53 ++++++++++++++++++++- 5 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 orderedjson/orderedjson.go create mode 100644 orderedjson/orderingjson_test.go diff --git a/client/client.go b/client/client.go index 11cef47..c98ede5 100644 --- a/client/client.go +++ b/client/client.go @@ -4,13 +4,15 @@ import ( "crypto/tls" "encoding/json" "fmt" + "os" + "strings" + + "github.com/conduktor/ctl/orderedjson" "github.com/conduktor/ctl/printutils" "github.com/conduktor/ctl/resource" "github.com/conduktor/ctl/schema" "github.com/conduktor/ctl/utils" "github.com/go-resty/resty/v2" - "os" - "strings" ) type Client struct { @@ -157,12 +159,18 @@ func (client *Client) Apply(resource *resource.Resource, dryMode bool) (string, } func printResponseAsYaml(bytes []byte) error { - var data interface{} + var data orderedjson.OrderedData //using this instead of interface{} keep json order + var finalData interface{} // in case it does not work we will failback to deserializing directly to interface{} err := json.Unmarshal(bytes, &data) if err != nil { - return err + err = json.Unmarshal(bytes, &finalData) + if err != nil { + return err + } + } else { + finalData = data } - return printutils.PrintResourceLikeYamlFile(os.Stdout, data) + return printutils.PrintResourceLikeYamlFile(os.Stdout, finalData) } func (client *Client) Get(kind *schema.Kind, parentPathValue []string) error { diff --git a/orderedjson/orderedjson.go b/orderedjson/orderedjson.go new file mode 100644 index 0000000..86278f2 --- /dev/null +++ b/orderedjson/orderedjson.go @@ -0,0 +1,60 @@ +package orderedjson + +import ( + "encoding/json" + + orderedmap "github.com/wk8/go-ordered-map/v2" + yaml "gopkg.in/yaml.v3" +) + +type OrderedData struct { + orderedMap *orderedmap.OrderedMap[string, OrderedData] + array *[]OrderedData + fallback *interface{} +} + +func (orderedData *OrderedData) UnmarshalJSON(data []byte) error { + orderedData.orderedMap = orderedmap.New[string, OrderedData]() + err := json.Unmarshal(data, &orderedData.orderedMap) + if err != nil { + orderedData.orderedMap = nil + orderedData.array = new([]OrderedData) + err = json.Unmarshal(data, orderedData.array) + } + if err != nil { + orderedData.array = nil + orderedData.fallback = new(interface{}) + err = json.Unmarshal(data, &orderedData.fallback) + } + return err +} + +// TODO: remove once hack in printYaml is not needed anymore +func (orderedData *OrderedData) GetMapOrNil() *orderedmap.OrderedMap[string, OrderedData] { + return orderedData.orderedMap +} + +func (orderedData OrderedData) MarshalJSON() ([]byte, error) { + if orderedData.orderedMap != nil { + return json.Marshal(orderedData.orderedMap) + } else if orderedData.array != nil { + return json.Marshal(orderedData.array) + } else if orderedData.fallback != nil { + return json.Marshal(orderedData.fallback) + } else { + return json.Marshal(nil) + } +} + +func (orderedData OrderedData) MarshalYAML() (interface{}, error) { + if orderedData.orderedMap != nil { + return orderedData.orderedMap, nil + } else if orderedData.array != nil { + return orderedData.array, nil + } + return orderedData.fallback, nil +} + +func (orderedData *OrderedData) UnmarshalYAML(value *yaml.Node) error { + panic("Not supported") +} diff --git a/orderedjson/orderingjson_test.go b/orderedjson/orderingjson_test.go new file mode 100644 index 0000000..245f26c --- /dev/null +++ b/orderedjson/orderingjson_test.go @@ -0,0 +1,79 @@ +package orderedjson + +import ( + "encoding/json" + "fmt" + "testing" + + yaml "gopkg.in/yaml.v3" +) + +func TestOrderedRecursiveMap(t *testing.T) { + testForJson(t, `{"name":"John","age":30,"city":"New York","children":[{"name":"Alice","age":5},{"name":"Bob","age":7}],"parent":{"name":"Jane","age":60,"city":"New York"}}`) + testForJson(t, `"yo"`) + testForJson(t, `true`) + testForJson(t, `false`) + testForJson(t, `42`) + testForJson(t, `42.2`) + testForJson(t, `[]`) + testForJson(t, `{}`) + testForJson(t, `{"z":{"x":{"v":{}}},"y":{"u":{"t":"p"}}}`) + testForJson(t, `[[[[]]]]`) + testForJson(t, `[{"z":42},{"b":{},"y":41,"a":[[{"z":42},{"b":{},"y":41,"a":[[{"z":42},{"b":{},"y":41,"a":[[{"z":42},{"b":{},"y":41,"a":[]}]]}]]}]]}]`) +} + +func testForJson(t *testing.T, originalJSON string) { + // Unmarshal the JSON into an OrderedRecursiveMap + var omap OrderedData + err := json.Unmarshal([]byte(originalJSON), &omap) + if err != nil { + t.Fatalf("Failed to unmarshal JSON: %+v", err) + } + + fmt.Printf("%v\n", omap) + // Marshal the OrderedRecursiveMap back into JSON + marshaledJSON, err := json.Marshal(&omap) + if err != nil { + t.Fatalf("Failed to marshal OrderedRecursiveMap: %v", err) + } + + // Check if the original JSON and the marshaled JSON are the same + if originalJSON != string(marshaledJSON) { + t.Errorf("Original JSON and marshaled JSON do not match. Original: %s, Marshaled: %s", originalJSON, string(marshaledJSON)) + } +} + +func TestYamlMarshallingKeepOrderTo(t *testing.T) { + // Unmarshal the JSON into an OrderedRecursiveMap + var omap OrderedData + err := json.Unmarshal([]byte(`{"name":"John","age":30,"city":"New York","children":[{"name":"Alice","age":5},{"name":"Bob","age":7}],"parent":{"name":"Jane","age":60,"city":"New York"}}`), &omap) + if err != nil { + t.Fatalf("Failed to unmarshal JSON: %+v", err) + } + + fmt.Printf("%v\n", omap) + // Marshal the OrderedRecursiveMap back into JSON + marshaledYaml, err := yaml.Marshal(&omap) + if err != nil { + t.Fatalf("Failed to marshal OrderedRecursiveMap: %v", err) + } + + expected := `name: John +age: 30 +city: New York +children: + - name: Alice + age: 5 + - name: Bob + age: 7 +parent: + name: Jane + age: 60 + city: New York +` + + // Check if the original JSON and the marshaled JSON are the same + if expected != string(marshaledYaml) { + t.Errorf("Marshalled yaml is not valid. Got:\n##\n%s\n##\n,\nMarshaled:\n##\n%s\n##", string(marshaledYaml), expected) + } +} diff --git a/printutils/printYaml.go b/printutils/printYaml.go index 79784a9..5899026 100644 --- a/printutils/printYaml.go +++ b/printutils/printYaml.go @@ -5,6 +5,8 @@ import ( "io" "slices" + "github.com/conduktor/ctl/orderedjson" + orderedmap "github.com/wk8/go-ordered-map/v2" yaml "gopkg.in/yaml.v3" ) @@ -21,6 +23,7 @@ func printKeyYaml(w io.Writer, key string, data interface{}) error { return nil } +// TODO: delete once backend properly send resource fields in correct order // this print a interface that is expected to a be a resource // with the following field "version", "kind", "spec", "metadata" // wit the field in a defined order. @@ -31,21 +34,49 @@ func printResource(w io.Writer, data interface{}) error { if err != nil { return err } - asMap, ok := data.(map[string]interface{}) - if !ok { - fmt.Fprint(w, string(yamlBytes)) + asMap, isMap := data.(map[string]interface{}) + orderedData, isOrderedData := data.(orderedjson.OrderedData) + isOrderedMap := false + var asOrderedMap *orderedmap.OrderedMap[string, orderedjson.OrderedData] + if isOrderedData { + asOrderedMap = orderedData.GetMapOrNil() + isOrderedMap = asOrderedMap != nil + } + if isOrderedMap { + printResourceOrderedMapInCorrectOrder(w, *asOrderedMap) + } else if isMap { + printResourceMapInCorrectOrder(w, asMap) } else { - wantedKeys := []string{"apiVersion", "kind", "metadata", "spec"} - for _, wantedKey := range wantedKeys { - printKeyYaml(w, wantedKey, asMap[wantedKey]) + fmt.Fprint(w, string(yamlBytes)) + } + return err +} + +func printResourceMapInCorrectOrder(w io.Writer, dataAsMap map[string]interface{}) { + wantedKeys := []string{"apiVersion", "kind", "metadata", "spec"} + for _, wantedKey := range wantedKeys { + printKeyYaml(w, wantedKey, dataAsMap[wantedKey]) + } + for otherKey, data := range dataAsMap { + if !slices.Contains(wantedKeys, otherKey) { + printKeyYaml(w, otherKey, data) } - for otherKey, data := range asMap { - if !slices.Contains(wantedKeys, otherKey) { - printKeyYaml(w, otherKey, data) - } + } +} + +func printResourceOrderedMapInCorrectOrder(w io.Writer, dataAsMap orderedmap.OrderedMap[string, orderedjson.OrderedData]) { + wantedKeys := []string{"apiVersion", "kind", "metadata", "spec"} + for _, wantedKey := range wantedKeys { + value, ok := dataAsMap.Get(wantedKey) + if ok { + printKeyYaml(w, wantedKey, value) + } + } + for pair := dataAsMap.Oldest(); pair != nil; pair = pair.Next() { + if !slices.Contains(wantedKeys, pair.Key) { + printKeyYaml(w, pair.Key, pair.Value) } } - return err } // take a interface that can be a resource or multiple resource diff --git a/printutils/printYaml_test.go b/printutils/printYaml_test.go index ac077bb..1715665 100644 --- a/printutils/printYaml_test.go +++ b/printutils/printYaml_test.go @@ -5,9 +5,11 @@ import ( "encoding/json" "strings" "testing" + + "github.com/conduktor/ctl/orderedjson" ) -func TestPrintResourceLikeYamlOnSingleResource(t *testing.T) { +func TestPrintResourceLikeYamlOnSingleResourceFromNormalJson(t *testing.T) { resourceFromBe := `{"spec": "someSpec", "apiVersion": "v4", "kind": "Gelato", "metadata": "arancia"}` var data interface{} err := json.Unmarshal([]byte(resourceFromBe), &data) @@ -27,6 +29,29 @@ spec: someSpec`) } } +func TestPrintResourceLikeYamlOnSingleResourceFromOrderedJson(t *testing.T) { + resourceFromBe := `{"spec": "someSpec", "apiVersion": "v4", "kind": "Gelato", "metadata": {"z": 1, "t": 2, "x": 3}}` + var data orderedjson.OrderedData + err := json.Unmarshal([]byte(resourceFromBe), &data) + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + PrintResourceLikeYamlFile(&output, data) + expected := strings.TrimSpace(` +apiVersion: v4 +kind: Gelato +metadata: + z: 1 + t: 2 + x: 3 +spec: someSpec`) + got := strings.TrimSpace(output.String()) + if got != expected { + t.Errorf("got:\n%s \nexpected:\n%s", got, expected) + } +} + func TestPrintResourceLikeYamlInCaseOfScalarValue(t *testing.T) { resourceFromBe := `[[1], 3, true, "cat"]` var data interface{} @@ -51,7 +76,31 @@ cat`) } } -func TestPrintResourceLikeYamlOnMultileResources(t *testing.T) { +func TestPrintResourceLikeYamlOnResourcesWithNewUnexpectedFieldFromOrderedJson(t *testing.T) { + resourceFromBe := `{"spec": "someSpec", "apiVersion": "v4", "newKind": "Gelato", "metadata": {"z": 1, "t": 2, "x": 3}}` + var data orderedjson.OrderedData + err := json.Unmarshal([]byte(resourceFromBe), &data) + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + PrintResourceLikeYamlFile(&output, data) + expected := strings.TrimSpace(` +apiVersion: v4 +metadata: + z: 1 + t: 2 + x: 3 +spec: someSpec +newKind: Gelato +`) + got := strings.TrimSpace(output.String()) + if got != expected { + t.Errorf("got:\n%s \nexpected:\n%s", got, expected) + } +} + +func TestPrintResourceLikeYamlOnResourcesWithNewUnexpectedFieldFromNormalJson(t *testing.T) { resourceFromBe := `{"spec": "someSpec", "apiVersion": "v4", "newKind": "Gelato", "metadata": "arancia"}` var data interface{} err := json.Unmarshal([]byte(resourceFromBe), &data)