Skip to content

Commit 203b3ba

Browse files
fix(api): handle mismatched dynamic array types in state and plan during serialization
1 parent 98682f0 commit 203b3ba

19 files changed

+776
-162
lines changed

internal/apiform/encoder.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,21 @@ func (e *encoder) terraformUnwrappedDynamicEncoder(unwrap terraformUnwrappingFun
172172
}
173173

174174
func (e *encoder) newTerraformTypeEncoder(t reflect.Type) encoderFunc {
175-
176175
if t == reflect.TypeOf(basetypes.BoolValue{}) {
177176
return e.terraformUnwrappedEncoder(reflect.TypeOf(true), func(value attr.Value) (any, diag.Diagnostics) {
178-
return value.(basetypes.BoolValue).ValueBool(), diag.Diagnostics{}
177+
return apijson.UnwrapTerraformAttrValue(value)
179178
})
180179
} else if t == reflect.TypeOf(basetypes.Int64Value{}) {
181180
return e.terraformUnwrappedEncoder(reflect.TypeOf(int64(0)), func(value attr.Value) (any, diag.Diagnostics) {
182-
return value.(basetypes.Int64Value).ValueInt64(), diag.Diagnostics{}
181+
return apijson.UnwrapTerraformAttrValue(value)
183182
})
184183
} else if t == reflect.TypeOf(basetypes.Float64Value{}) {
185184
return e.terraformUnwrappedEncoder(reflect.TypeOf(float64(0)), func(value attr.Value) (any, diag.Diagnostics) {
186-
return value.(basetypes.Float64Value).ValueFloat64(), diag.Diagnostics{}
185+
return apijson.UnwrapTerraformAttrValue(value)
187186
})
188187
} else if t == reflect.TypeOf(basetypes.StringValue{}) {
189188
return e.terraformUnwrappedEncoder(reflect.TypeOf(""), func(value attr.Value) (any, diag.Diagnostics) {
190-
return value.(basetypes.StringValue).ValueString(), diag.Diagnostics{}
189+
return apijson.UnwrapTerraformAttrValue(value)
191190
})
192191
} else if t == reflect.TypeOf(timetypes.RFC3339{}) {
193192
return e.terraformUnwrappedEncoder(reflect.TypeOf(time.Time{}), func(value attr.Value) (any, diag.Diagnostics) {
@@ -209,9 +208,11 @@ func (e *encoder) newTerraformTypeEncoder(t reflect.Type) encoderFunc {
209208
return encodePartAsJSON
210209
} else if t == reflect.TypeOf(basetypes.ObjectValue{}) {
211210
return encodePartAsJSON
212-
} else if t == reflect.TypeOf(basetypes.DynamicValue{}) {
211+
} else if t.Implements(reflect.TypeOf((*basetypes.DynamicValuable)(nil)).Elem()) {
213212
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
214-
return value.(basetypes.DynamicValue).UnderlyingValue(), diag.Diagnostics{}
213+
ctx := context.TODO()
214+
val, d := value.(basetypes.DynamicValuable).ToDynamicValue(ctx)
215+
return val.UnderlyingValue(), d
215216
})
216217
} else if t.Implements(reflect.TypeOf((*customfield.NestedObjectLike)(nil)).Elem()) {
217218
return encodePartAsJSON

internal/apiform/form_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type TerraformTypes struct {
5858
P customfield.NestedObjectMap[NestedTerraformType] `tfsdk:"p" json:"p"`
5959
Q customfield.NestedObjectSet[NestedTerraformType] `tfsdk:"q" json:"q"`
6060
R jsontypes.Normalized `tfsdk:"r" json:"r"`
61+
S customfield.NormalizedDynamicValue `tfsdk:"s" json:"s"`
6162
}
6263

6364
type NestedTerraformType struct {
@@ -314,6 +315,11 @@ Content-Disposition: form-data; name="r"
314315
Content-Type: application/json
315316
316317
{"hello": "world"}
318+
--xxx
319+
Content-Disposition: form-data; name="s"
320+
Content-Type: application/json
321+
322+
{"dynamic_hello":"dynamic_world"}
317323
--xxx--
318324
`,
319325
TerraformTypes{
@@ -358,6 +364,7 @@ Content-Type: application/json
358364
},
359365
}),
360366
R: jsontypes.NewNormalizedValue(`{"hello": "world"}`),
367+
S: customfield.RawNormalizedDynamicValue(types.DynamicValue(types.ObjectValueMust(map[string]attr.Type{"dynamic_hello": basetypes.StringType{}}, map[string]attr.Value{"dynamic_hello": basetypes.NewStringValue("dynamic_world")}))),
361368
},
362369
},
363370

internal/apijson/decoder.go

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ func (d *decoderBuilder) newTerraformTypeDecoder(t reflect.Type) decoderFunc {
608608
eleType := value.Interface().(basetypes.SetValue).ElementType(ctx)
609609
switch node.Type {
610610
case gjson.Null:
611-
value.Set(reflect.ValueOf(types.ListNull(eleType)))
611+
value.Set(reflect.ValueOf(types.SetNull(eleType)))
612612
return nil
613613
case gjson.JSON:
614614
elementType, attributes, err := d.parseArrayOfValues(node)
@@ -636,7 +636,7 @@ func (d *decoderBuilder) newTerraformTypeDecoder(t reflect.Type) decoderFunc {
636636
switch node.Type {
637637
case gjson.Null:
638638
if b == Always {
639-
value.Set(reflect.ValueOf(types.ListNull(eleType)))
639+
value.Set(reflect.ValueOf(types.MapNull(eleType)))
640640
}
641641
return nil
642642
case gjson.JSON:
@@ -868,19 +868,28 @@ func (d *decoderBuilder) newTerraformTypeDecoder(t reflect.Type) decoderFunc {
868868
}
869869
}
870870

871-
if (t == reflect.TypeOf(basetypes.DynamicValue{})) {
871+
if t.Implements(reflect.TypeOf((*basetypes.DynamicValuable)(nil)).Elem()) {
872+
bsValue := t == reflect.TypeOf(basetypes.DynamicValue{})
873+
872874
return func(node gjson.Result, value reflect.Value, state *decoderState) error {
873875
if !shouldUpdatePrimitive(value, b) {
874876
return nil
875877
}
876-
dynamic := value.Interface().(basetypes.DynamicValue)
878+
dynValuable := value.Interface().(basetypes.DynamicValuable)
879+
dynamic, _ := dynValuable.ToDynamicValue(ctx)
880+
877881
underlying := dynamic.UnderlyingValue()
878882
if !shouldUpdatePrimitive(reflect.ValueOf(underlying), b) {
879883
return nil
880884
}
881885
if node.Type == gjson.Null && underlying == nil {
882886
// special case of null means we don't have an underlying type
883-
value.Set(reflect.ValueOf(types.DynamicNull()))
887+
val := types.DynamicNull()
888+
if bsValue {
889+
value.Set(reflect.ValueOf(val))
890+
} else {
891+
value.Set(reflect.ValueOf(customfield.RawNormalizedDynamicValue(val)))
892+
}
884893
return nil
885894
}
886895
if underlying != nil {
@@ -892,15 +901,26 @@ func (d *decoderBuilder) newTerraformTypeDecoder(t reflect.Type) decoderFunc {
892901
if err != nil {
893902
return err
894903
}
895-
value.Set(reflect.ValueOf(types.DynamicValue(underlyingValue.Interface().(attr.Value))))
904+
905+
val := types.DynamicValue(underlyingValue.Interface().(attr.Value))
906+
if bsValue {
907+
value.Set(reflect.ValueOf(val))
908+
} else {
909+
value.Set(reflect.ValueOf(customfield.RawNormalizedDynamicValue(val)))
910+
}
896911
} else {
897912
// just decode from the json itself
898913
attr, err := d.inferTerraformAttrFromValue(node)
899914
if err != nil {
900915
return err
901916
}
902917

903-
value.Set(reflect.ValueOf(types.DynamicValue(attr)))
918+
val := types.DynamicValue(attr)
919+
if bsValue {
920+
value.Set(reflect.ValueOf(val))
921+
} else {
922+
value.Set(reflect.ValueOf(customfield.RawNormalizedDynamicValue(val)))
923+
}
904924
}
905925
return nil
906926
}
@@ -1326,15 +1346,22 @@ func (d *decoderBuilder) inferTerraformAttrFromValue(node gjson.Result) (attr.Va
13261346
return types.StringValue(node.String()), nil
13271347
case gjson.JSON:
13281348
if node.IsArray() {
1329-
elementType, attributes, err := d.parseArrayOfValues(node)
1330-
if err != nil {
1331-
return nil, err
1332-
}
1333-
newVal, diags := basetypes.NewListValue(elementType, attributes)
1334-
if diags.HasError() {
1335-
return nil, errorFromDiagnostics(diags)
1349+
isHomogeneous, elementType, attributes, elementTypes := d.analyzeArrayTypes(node)
1350+
if isHomogeneous {
1351+
// Create ListValue for homogeneous arrays
1352+
newVal, diags := basetypes.NewListValue(elementType, attributes)
1353+
if diags.HasError() {
1354+
return nil, errorFromDiagnostics(diags)
1355+
}
1356+
return newVal, nil
1357+
} else {
1358+
// Create TupleValue for heterogeneous arrays
1359+
newVal, diags := basetypes.NewTupleValue(elementTypes, attributes)
1360+
if diags.HasError() {
1361+
return nil, errorFromDiagnostics(diags)
1362+
}
1363+
return newVal, nil
13361364
}
1337-
return newVal, nil
13381365
} else if node.IsObject() {
13391366
attributes := map[string]attr.Value{}
13401367
attributeTypes := map[string]attr.Type{}
@@ -1363,6 +1390,37 @@ func (d *decoderBuilder) inferTerraformAttrFromValue(node gjson.Result) (attr.Va
13631390
return nil, fmt.Errorf("apijson: cannot infer terraform attribute from value")
13641391
}
13651392

1393+
// analyzeArrayTypes analyzes a JSON array and determines if it's homogeneous or heterogeneous
1394+
// Returns: (isHomogeneous, elementType, attributes, elementTypes)
1395+
func (d *decoderBuilder) analyzeArrayTypes(node gjson.Result) (bool, attr.Type, []attr.Value, []attr.Type) {
1396+
ctx := context.TODO()
1397+
attributes := []attr.Value{}
1398+
elementTypes := []attr.Type{}
1399+
var firstElementType attr.Type
1400+
isHomogeneous := true
1401+
1402+
node.ForEach(func(_, value gjson.Result) bool {
1403+
val, err := d.inferTerraformAttrFromValue(value)
1404+
if err != nil {
1405+
return false
1406+
}
1407+
1408+
valType := val.Type(ctx)
1409+
attributes = append(attributes, val)
1410+
elementTypes = append(elementTypes, valType)
1411+
1412+
if firstElementType == nil {
1413+
firstElementType = valType
1414+
} else if !firstElementType.Equal(valType) {
1415+
isHomogeneous = false
1416+
}
1417+
1418+
return true
1419+
})
1420+
1421+
return isHomogeneous, firstElementType, attributes, elementTypes
1422+
}
1423+
13661424
func (d *decoderBuilder) parseArrayOfValues(node gjson.Result) (attr.Type, []attr.Value, error) {
13671425
ctx := context.TODO()
13681426
loopErr := error(nil)

internal/apijson/encoder.go

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -378,56 +378,86 @@ func (e encoder) handleNullAndUndefined(innerFunc func(attr.Value, attr.Value) (
378378
}
379379
}
380380

381+
// safeCollectionElements safely extracts elements from List, Tuple, or Set values
382+
// This prevents panics when plan and state have different collection types
383+
func UnwrapTerraformAttrValue(value attr.Value) (out any, diags diag.Diagnostics) {
384+
switch v := value.(type) {
385+
case basetypes.BoolValue:
386+
return v.ValueBool(), nil
387+
case basetypes.Int32Value:
388+
return v.ValueInt32(), nil
389+
case basetypes.Int64Value:
390+
return v.ValueInt64(), nil
391+
case basetypes.Float32Value:
392+
return v.ValueFloat32(), nil
393+
case basetypes.Float64Value:
394+
return v.ValueFloat64(), nil
395+
case basetypes.NumberValue:
396+
return v.ValueBigFloat(), nil
397+
case basetypes.StringValue:
398+
return v.ValueString(), nil
399+
case basetypes.TupleValue:
400+
return v.Elements(), nil
401+
case basetypes.ListValue:
402+
return v.Elements(), nil
403+
case basetypes.SetValue:
404+
return v.Elements(), nil
405+
case basetypes.MapValue:
406+
return v.Elements(), nil
407+
case basetypes.ObjectValue:
408+
return v.Attributes(), nil
409+
default:
410+
diags.AddError("unknown type received at terraform encoder", fmt.Sprintf("received: %s", value.Type(context.TODO())))
411+
return nil, diags
412+
}
413+
}
414+
381415
func (e encoder) newTerraformTypeEncoder(t reflect.Type) encoderFunc {
382416

383417
if t == reflect.TypeOf(basetypes.BoolValue{}) {
384418
return e.terraformUnwrappedEncoder(reflect.TypeOf(true), func(value attr.Value) (any, diag.Diagnostics) {
385-
return value.(basetypes.BoolValue).ValueBool(), diag.Diagnostics{}
419+
return UnwrapTerraformAttrValue(value)
386420
})
387421
} else if t == reflect.TypeOf(basetypes.Int64Value{}) {
388422
return e.terraformUnwrappedEncoder(reflect.TypeOf(int64(0)), func(value attr.Value) (any, diag.Diagnostics) {
389-
return value.(basetypes.Int64Value).ValueInt64(), diag.Diagnostics{}
423+
return UnwrapTerraformAttrValue(value)
390424
})
391425
} else if t == reflect.TypeOf(basetypes.Float64Value{}) {
392426
return e.terraformUnwrappedEncoder(reflect.TypeOf(float64(0)), func(value attr.Value) (any, diag.Diagnostics) {
393-
return value.(basetypes.Float64Value).ValueFloat64(), diag.Diagnostics{}
427+
return UnwrapTerraformAttrValue(value)
394428
})
395429
} else if t == reflect.TypeOf(basetypes.NumberValue{}) {
396430
return e.terraformUnwrappedEncoder(reflect.TypeOf(big.NewFloat(0)), func(value attr.Value) (any, diag.Diagnostics) {
397-
return value.(basetypes.NumberValue).ValueBigFloat(), diag.Diagnostics{}
431+
return UnwrapTerraformAttrValue(value)
398432
})
399433
} else if t == reflect.TypeOf(basetypes.StringValue{}) {
400434
return e.terraformUnwrappedEncoder(reflect.TypeOf(""), func(value attr.Value) (any, diag.Diagnostics) {
401-
return value.(basetypes.StringValue).ValueString(), diag.Diagnostics{}
435+
return UnwrapTerraformAttrValue(value)
402436
})
403437
} else if t == reflect.TypeOf(timetypes.RFC3339{}) {
404438
return e.terraformUnwrappedEncoder(reflect.TypeOf(time.Time{}), func(value attr.Value) (any, diag.Diagnostics) {
405439
return value.(timetypes.RFC3339).ValueRFC3339Time()
406440
})
407441
} else if t == reflect.TypeOf(basetypes.ListValue{}) {
408-
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
409-
return value.(basetypes.ListValue).Elements(), diag.Diagnostics{}
410-
})
442+
return e.terraformUnwrappedDynamicEncoder(UnwrapTerraformAttrValue)
411443
} else if t == reflect.TypeOf(basetypes.TupleValue{}) {
412-
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
413-
return value.(basetypes.TupleValue).Elements(), diag.Diagnostics{}
414-
})
444+
return e.terraformUnwrappedDynamicEncoder(UnwrapTerraformAttrValue)
415445
} else if t == reflect.TypeOf(basetypes.SetValue{}) {
416-
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
417-
return value.(basetypes.SetValue).Elements(), diag.Diagnostics{}
418-
})
446+
return e.terraformUnwrappedDynamicEncoder(UnwrapTerraformAttrValue)
419447
} else if t == reflect.TypeOf(basetypes.MapValue{}) {
420448
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
421-
return value.(basetypes.MapValue).Elements(), diag.Diagnostics{}
449+
return UnwrapTerraformAttrValue(value)
422450
})
423451
} else if t == reflect.TypeOf(basetypes.ObjectValue{}) {
424452
return e.terraformUnwrappedDynamicEncoder(func(value attr.Value) (any, diag.Diagnostics) {
425-
return value.(basetypes.ObjectValue).Attributes(), diag.Diagnostics{}
453+
return UnwrapTerraformAttrValue(value)
426454
})
427-
} else if t == reflect.TypeOf(basetypes.DynamicValue{}) {
455+
} else if t.Implements(reflect.TypeOf((*basetypes.DynamicValuable)(nil)).Elem()) {
428456
return func(plan reflect.Value, state reflect.Value) ([]byte, error) {
429-
tfPlan := plan.Interface().(basetypes.DynamicValue)
430-
tfState := state.Interface().(basetypes.DynamicValue)
457+
ctx := context.TODO()
458+
tfPlan, _ := plan.Interface().(basetypes.DynamicValuable).ToDynamicValue(ctx)
459+
tfState, _ := state.Interface().(basetypes.DynamicValuable).ToDynamicValue(ctx)
460+
431461
planNull := tfPlan.IsNull() || tfPlan.IsUnderlyingValueNull()
432462
stateMissing := tfState.IsNull() || tfState.IsUnderlyingValueNull() || tfState.IsUnderlyingValueNull() || tfState.IsUnderlyingValueUnknown()
433463
if stateMissing && planNull {

0 commit comments

Comments
 (0)