Skip to content

Commit

Permalink
feat: allow overwrites of array fields (#30)
Browse files Browse the repository at this point in the history
* feat: allow overwrites of array fields

Signed-off-by: André Kesser <[email protected]>

* fix: fix typos

Signed-off-by: André Kesser <[email protected]>

* fix: renamed test function

Signed-off-by: André Kesser <[email protected]>

---------

Signed-off-by: André Kesser <[email protected]>
Co-authored-by: André Kesser <[email protected]>
  • Loading branch information
akesser and André Kesser authored Jun 27, 2024
1 parent 4c6bb2a commit e9f1f3b
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## WARNING: This file was autogenerated!
## Manual modifications will be overwritten
## unless ignore: true is set in generate.yaml!
## Last Modification: 10:09:00 on 05-08-2024.
## Last Modification: 10:57:26 on 05-16-2024.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
Expand Down
2 changes: 1 addition & 1 deletion package/EKS-Cluster/definition.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## WARNING: This file was autogenerated!
## Manual modifications will be overwritten
## unless ignore: true is set in generate.yaml!
## Last Modification: 10:09:00 on 05-08-2024.
## Last Modification: 10:57:26 on 05-16-2024.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
Expand Down
150 changes: 123 additions & 27 deletions pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"

p "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
Expand Down Expand Up @@ -52,7 +54,7 @@ type OverrideFieldDefinition struct {
Schema *v1.JSONSchemaProps
Required bool
Replacement bool
PathSegments []string
PathSegments []pathSegment
Patches []p.PatchSetPatch
OriginalEnum []v1.JSON
Overwrites *t.OverrideFieldInClaim
Expand Down Expand Up @@ -732,63 +734,157 @@ func (g *XGenerator) generateBase(comp t.Composition) []byte {
}
}

//overwites := map[string]interface{}{}
for _, overwite := range g.OverrideFields {
base = applyOverrideFields(base, g.OverrideFields)

object, err := json.Marshal(base)
if err != nil {
fmt.Printf("unable to marshal base: %v\n", err)
}

return object
}

func applyOverrideFields(base map[string]interface{}, overrideFields []t.OverrideField) map[string]interface{} {
for _, overwite := range overrideFields {
if overwite.Value != nil {
//overwites[overwite.Path] = overwite.Value
path := splitPath(overwite.Path)
c := &base
var current interface{}
current = base
pathLength := len(path)

for i := 0; i < pathLength-1; i++ {
p := path[i]
//for _, p := range path {
if (*c)[p] == nil {
(*c)[p] = map[string]interface{}{}
segment := path[i]
property := path[i].path
if segment.pathType == "object" {
if current.(map[string]interface{})[property] == nil {
current.(map[string]interface{})[property] = map[string]interface{}{}
}

current = current.(map[string]interface{})[property].(map[string]interface{})
} else if segment.pathType == "array" {
if current.(map[string]interface{})[property] == nil {
current.(map[string]interface{})[property] = []map[string]interface{}{}
}

var b interface{}
b = current.(map[string]interface{})[property].([]map[string]interface{})
currentSize := len(b.([]map[string]interface{}))
wantedSize := segment.arrayPosition + 1
if currentSize < wantedSize {
sizeToGrow := wantedSize - currentSize
b = slices.Grow(b.([]map[string]interface{}), sizeToGrow)
b = b.([]map[string]interface{})[:cap(b.([]map[string]interface{}))]
b.([]map[string]interface{})[segment.arrayPosition] = map[string]interface{}{}
}
current.(map[string]interface{})[property] = b
current = b.([]map[string]interface{})[segment.arrayPosition]
}
b := (*c)[p].(map[string]interface{})
c = &b
}
(*c)[path[pathLength-1]] = overwite.Value
}
}
segment := path[pathLength-1]
if segment.pathType == "object" {
(current).(map[string]interface{})[path[pathLength-1].path] = overwite.Value
}
if segment.pathType == "array" {
property := path[pathLength-1].path

object, err := json.Marshal(base)
if err != nil {
fmt.Println("error")
if (current.(map[string]interface{}))[property] == nil {
(current.(map[string]interface{}))[property] = []interface{}{}
}

var b interface{}
b = (current.(map[string]interface{}))[property].([]interface{})
currentSize := len(b.([]interface{}))
wantedSize := segment.arrayPosition + 1
if currentSize < wantedSize {
sizeToGrow := wantedSize - currentSize
b = slices.Grow(b.([]interface{}), sizeToGrow)
b = b.([]interface{})[:cap(b.([]interface{}))]
(b.([]interface{})[segment.arrayPosition]) = overwite.Value
}
current.(map[string]interface{})[property] = b

}
}
}
return base
}

return object
type pathSegment struct {
path string
pathType string
arrayPosition int
}

func splitPath(path string) []string {
func splitPath(path string) []pathSegment {
inString := false
result := []string{}
result := []pathSegment{}
current := ""
escaped := false
for _, r := range path {
switch r {
case '"':
current += string(r)
inString = !inString
escaped = false

case '\\':
current += string(r)
escaped = true
case '.':
if current != "" {
if !inString && !escaped {
segment := pathSegment{
path: current,
pathType: "object",
}
result = append(result, segment)
current = ""
} else {
current += string(r)
}
}
case '[':
if !inString && !escaped {
result = append(result, current)
segment := pathSegment{
path: current,
pathType: "object",
}
result = append(result, segment)
current = ""
} else {
current += string(r)
}
case ']':
if !inString && !escaped {
lastSegemnt := result[len(result)-1]
arrayIndex, err := strconv.Atoi(current)
if err == nil {
lastSegemnt.pathType = "array"
lastSegemnt.arrayPosition = arrayIndex
result[len(result)-1] = lastSegemnt
} else {
segment := pathSegment{
path: current,
pathType: "object",
}
result = append(result, segment)
}
current = ""

} else {
current += string(r)
}
default:
current += string(r)
escaped = false

}
}
result = append(result, current)
if current != "" {
segment := pathSegment{
path: current,
pathType: "object",
}
result = append(result, segment)
}
return result
}

Expand Down Expand Up @@ -832,7 +928,7 @@ func (g *XGenerator) generateAdditonalPipelineStep(s t.PipelineStep) (*c.Pipelin

func (g *XGenerator) overwrittenFields(schema *v1.JSONSchemaProps, path string) error {
for _, o := range g.overrideFieldDefinitions {
if o.PathSegments[0] == path && !o.IgnoreInClaim {
if o.PathSegments[0].path == path && !o.IgnoreInClaim {
err := overwrittenFields(schema, path, o, 1)
if err != nil {
return err
Expand All @@ -845,7 +941,7 @@ func (g *XGenerator) overwrittenFields(schema *v1.JSONSchemaProps, path string)
func overwrittenFields(schema *v1.JSONSchemaProps, path string, definition *OverrideFieldDefinition, level int) error {
if len(definition.PathSegments)-1 > level {
if schema.Type == "object" {
pathSegment := definition.PathSegments[level]
pathSegment := definition.PathSegments[level].path
prop, ok := schema.Properties[pathSegment]
if !ok {
schema.Properties[pathSegment] = v1.JSONSchemaProps{
Expand All @@ -860,7 +956,7 @@ func overwrittenFields(schema *v1.JSONSchemaProps, path string, definition *Over
}
}
} else {
pathSegment := definition.PathSegments[level]
pathSegment := definition.PathSegments[level].path
if definition.Schema == nil {
return fmt.Errorf("schema must be given for new property: %s", definition.ClaimPath)
}
Expand Down
130 changes: 130 additions & 0 deletions pkg/generator/generattor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package generator

import (
"encoding/json"
"testing"

tp "github.com/crossplane-contrib/x-generation/pkg/types"
)

func Test_generateOverrideFields(t *testing.T) {
tests := []struct {
name string
base map[string]interface{}
overridePaths []tp.OverrideField
want string
}{
{
name: "Should render path",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider.certificateAuthorityConfiguration",
Value: "testA",
},
},
want: `{"spec": {"forProvider": {"certificateAuthorityConfiguration":"testA"}}}`,
},
{
name: "Should not override existing path",
base: map[string]interface{}{
"spec": map[string]interface{}{
"forProvider": map[string]interface{}{
"test": "testValue",
},
},
},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider.certificateAuthorityConfiguration",
Value: "testA",
},
},
want: `{"spec": {"forProvider": {"test":"testValue", "certificateAuthorityConfiguration":"testA"}}}`,
},
{
name: "Should create arrays of strings",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider.certificateAuthorityConfiguration[0]",
Value: "testA",
},
},
want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": ["testA"]}}}`,
},
{
name: "Should create arrays of arrays",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider.certificateAuthorityConfiguration[0].subproperty[0]",
Value: "testA",
},
},
want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": [{"subproperty":["testA"]}]}}}`,
},
{
name: "Should create arrays of arrays with properies",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider.certificateAuthorityConfiguration[0].subproperty[0].arg",
Value: "testA",
},
},
want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": [{"subproperty":[{"arg":"testA"}]}]}}}`,
},
{
name: "Should render path",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider[\"certificateAuthorityConfiguration\"]",
Value: "testA",
},
},
want: `{"spec": {"forProvider": {"certificateAuthorityConfiguration":"testA"}}}`,
},
{
name: "Should render path",
base: map[string]interface{}{},
overridePaths: []tp.OverrideField{
{
Path: "spec.forProvider[\"certificateAuthority.Configuration\"]",
Value: "testA",
},
},
want: `{"spec": {"forProvider": {"certificateAuthority.Configuration":"testA"}}}`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := applyOverrideFields(tt.base, tt.overridePaths)
object, err := json.Marshal(got)
if err != nil {
t.Errorf("error marshalling object %v", err)
}

// unmarshal and marshal wanted value to be independent of formatting and oder of properties
var wantobject interface{}
err = json.Unmarshal([]byte(tt.want), &wantobject)
if err != nil {
t.Errorf("want is not valid json %v", err)
}

wantbyte, err := json.Marshal(wantobject)

if err != nil {
t.Errorf("error marshalling object %v", err)
}

gotString := string(object)
wantString := string(wantbyte)
if gotString != wantString {
t.Errorf("Expaced object does not match got\n%v, want\n%v", gotString, wantString)
}
})
}
}

0 comments on commit e9f1f3b

Please sign in to comment.