Skip to content

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7b6853e
failing TestUnmarshalNestedAllTypes
lantoli Apr 8, 2025
0e6d4b6
keepKeyCase doc
lantoli Apr 9, 2025
39c5113
marshal bool
lantoli Apr 9, 2025
da07fcb
rename jsonResp
lantoli Apr 10, 2025
91d0a8f
simple case test pass for case map[string]any
lantoli Apr 10, 2025
e664b47
rename model struct
lantoli Apr 10, 2025
e0509ca
skip attributes that are not in the model
lantoli Apr 10, 2025
f62a639
test case: model attr types in objects must match JSON types
lantoli Apr 10, 2025
879b454
pass test case: model attr types in objects must match JSON types
lantoli Apr 10, 2025
a8b0c45
add float and bool to modelTest
lantoli Apr 10, 2025
f8253dc
unkown and null attributes in model to get unmarshalled into
lantoli Apr 10, 2025
123d080
compare model instead of individual attrs
lantoli Apr 10, 2025
8460091
support null object as model source
lantoli Apr 11, 2025
a7a35ba
convertUnknownToNull
lantoli Apr 11, 2025
f6cca03
refactor getObjAttrsAndTypes and getNullAttr
lantoli Apr 11, 2025
1e021a4
setObjAttrModel
lantoli Apr 11, 2025
ee84181
test case: JSON maps not supported yet
lantoli Apr 11, 2025
8fde16d
more test cases in TestUnmarshalErrors
lantoli Apr 11, 2025
775d095
extract tom setAttrModel and setObjAttrTfModel
lantoli Apr 11, 2025
3aafe5a
getNullAttr for objects
lantoli Apr 11, 2025
46af9fe
Unmarshal recursive support for objects
lantoli Apr 11, 2025
39b9484
refactor setObjAttrModel
lantoli Apr 11, 2025
da33c25
support list and set of strings at root level
lantoli Apr 11, 2025
ef9e8c3
failing test for object list
lantoli Apr 11, 2025
5b87cd5
getTfAttr
lantoli Apr 11, 2025
5c6f453
recursive logic in getCollectionElements
lantoli Apr 11, 2025
1c5589d
fix use of newValue
lantoli Apr 11, 2025
78e8d05
set example
lantoli Apr 11, 2025
4d58b66
small doc change
lantoli Apr 11, 2025
c0631b0
Map comment
lantoli Apr 14, 2025
942e99b
split file for marshal and unmarshal
lantoli Apr 14, 2025
367dc60
don't convert unknown to null in this PR
lantoli Apr 14, 2025
3ca85cd
recursive lists and sets
lantoli Apr 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 11 additions & 66 deletions internal/common/autogeneration/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,6 @@ func Marshal(model any, isUpdate bool) ([]byte, error) {
return json.Marshal(objJSON)
}

// 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.
func Unmarshal(raw []byte, model any) error {
var objJSON map[string]any
if err := json.Unmarshal(raw, &objJSON); err != nil {
return err
}
return unmarshalAttrs(objJSON, model)
}

func marshalAttrs(valModel reflect.Value, isUpdate bool) (map[string]any, error) {
objJSON := make(map[string]any)
for i := 0; i < valModel.NumField(); i++ {
Expand All @@ -73,7 +63,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
}
Expand All @@ -83,13 +73,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:
Expand All @@ -110,7 +102,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
}
Expand All @@ -121,10 +113,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
}
Expand All @@ -133,57 +127,8 @@ func getMapAttr(elms map[string]attr.Value, keepKeyCase bool) (any, error) {
if keepKeyCase {
nameJSON = name
}
obj[nameJSON] = valChild
objJSON[nameJSON] = valChild
}
}
return obj, nil
}

func unmarshalAttrs(objJSON map[string]any, model any) error {
valModel := reflect.ValueOf(model)
if valModel.Kind() != reflect.Ptr {
panic("model must be pointer")
}
valModel = valModel.Elem()
if valModel.Kind() != reflect.Struct {
panic("model must be pointer to struct")
}
for attrNameJSON, attrObjJSON := range objJSON {
if err := unmarshalAttr(attrNameJSON, attrObjJSON, valModel); err != nil {
return err
}
}
return nil
}

func unmarshalAttr(attrNameJSON string, attrObjJSON any, valModel reflect.Value) error {
attrNameModel := xstrings.ToPascalCase(attrNameJSON)
fieldModel := valModel.FieldByName(attrNameModel)
if !fieldModel.CanSet() {
return nil // skip fields that cannot be set, are invalid or not found
}
switch v := attrObjJSON.(type) {
case string:
return setAttrModel(attrNameModel, fieldModel, types.StringValue(v))
case bool:
return setAttrModel(attrNameModel, fieldModel, types.BoolValue(v))
case float64: // number: try int or float
if setAttrModel(attrNameModel, fieldModel, types.Float64Value(v)) == nil {
return nil
}
return setAttrModel(attrNameModel, fieldModel, types.Int64Value(int64(v)))
case nil:
return nil // skip nil values, no need to set anything
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 {
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
return objJSON, nil
}
Loading