-
Notifications
You must be signed in to change notification settings - Fork 196
chore: Support nested attributes in conversion logic for generating Resource Model from API response #3261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: Support nested attributes in conversion logic for generating Resource Model from API response #3261
Changes from 30 commits
7b6853e
0e6d4b6
39c5113
da07fcb
91d0a8f
e664b47
e0509ca
f62a639
879b454
a8b0c45
f8253dc
123d080
8460091
a7a35ba
f6cca03
1e021a4
ee84181
8fde16d
775d095
3aafe5a
46af9fe
39b9484
da33c25
ef9e8c3
5b87cd5
5c6f453
1c5589d
78e8d05
4d58b66
c0631b0
942e99b
367dc60
3ca85cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package autogeneration | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"reflect" | ||
|
@@ -38,7 +39,10 @@ func Marshal(model any, isUpdate bool) ([]byte, error) { | |
} | ||
|
||
// Unmarshal gets a JSON (e.g. from an Atlas response) and unmarshals it into a Terraform model. | ||
// It supports the following Terraform model types: String, Bool, Int64, Float64. | ||
// It supports the following Terraform model types: String, Bool, Int64, Float64, Object, List, Set. | ||
// Map is not supported yet, will be done in CLOUDP-312797. | ||
// Attributes that are in JSON but not in the model are ignored, no error is returned. | ||
// Object attributes that are unknown are converted to null as all values must be known in the response state. | ||
func Unmarshal(raw []byte, model any) error { | ||
var objJSON map[string]any | ||
if err := json.Unmarshal(raw, &objJSON); err != nil { | ||
|
@@ -73,7 +77,7 @@ func marshalAttr(attrNameModel string, attrValModel reflect.Value, objJSON map[s | |
if !ok { | ||
panic("marshal expects only Terraform types in the model") | ||
} | ||
val, err := getAttr(obj) | ||
val, err := getModelAttr(obj) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -83,13 +87,15 @@ func marshalAttr(attrNameModel string, attrValModel reflect.Value, objJSON map[s | |
return nil | ||
} | ||
|
||
func getAttr(val attr.Value) (any, error) { | ||
func getModelAttr(val attr.Value) (any, error) { | ||
if val.IsNull() || val.IsUnknown() { | ||
return nil, nil // skip null or unknown values | ||
} | ||
switch v := val.(type) { | ||
case types.String: | ||
return v.ValueString(), nil | ||
case types.Bool: | ||
return v.ValueBool(), nil | ||
case types.Int64: | ||
return v.ValueInt64(), nil | ||
case types.Float64: | ||
|
@@ -110,7 +116,7 @@ func getAttr(val attr.Value) (any, error) { | |
func getListAttr(elms []attr.Value) (any, error) { | ||
arr := make([]any, 0) | ||
for _, attr := range elms { | ||
valChild, err := getAttr(attr) | ||
valChild, err := getModelAttr(attr) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -121,10 +127,12 @@ func getListAttr(elms []attr.Value) (any, error) { | |
return arr, nil | ||
} | ||
|
||
// getMapAttr gets a map of attributes and returns a map of JSON attributes. | ||
// keepKeyCase is used for types.Map to keep key case. However, we want to use JSON key case for types.Object | ||
func getMapAttr(elms map[string]attr.Value, keepKeyCase bool) (any, error) { | ||
obj := make(map[string]any) | ||
objJSON := make(map[string]any) | ||
for name, attr := range elms { | ||
valChild, err := getAttr(attr) | ||
valChild, err := getModelAttr(attr) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -133,10 +141,10 @@ func getMapAttr(elms map[string]attr.Value, keepKeyCase bool) (any, error) { | |
if keepKeyCase { | ||
nameJSON = name | ||
} | ||
obj[nameJSON] = valChild | ||
objJSON[nameJSON] = valChild | ||
} | ||
} | ||
return obj, nil | ||
return objJSON, nil | ||
} | ||
|
||
func unmarshalAttrs(objJSON map[string]any, model any) error { | ||
|
@@ -153,6 +161,7 @@ func unmarshalAttrs(objJSON map[string]any, model any) error { | |
return err | ||
} | ||
} | ||
convertUnknownToNull(valModel) | ||
return nil | ||
} | ||
|
||
|
@@ -164,26 +173,211 @@ func unmarshalAttr(attrNameJSON string, attrObjJSON any, valModel reflect.Value) | |
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth it to log a debug message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure, in general I prefer not to log debug messages, I don't know if this case is so important to be logged |
||
switch v := attrObjJSON.(type) { | ||
case string: | ||
return setAttrModel(attrNameModel, fieldModel, types.StringValue(v)) | ||
return setAttrTfModel(attrNameModel, fieldModel, types.StringValue(v)) | ||
case bool: | ||
return setAttrModel(attrNameModel, fieldModel, types.BoolValue(v)) | ||
return setAttrTfModel(attrNameModel, fieldModel, types.BoolValue(v)) | ||
case float64: // number: try int or float | ||
if setAttrModel(attrNameModel, fieldModel, types.Float64Value(v)) == nil { | ||
if setAttrTfModel(attrNameModel, fieldModel, types.Float64Value(v)) == nil { | ||
return nil | ||
} | ||
return setAttrModel(attrNameModel, fieldModel, types.Int64Value(int64(v))) | ||
return setAttrTfModel(attrNameModel, fieldModel, types.Int64Value(int64(v))) | ||
case nil: | ||
return nil // skip nil values, no need to set anything | ||
case map[string]any: | ||
obj, ok := fieldModel.Interface().(types.Object) | ||
if !ok { | ||
return fmt.Errorf("unmarshal expects object for field %s", attrNameJSON) | ||
} | ||
objNew, err := setObjAttrModel(obj, v) | ||
if err != nil { | ||
return err | ||
} | ||
return setAttrTfModel(attrNameModel, fieldModel, objNew) | ||
case []any: | ||
switch collection := fieldModel.Interface().(type) { | ||
case types.List: | ||
list, err := setListAttrModel(collection, v) | ||
if err != nil { | ||
return err | ||
} | ||
return setAttrTfModel(attrNameModel, fieldModel, list) | ||
case types.Set: | ||
set, err := setSetAttrModel(collection, v) | ||
if err != nil { | ||
return err | ||
} | ||
return setAttrTfModel(attrNameModel, fieldModel, set) | ||
} | ||
return fmt.Errorf("unmarshal expects array for field %s", attrNameJSON) | ||
default: | ||
return fmt.Errorf("unmarshal not supported yet for type %T for field %s", v, attrNameJSON) | ||
} | ||
} | ||
|
||
func setAttrModel(name string, field reflect.Value, val attr.Value) error { | ||
func setAttrTfModel(name string, field reflect.Value, val attr.Value) error { | ||
obj := reflect.ValueOf(val) | ||
if !field.Type().AssignableTo(obj.Type()) { | ||
return fmt.Errorf("unmarshal can't assign value to model field %s", name) | ||
} | ||
field.Set(obj) | ||
return nil | ||
} | ||
|
||
func setMapAttrModel(name string, value any, mapAttrs map[string]attr.Value, mapTypes map[string]attr.Type) error { | ||
nameChildTf := xstrings.ToSnakeCase(name) | ||
valueType, found := mapTypes[nameChildTf] | ||
if !found { | ||
return nil // skip attributes that are not in the model | ||
} | ||
newValue, err := getTfAttr(value, valueType, mapAttrs[nameChildTf]) | ||
if err != nil { | ||
return err | ||
} | ||
if newValue != nil { | ||
mapAttrs[nameChildTf] = newValue | ||
} | ||
return nil | ||
} | ||
|
||
func getTfAttr(value any, valueType attr.Type, oldValue attr.Value) (attr.Value, error) { | ||
switch v := value.(type) { | ||
case string: | ||
if valueType == types.StringType { | ||
return types.StringValue(v), nil | ||
} | ||
return nil, fmt.Errorf("unmarshal gets incorrect string for value: %v", v) | ||
case bool: | ||
if valueType == types.BoolType { | ||
return types.BoolValue(v), nil | ||
} | ||
return nil, fmt.Errorf("unmarshal gets incorrect bool for value: %v", v) | ||
case float64: | ||
switch valueType { | ||
case types.Int64Type: | ||
return types.Int64Value(int64(v)), nil | ||
case types.Float64Type: | ||
return types.Float64Value(v), nil | ||
} | ||
return nil, fmt.Errorf("unmarshal gets incorrect number for value: %v", v) | ||
case map[string]any: | ||
obj, ok := oldValue.(types.Object) | ||
if !ok { | ||
return nil, fmt.Errorf("unmarshal gets incorrect object for value: %v", v) | ||
} | ||
objNew, err := setObjAttrModel(obj, v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return objNew, nil | ||
case nil: | ||
return nil, nil // skip nil values, no need to set anything | ||
} | ||
return nil, fmt.Errorf("unmarshal not supported yet for type %T", value) | ||
} | ||
|
||
func setObjAttrModel(obj types.Object, objJSON map[string]any) (attr.Value, error) { | ||
AgustinBettati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
mapAttrs, mapTypes, err := getObjAttrsAndTypes(obj) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for nameChild, valueChild := range objJSON { | ||
if err := setMapAttrModel(nameChild, valueChild, mapAttrs, mapTypes); err != nil { | ||
return nil, err | ||
} | ||
} | ||
objNew, diags := types.ObjectValue(obj.AttributeTypes(context.Background()), mapAttrs) | ||
if diags.HasError() { | ||
return nil, fmt.Errorf("unmarshal failed to convert map to object: %v", diags) | ||
} | ||
return objNew, nil | ||
} | ||
|
||
func setListAttrModel(list types.List, arrayJSON []any) (attr.Value, error) { | ||
elmType := list.ElementType(context.Background()) | ||
elms, err := getCollectionElements(arrayJSON, elmType) | ||
if err != nil { | ||
return nil, err | ||
} | ||
listNew, diags := types.ListValue(elmType, elms) | ||
if diags.HasError() { | ||
return nil, fmt.Errorf("unmarshal failed to convert list to object: %v", diags) | ||
} | ||
return listNew, nil | ||
} | ||
|
||
func setSetAttrModel(set types.Set, arrayJSON []any) (attr.Value, error) { | ||
elmType := set.ElementType(context.Background()) | ||
AgustinBettati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
elms, err := getCollectionElements(arrayJSON, elmType) | ||
if err != nil { | ||
return nil, err | ||
} | ||
setNew, diags := types.SetValue(elmType, elms) | ||
if diags.HasError() { | ||
return nil, fmt.Errorf("unmarshal failed to convert set to object: %v", diags) | ||
} | ||
return setNew, nil | ||
} | ||
|
||
func getCollectionElements(arrayJSON []any, valueType attr.Type) ([]attr.Value, error) { | ||
elms := make([]attr.Value, len(arrayJSON)) | ||
nullVal, err := getNullAttr(valueType) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for i, item := range arrayJSON { | ||
newValue, err := getTfAttr(item, valueType, nullVal) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if newValue != nil { | ||
elms[i] = newValue | ||
} | ||
} | ||
return elms, nil | ||
} | ||
|
||
func getObjAttrsAndTypes(obj types.Object) (mapAttrs map[string]attr.Value, mapTypes map[string]attr.Type, err error) { | ||
// mapTypes has all attributes, mapAttrs might not have them, e.g. in null or unknown objects | ||
mapAttrs = obj.Attributes() | ||
mapTypes = obj.AttributeTypes(context.Background()) | ||
for attrName, attrType := range mapTypes { | ||
if _, found := mapAttrs[attrName]; found { | ||
continue // skip attributes that are already set | ||
} | ||
nullVal, err := getNullAttr(attrType) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
mapAttrs[attrName] = nullVal | ||
} | ||
return mapAttrs, mapTypes, nil | ||
} | ||
|
||
func getNullAttr(attrType attr.Type) (attr.Value, error) { | ||
switch attrType { | ||
case types.StringType: | ||
return types.StringNull(), nil | ||
case types.BoolType: | ||
return types.BoolNull(), nil | ||
case types.Int64Type: | ||
return types.Int64Null(), nil | ||
case types.Float64Type: | ||
return types.Float64Null(), nil | ||
default: | ||
if objType, ok := attrType.(types.ObjectType); ok { | ||
return types.ObjectNull(objType.AttributeTypes()), nil | ||
} | ||
return nil, fmt.Errorf("unmarshal to get null value not supported yet for type %T", attrType) | ||
} | ||
} | ||
|
||
func convertUnknownToNull(valModel reflect.Value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this logic be recursive? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unknown to null logic moved to another PR |
||
for i := 0; i < valModel.NumField(); i++ { | ||
field := valModel.Field(i) | ||
if obj, ok := field.Interface().(types.Object); ok { | ||
if obj.IsUnknown() && field.CanSet() { | ||
field.Set(reflect.ValueOf(types.ObjectNull(obj.AttributeTypes(context.Background())))) | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.