Skip to content

Commit

Permalink
Allow to build a complex value for attribute mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
stefbenoist committed Feb 25, 2020
1 parent 74a9bc4 commit 7e96361
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 62 deletions.
162 changes: 139 additions & 23 deletions deployments/attribute_mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,176 @@ import (
"context"
"github.com/pkg/errors"
"github.com/ystia/yorc/v4/helper/collections"
"strconv"
)

// ResolveAttributeMapping allows to resolve an attribute mapping
// i.e update the corresponding attribute with the defined value
// parameters can be nested keys and/or attribute name in case of capability
func ResolveAttributeMapping(ctx context.Context, deploymentID, nodeName, instanceName, capabilityOrAttributeName string, attributeValue interface{}, parameters ...string) error {
// Check if node has an attribute with name
// Check if node has an attribute with corresponding name
attrs, err := GetNodeAttributesNames(ctx, deploymentID, nodeName)
if err != nil {
return err
}

if collections.ContainsString(attrs, capabilityOrAttributeName) {
// It's an attribute: get the complex value and update it
res, err := GetInstanceAttributeValue(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, parameters...)
return resolveInstanceAttributeMapping(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, attributeValue, parameters...)
}

// At this point, it's a capability name, so we ensure we have the attribute name in parameters
if len(parameters) < 1 {
return errors.Errorf("attribute name is missing in parameters for resolving attribute mapping for deploymentId:%q, node name:%q, instance name:%q, capability name:%q", deploymentID, nodeName, instanceName, capabilityOrAttributeName)
}

// It's a capability attribute: get the complex value and update it
return resolveCapabilityAttributeMapping(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, parameters[0], attributeValue, parameters[1:]...)
}

func resolveInstanceAttributeMapping(ctx context.Context, deploymentID, nodeName, instanceName, attributeName string, attributeValue interface{}, nestedKeys ...string) error {
// The simplest case
if len(nestedKeys) == 0 {
return SetInstanceAttributeComplex(ctx, deploymentID, nodeName, instanceName, attributeName, attributeValue)
}

var value interface{}
// Get existing or default value
res, err := GetInstanceAttributeValue(ctx, deploymentID, nodeName, instanceName, attributeName)
if err != nil {
return err
}

if res != nil && res.Value == nil {
value = res.Value
} else {
nodeType, err := GetNodeType(ctx, deploymentID, nodeName)
if err != nil {
return err
}
if res != nil && res.Value != nil {
updated, err := updateNestedValue(res.Value, attributeValue, parameters...)
if err != nil {
return err
}
return SetInstanceAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, updated)

attrDataType, err := GetTypeAttributeDataType(ctx, deploymentID, nodeType, attributeName)
if err != nil {
return err
}

// If it's a literal, just set the instance attribute
if len(parameters) == 0 {
return SetInstanceAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, attributeValue)
// Build value from scratch
value, err = buildValue(ctx, deploymentID, attrDataType, nestedKeys...)
if err != nil {
return err
}
return errors.Errorf("failed to set instance attribute %q with deploymentID:%q, nodeName:%q, instance:%q, nested keys:%v", capabilityOrAttributeName, deploymentID, nodeName, instanceName, parameters)
}

// At this point, it's a capability name, so we ensure we have the attribute name in parameters
if len(parameters) < 1 {
return errors.Errorf("an attribute name is missing in corresponding nested keys:%v", parameters)
updated, err := updateValue(value, attributeValue, nestedKeys...)
if err != nil {
return err
}
return SetInstanceAttributeComplex(ctx, deploymentID, nodeName, instanceName, attributeName, updated)
}

// It's a capability attribute: get the complex value and update it
res, err := GetInstanceCapabilityAttributeValue(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, parameters[0], parameters[1:]...)
func resolveCapabilityAttributeMapping(ctx context.Context, deploymentID, nodeName, instanceName, capabilityName, attributeName string, attributeValue interface{}, nestedKeys ...string) error {
// The simplest case
if len(nestedKeys) == 0 {
return SetInstanceCapabilityAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityName, attributeName, attributeValue)
}

var value interface{}
// Get existing or default value
res, err := GetInstanceCapabilityAttributeValue(ctx, deploymentID, nodeName, instanceName, capabilityName, attributeName)
if err != nil {
return err
}

if res != nil && res.Value != nil {
updated, err := updateNestedValue(res.Value, attributeValue, parameters[1:]...)
value = res.Value
} else {
// Build value from scratch
nodeType, err := GetNodeCapabilityType(ctx, deploymentID, nodeName, capabilityName)
if err != nil {
return err
}

attrDataType, err := GetTypeAttributeDataType(ctx, deploymentID, nodeType, attributeName)
if err != nil {
return err
}

value, err = buildValue(ctx, deploymentID, attrDataType, nestedKeys...)
if err != nil {
return err
}
return SetInstanceCapabilityAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, parameters[0], updated)
}

// If it's a literal, just set the instance attribute
if len(parameters) == 1 {
return SetInstanceCapabilityAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityOrAttributeName, parameters[0], attributeValue)
updated, err := updateValue(value, attributeValue, nestedKeys...)
if err != nil {
return err
}
return SetInstanceCapabilityAttributeComplex(ctx, deploymentID, nodeName, instanceName, capabilityName, attributeName, updated)
}

func buildValue(ctx context.Context, deploymentID, baseDataType string, nestedKeys ...string) (interface{}, error) {
var parent interface{}
dType := getDataTypeComplexType(baseDataType)
switch dType {
case "list":
parent = make([]interface{}, 0)
default:
parent = make(map[string]interface{}, 0)
}

tmp := parent
for i := 0; i < len(nestedKeys); i++ {
dataType, err := GetNestedDataType(ctx, deploymentID, baseDataType, nestedKeys[:i]...)
if err != nil {
return nil, err
}
var nestedValue interface{}
dType := getDataTypeComplexType(dataType)
switch dType {
case "list":
nestedValue = make([]interface{}, 0)
default:
nestedValue = make(map[string]interface{}, 0)
}

switch v := tmp.(type) {
case []interface{}:
tmp = append(v, nestedValue)
case map[string]interface{}:
v[nestedKeys[i]] = nestedValue
tmp = v[nestedKeys[i]]
}
}
return parent, nil
}

func updateValue(originalValue, nestedValueToUpdate interface{}, nestedKeys ...string) (interface{}, error) {
if len(nestedKeys) > 0 {
value := originalValue
for i := 0; i < len(nestedKeys); i++ {
nk := nestedKeys[i]
switch v := value.(type) {
case []interface{}:
ind, err := strconv.Atoi(nk)
// Check the slice index is valid
if err != nil {
return nil, errors.Errorf("%q is not a valid array index", nk)
}
if ind+1 > len(v) {
return nil, errors.Errorf("%q: index not found", ind)
}
if i == len(nestedKeys)-1 {
v[ind] = nestedValueToUpdate
}
value = v[ind]
case map[string]interface{}:
if i == len(nestedKeys)-1 {
v[nk] = nestedValueToUpdate
}
value = v[nk]
}
}
return originalValue, nil
}
return errors.Errorf("failed to set instance capability %q attribute %q with deploymentID:%q, nodeName:%q, instance:%q, nested keys:%v", capabilityOrAttributeName, parameters[0], deploymentID, nodeName, instanceName, parameters)
return nestedValueToUpdate, nil
}
48 changes: 43 additions & 5 deletions deployments/attribute_mappings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package deployments
import (
"context"
"github.com/stretchr/testify/require"
"reflect"
"strings"
"testing"
)
Expand All @@ -29,6 +30,10 @@ func testResolveAttributeMapping(t *testing.T) {
require.Nil(t, err)

t.Run("groupResolveAttributeMapping", func(t *testing.T) {
t.Run("testBuildNestedValue", func(t *testing.T) {
testBuildNestedValue(t, ctx, deploymentID)
})
t.Skip()
t.Run("testResolveAttributeMappingWithInstanceAttribute", func(t *testing.T) {
testResolveAttributeMappingWithInstanceAttribute(t, ctx, deploymentID)
})
Expand All @@ -42,9 +47,9 @@ func testResolveAttributeMappingWithInstanceAttribute(t *testing.T, ctx context.
// Then test node attributes
err := ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "lit", "myLiteral")
require.NoError(t, err)
err = ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "listAttr", []int{42, 43, 44})
err = ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "listAttr", 42, "0")
require.NoError(t, err)
err = ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "mapAttr", map[string]interface{}{"map1": "v1", "map2": "v2", "map3": "v3"})
err = ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "mapAttr", "v2", "map2")
require.NoError(t, err)
err = ResolveAttributeMapping(ctx, deploymentID, "VANode1", "0", "complexAttr", map[string]interface{}{"literal": "11", "literalDefault": "VANode1LitDef"})
require.NoError(t, err)
Expand Down Expand Up @@ -81,10 +86,8 @@ func testResolveAttributeMappingWithInstanceAttribute(t *testing.T, ctx context.
{"TestNodeAttrListIndex0", nodeAttrArgs{"VANode1", "0", "listAttr", []string{"0"}}, false, true, `42`},
{"TestNodeAttrListIndex1", nodeAttrArgs{"VANode1", "0", "listAttr", []string{"1"}}, false, true, `43`},
{"TestNodeAttrListIndex2", nodeAttrArgs{"VANode1", "0", "listAttr", []string{"2"}}, false, true, `44`},
{"TestNodeAttrMapAll", nodeAttrArgs{"VANode1", "0", "mapAttr", nil}, false, true, `{"map1":"v1","map2":"v2","map3":"v3"}`},
{"TestNodeAttrMapKey1", nodeAttrArgs{"VANode1", "0", "mapAttr", []string{"map1"}}, false, true, `v1`},
{"TestNodeAttrMapAll", nodeAttrArgs{"VANode1", "0", "mapAttr", nil}, false, true, `{"map2":"v2"}`},
{"TestNodeAttrMapKey2", nodeAttrArgs{"VANode1", "0", "mapAttr", []string{"map2"}}, false, true, `v2`},
{"TestNodeAttrMapKey3", nodeAttrArgs{"VANode1", "0", "mapAttr", []string{"map3"}}, false, true, `v3`},
{"TestAttrComplexTypeLit", nodeAttrArgs{"VANode1", "0", "complexAttr", []string{"literal"}}, false, true, `11`},
{"TestAttrComplexTypeLitDef", nodeAttrArgs{"VANode1", "0", "complexAttr", []string{"literalDefault"}}, false, true, `VANode1LitDef`},
{"TestAttrComplexTypeAll", nodeAttrArgs{"VANode1", "0", "complexAttr", nil}, false, true, `{"literal":"11","literalDefault":"VANode1LitDef"}`},
Expand Down Expand Up @@ -248,3 +251,38 @@ func testResolveAttributeMappingWithCapabilityAttribute(t *testing.T, ctx contex
})
}
}

func testBuildNestedValue(t *testing.T, ctx context.Context, deploymentID string) {
type args struct {
baseType string
nestedKeys []string
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{"NestedTypeSubComplexMapOnBaseType", args{"yorc.tests.datatypes.BaseType", []string{"nestedType", "mapofcomplex", "something", "literal"}}, map[string]interface{}{
"nestedType": map[string]interface{}{
"mapofcomplex": map[string]interface{}{
"something": map[string]interface{}{
"literal": map[string]interface{}{},
},
},
},
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := buildValue(ctx, deploymentID, tt.args.baseType, tt.args.nestedKeys...)
if (err != nil) != tt.wantErr {
t.Errorf("buildValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("buildValue() = %v, want %v", got, tt.want)
}
})
}
}
31 changes: 0 additions & 31 deletions deployments/commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,37 +60,6 @@ func getNestedValue(value interface{}, nestedKeys ...string) interface{} {
return value
}

func updateNestedValue(complexValue, nestedValue interface{}, nestedKeys ...string) (interface{}, error) {
if len(nestedKeys) > 0 {
value := complexValue
for i := 0; i < len(nestedKeys); i++ {
nk := nestedKeys[i]
switch v := value.(type) {
case []interface{}:
ind, err := strconv.Atoi(nk)
// Check the slice index is valid
if err != nil {
return nil, errors.Errorf("%q is not a valid array index", nk)
}
if ind+1 > len(v) {
return nil, errors.Errorf("%q: index not found", ind)
}
if i == len(nestedKeys)-1 {
v[ind] = nestedValue
}
value = v[ind]
case map[string]interface{}:
if i == len(nestedKeys)-1 {
v[nk] = nestedValue
}
value = v[nk]
}
}
return complexValue, nil
}
return nestedValue, nil
}

func readComplexVA(ctx context.Context, deploymentID string, value interface{}, baseDataType string, nestedKeys ...string) (interface{}, error) {
result := getNestedValue(value, nestedKeys...)
err := checkForDefaultValuesInComplexTypes(ctx, deploymentID, baseDataType, "", result, nestedKeys...)
Expand Down
6 changes: 3 additions & 3 deletions deployments/commons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ func TestUpdateNestedValue(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := updateNestedValue(tt.args.value, tt.args.nestedValue, tt.args.nestedKeys...)
got, err := updateValue(tt.args.value, tt.args.nestedValue, tt.args.nestedKeys...)
if (err != nil) != tt.wantErr {
t.Errorf("updateNestedValue() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("updateValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("updateNestedValue() = %v, want %v", got, tt.want)
t.Errorf("updateValue() = %v, want %v", got, tt.want)
}
})
}
Expand Down

0 comments on commit 7e96361

Please sign in to comment.