Skip to content

Commit

Permalink
keep order from api response
Browse files Browse the repository at this point in the history
  • Loading branch information
strokyl committed Jun 20, 2024
1 parent 163e747 commit 30b419e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 18 deletions.
18 changes: 13 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
60 changes: 60 additions & 0 deletions orderedjson/orderedjson.go
Original file line number Diff line number Diff line change
@@ -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")
}
79 changes: 79 additions & 0 deletions orderedjson/orderingjson_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
53 changes: 42 additions & 11 deletions printutils/printYaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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.
Expand All @@ -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
Expand Down
53 changes: 51 additions & 2 deletions printutils/printYaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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{}
Expand All @@ -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)
Expand Down

0 comments on commit 30b419e

Please sign in to comment.