package tftypes import ( "encoding/json" "fmt" "sort" "strings" ) // Object is a Terraform type representing an unordered collection of // attributes, potentially of differing types, each identifiable with a unique // string name. The number of attributes, their names, and their types are part // of the type signature for the Object, and so two Objects with different // attribute names or types are considered to be distinct types. type Object struct { // AttributeTypes is a map of attributes to their types. The key should // be the name of the attribute, and the value should be the type of // the attribute. AttributeTypes map[string]Type // OptionalAttributes is a set of attributes that are optional. This // allows values of this object type to include or not include those // attributes without changing the type of the value; other attributes // are considered part of the type signature, and their absence means a // value is no longer of that type. // // OptionalAttributes is only valid when declaring a type constraint // (e.g. Schema) and should not be used as part of a Type when creating // a Value (e.g. NewValue()). When creating a Value, all OptionalAttributes // must still be defined in the Object by setting each attribute to a null // or known value for its attribute type. // // The key of OptionalAttributes should be the name of the attribute // that is optional. The value should be an empty struct, used only to // indicate presence. // // OptionalAttributes must also be listed in the AttributeTypes // property, indicating their types. OptionalAttributes map[string]struct{} // used to make this type uncomparable // see https://golang.org/ref/spec#Comparison_operators // this enforces the use of Is, instead _ []struct{} } // ApplyTerraform5AttributePathStep applies an AttributePathStep to an Object, // returning the Type found at that AttributePath within the Object. If the // AttributePathStep cannot be applied to the Object, an ErrInvalidStep error // will be returned. func (o Object) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { switch s := step.(type) { case AttributeName: if len(o.AttributeTypes) == 0 { return nil, ErrInvalidStep } attrType, ok := o.AttributeTypes[string(s)] if !ok { return nil, ErrInvalidStep } return attrType, nil default: return nil, ErrInvalidStep } } // Equal returns true if the two Objects are exactly equal. Unlike Is, passing // in an Object with no AttributeTypes will always return false. func (o Object) Equal(other Type) bool { v, ok := other.(Object) if !ok { return false } if v.AttributeTypes == nil || o.AttributeTypes == nil { // when doing exact comparisons, we can't compare types that // don't have attribute types set, so we just consider them not // equal return false } // if the don't have the exact same optional attributes, they're not // the same type. if len(v.OptionalAttributes) != len(o.OptionalAttributes) { return false } for attr := range o.OptionalAttributes { if !v.attrIsOptional(attr) { return false } } // if they don't have the same attribute types, they're not the // same type. if len(v.AttributeTypes) != len(o.AttributeTypes) { return false } for k, typ := range o.AttributeTypes { if _, ok := v.AttributeTypes[k]; !ok { return false } if !typ.Equal(v.AttributeTypes[k]) { return false } } return true } // UsableAs returns whether the two Objects are type compatible. // // If the other type is DynamicPseudoType, it will return true. // If the other type is not a Object, it will return false. // If the other Object does not have matching AttributeTypes length, it will // return false. // If the other Object does not have a type compatible ElementType for every // nested attribute, it will return false. // // If the current type contains OptionalAttributes, it will panic. func (o Object) UsableAs(other Type) bool { if other.Is(DynamicPseudoType) { return true } v, ok := other.(Object) if !ok { return false } if len(o.OptionalAttributes) > 0 { panic("Objects with OptionalAttributes cannot be used.") } if len(v.AttributeTypes) != len(o.AttributeTypes) { return false } for k, typ := range o.AttributeTypes { otherTyp, ok := v.AttributeTypes[k] if !ok { return false } if !typ.UsableAs(otherTyp) { return false } } return true } // Is returns whether `t` is an Object type or not. It does not perform any // AttributeTypes checks. func (o Object) Is(t Type) bool { _, ok := t.(Object) return ok } func (o Object) attrIsOptional(attr string) bool { if o.OptionalAttributes == nil { return false } _, ok := o.OptionalAttributes[attr] return ok } func (o Object) String() string { var res strings.Builder res.WriteString("tftypes.Object[") keys := make([]string, 0, len(o.AttributeTypes)) for k := range o.AttributeTypes { keys = append(keys, k) } sort.Strings(keys) for pos, key := range keys { if pos != 0 { res.WriteString(", ") } res.WriteString(`"` + key + `":`) res.WriteString(o.AttributeTypes[key].String()) if o.attrIsOptional(key) { res.WriteString(`?`) } } res.WriteString("]") return res.String() } func (o Object) private() {} func (o Object) supportedGoTypes() []string { return []string{"map[string]tftypes.Value"} } func valueFromObject(types map[string]Type, optionalAttrs map[string]struct{}, in interface{}) (Value, error) { switch value := in.(type) { case map[string]Value: // types should only be null if the "Object" is actually a // DynamicPseudoType being created from a map[string]Value. In // which case, we don't know what types it should have, or even // how many there will be, so let's not validate that at all if types != nil { for k := range types { if _, ok := optionalAttrs[k]; ok { // if it's optional, we don't need to check that it has a value continue } if _, ok := value[k]; !ok { return Value{}, fmt.Errorf("can't create a tftypes.Value of type %s, required attribute %q not set", Object{AttributeTypes: types}, k) } } for k, v := range value { typ, ok := types[k] if !ok { return Value{}, fmt.Errorf("can't set a value on %q in tftypes.NewValue, key not part of the object type %s", k, Object{AttributeTypes: types}) } if v.Type() == nil { return Value{}, NewAttributePath().WithAttributeName(k).NewErrorf("missing value type") } if !v.Type().UsableAs(typ) { return Value{}, NewAttributePath().WithAttributeName(k).NewErrorf("can't use %s as %s", v.Type(), typ) } } } return Value{ typ: Object{AttributeTypes: types, OptionalAttributes: optionalAttrs}, value: value, }, nil default: return Value{}, fmt.Errorf("tftypes.NewValue can't use %T as a tftypes.Object; expected types are: %s", in, formattedSupportedGoTypes(Object{})) } } // MarshalJSON returns a JSON representation of the full type signature of `o`, // including the AttributeTypes. // // Deprecated: this is not meant to be called by third-party code. func (o Object) MarshalJSON() ([]byte, error) { attrs, err := json.Marshal(o.AttributeTypes) if err != nil { return nil, err } var optionalAttrs []byte if len(o.OptionalAttributes) > 0 { optionalAttrs = append(optionalAttrs, []byte(",")...) names := make([]string, 0, len(o.OptionalAttributes)) for k := range o.OptionalAttributes { names = append(names, k) } sort.Strings(names) optionalsJSON, err := json.Marshal(names) if err != nil { return nil, err } optionalAttrs = append(optionalAttrs, optionalsJSON...) } return []byte(`["object",` + string(attrs) + string(optionalAttrs) + `]`), nil }