internal/services/dynamic/dynamic.go (431 lines of code) (raw):
package dynamic
import (
"encoding/json"
"fmt"
"math/big"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
type UnknownValueHandler func(val attr.Value) ([]byte, error)
func ToJSON(d types.Dynamic) ([]byte, error) {
return attrValueToJSON(d.UnderlyingValue(), nil)
}
func ToJSONWithUnknownValueHandler(d types.Dynamic, handler UnknownValueHandler) ([]byte, error) {
return attrValueToJSON(d.UnderlyingValue(), handler)
}
func attrListToJSON(in []attr.Value, handler UnknownValueHandler) ([]json.RawMessage, error) {
l := make([]json.RawMessage, 0)
for _, v := range in {
vv, err := attrValueToJSON(v, handler)
if err != nil {
return nil, err
}
l = append(l, json.RawMessage(vv))
}
return l, nil
}
func attrMapToJSON(in map[string]attr.Value, handler UnknownValueHandler) (map[string]json.RawMessage, error) {
m := map[string]json.RawMessage{}
for k, v := range in {
vv, err := attrValueToJSON(v, handler)
if err != nil {
return nil, err
}
m[k] = json.RawMessage(vv)
}
return m, nil
}
func attrValueToJSON(val attr.Value, handler UnknownValueHandler) ([]byte, error) {
if val == nil || val.IsNull() {
return json.Marshal(nil)
}
if val.IsUnknown() {
if handler != nil {
return handler(val)
}
}
switch value := val.(type) {
case types.Bool:
return json.Marshal(value.ValueBool())
case types.String:
return json.Marshal(value.ValueString())
case types.Int64:
return json.Marshal(value.ValueInt64())
case types.Float64:
return json.Marshal(value.ValueFloat64())
case types.Number:
v, _ := value.ValueBigFloat().Float64()
return json.Marshal(v)
case types.List:
l, err := attrListToJSON(value.Elements(), handler)
if err != nil {
return nil, err
}
return json.Marshal(l)
case types.Set:
l, err := attrListToJSON(value.Elements(), handler)
if err != nil {
return nil, err
}
return json.Marshal(l)
case types.Tuple:
l, err := attrListToJSON(value.Elements(), handler)
if err != nil {
return nil, err
}
return json.Marshal(l)
case types.Map:
m, err := attrMapToJSON(value.Elements(), handler)
if err != nil {
return nil, err
}
return json.Marshal(m)
case types.Object:
m, err := attrMapToJSON(value.Attributes(), handler)
if err != nil {
return nil, err
}
return json.Marshal(m)
default:
return nil, fmt.Errorf("Unhandled type: %T", value)
}
}
func FromJSON(b []byte, typ attr.Type) (types.Dynamic, error) {
v, err := attrValueFromJSON(b, typ)
if err != nil {
return types.Dynamic{}, err
}
return types.DynamicValue(v), nil
}
func attrListFromJSON(b []byte, etyp attr.Type) ([]attr.Value, error) {
var l []json.RawMessage
if err := json.Unmarshal(b, &l); err != nil {
return nil, err
}
vals := make([]attr.Value, 0)
for _, b := range l {
val, err := attrValueFromJSON(b, etyp)
if err != nil {
return nil, err
}
vals = append(vals, val)
}
return vals, nil
}
func attrValueFromJSON(b []byte, typ attr.Type) (attr.Value, error) {
switch typ := typ.(type) {
case basetypes.BoolType:
if b == nil || string(b) == "null" {
return types.BoolNull(), nil
}
var v bool
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return types.BoolValue(v), nil
case basetypes.StringType:
if b == nil || string(b) == "null" {
return types.StringNull(), nil
}
var v string
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return types.StringValue(v), nil
case basetypes.Int64Type:
if b == nil || string(b) == "null" {
return types.Int64Null(), nil
}
var v int64
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return types.Int64Value(v), nil
case basetypes.Float64Type:
if b == nil || string(b) == "null" {
return types.Float64Null(), nil
}
var v float64
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return types.Float64Value(v), nil
case basetypes.NumberType:
if b == nil || string(b) == "null" {
return types.NumberNull(), nil
}
var v float64
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return types.NumberValue(big.NewFloat(v)), nil
case basetypes.ListType:
if b == nil || string(b) == "null" {
return types.ListNull(typ.ElemType), nil
}
vals, err := attrListFromJSON(b, typ.ElemType)
if err != nil {
return nil, err
}
vv, diags := types.ListValue(typ.ElemType, vals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return vv, nil
case basetypes.SetType:
if b == nil || string(b) == "null" {
return types.SetNull(typ.ElemType), nil
}
vals, err := attrListFromJSON(b, typ.ElemType)
if err != nil {
return nil, err
}
vv, diags := types.SetValue(typ.ElemType, vals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return vv, nil
case basetypes.TupleType:
if b == nil || string(b) == "null" {
return types.TupleNull(typ.ElemTypes), nil
}
var l []json.RawMessage
if err := json.Unmarshal(b, &l); err != nil {
return nil, err
}
if len(l) != len(typ.ElemTypes) {
return nil, fmt.Errorf("tuple element size not match: json=%d, type=%d", len(l), len(typ.ElemTypes))
}
vals := make([]attr.Value, 0)
for i, b := range l {
val, err := attrValueFromJSON(b, typ.ElemTypes[i])
if err != nil {
return nil, err
}
vals = append(vals, val)
}
vv, diags := types.TupleValue(typ.ElemTypes, vals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return vv, nil
case basetypes.MapType:
if b == nil || string(b) == "null" {
return types.MapNull(typ.ElemType), nil
}
var m map[string]json.RawMessage
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
vals := map[string]attr.Value{}
for k, v := range m {
val, err := attrValueFromJSON(v, typ.ElemType)
if err != nil {
return nil, err
}
vals[k] = val
}
vv, diags := types.MapValue(typ.ElemType, vals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return vv, nil
case basetypes.ObjectType:
if b == nil || string(b) == "null" {
return types.ObjectNull(typ.AttributeTypes()), nil
}
var m map[string]json.RawMessage
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
vals := map[string]attr.Value{}
attrTypes := typ.AttributeTypes()
for k, attrType := range attrTypes {
val, err := attrValueFromJSON(m[k], attrType)
if err != nil {
return nil, err
}
vals[k] = val
}
vv, diags := types.ObjectValue(attrTypes, vals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return vv, nil
case basetypes.DynamicType:
if b == nil || string(b) == "null" {
return types.DynamicNull(), nil
}
_, vv, err := attrValueFromJSONImplied(b)
return vv, err
default:
return nil, fmt.Errorf("Unhandled type: %T", typ)
}
}
// FromJSONImplied is similar to FromJSON, while it is for typeless case.
// In which case, the following type conversion rules are applied (Go -> TF):
// - bool: bool
// - float64: number
// - string: string
// - []interface{}: tuple
// - map[string]interface{}: object
// - nil: null (dynamic)
// - <unknown>: unknown (dynamic)
func FromJSONImplied(b []byte) (types.Dynamic, error) {
_, v, err := attrValueFromJSONImplied(b)
if err != nil {
return types.Dynamic{}, err
}
return types.DynamicValue(v), nil
}
func attrValueFromJSONImplied(b []byte) (attr.Type, attr.Value, error) {
if string(b) == "null" {
return types.DynamicType, types.DynamicNull(), nil
}
if string(b) == "<unknown>" {
return types.DynamicType, types.DynamicUnknown(), nil
}
var object map[string]json.RawMessage
if err := json.Unmarshal(b, &object); err == nil {
attrTypes := map[string]attr.Type{}
attrVals := map[string]attr.Value{}
for k, v := range object {
attrTypes[k], attrVals[k], err = attrValueFromJSONImplied(v)
if err != nil {
return nil, nil, err
}
}
typ := types.ObjectType{AttrTypes: attrTypes}
val, diags := types.ObjectValue(attrTypes, attrVals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return typ, val, nil
}
var array []json.RawMessage
if err := json.Unmarshal(b, &array); err == nil {
eTypes := []attr.Type{}
eVals := []attr.Value{}
for _, e := range array {
eType, eVal, err := attrValueFromJSONImplied(e)
if err != nil {
return nil, nil, err
}
eTypes = append(eTypes, eType)
eVals = append(eVals, eVal)
}
typ := types.TupleType{ElemTypes: eTypes}
val, diags := types.TupleValue(eTypes, eVals)
if diags.HasError() {
diag := diags.Errors()[0]
return nil, nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
}
return typ, val, nil
}
// Primitives
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal %s: %v", string(b), err)
}
switch v := v.(type) {
case bool:
return types.BoolType, types.BoolValue(v), nil
case float64:
return types.NumberType, types.NumberValue(big.NewFloat(v)), nil
case string:
return types.StringType, types.StringValue(v), nil
case nil:
return types.DynamicType, types.DynamicNull(), nil
default:
return nil, nil, fmt.Errorf("Unhandled type: %T", v)
}
}
func SemanticallyEqual(a, b types.Dynamic) bool {
aJson, err := ToJSONWithUnknownValueHandler(a, func(value attr.Value) ([]byte, error) {
return json.Marshal(nil)
})
if err != nil {
return false
}
bJson, err := ToJSONWithUnknownValueHandler(b, func(value attr.Value) ([]byte, error) {
return json.Marshal(nil)
})
if err != nil {
return false
}
return utils.NormalizeJson(string(aJson)) == utils.NormalizeJson(string(bJson))
}
// IsFullyKnown returns true if `val` is known. If `val` is an aggregate type,
// IsFullyKnown only returns true if all elements and attributes are known, as
// well.
func IsFullyKnown(val attr.Value) bool {
if val == nil {
return true
}
if val.IsUnknown() {
return false
}
switch v := val.(type) {
case types.Dynamic:
return IsFullyKnown(v.UnderlyingValue())
case types.List:
for _, e := range v.Elements() {
if !IsFullyKnown(e) {
return false
}
}
return true
case types.Set:
for _, e := range v.Elements() {
if !IsFullyKnown(e) {
return false
}
}
return true
case types.Tuple:
for _, e := range v.Elements() {
if !IsFullyKnown(e) {
return false
}
}
return true
case types.Map:
for _, e := range v.Elements() {
if !IsFullyKnown(e) {
return false
}
}
return true
case types.Object:
for _, e := range v.Attributes() {
if !IsFullyKnown(e) {
return false
}
}
return true
default:
return true
}
}
func MergeDynamic(a, b types.Dynamic) (types.Dynamic, error) {
aJson, err := ToJSONWithUnknownValueHandler(a, func(val attr.Value) ([]byte, error) {
return json.Marshal("<unknown>")
})
if err != nil {
return types.Dynamic{}, err
}
bJson, err := ToJSONWithUnknownValueHandler(b, func(val attr.Value) ([]byte, error) {
return json.Marshal("<unknown>")
})
if err != nil {
return types.Dynamic{}, err
}
var aObj, bObj interface{}
if err = json.Unmarshal(aJson, &aObj); err != nil {
return types.Dynamic{}, err
}
if err = json.Unmarshal(bJson, &bObj); err != nil {
return types.Dynamic{}, err
}
merged := utils.MergeObject(aObj, bObj)
mergedJson, err := json.Marshal(merged)
if err != nil {
return types.Dynamic{}, err
}
return FromJSONImplied(mergedJson)
}