Extend hcl2 support with more functions

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2020-12-23 12:33:55 +01:00
parent 74f76cf4e9
commit 96e7f3224a
90 changed files with 4425 additions and 198 deletions

View File

@ -13,6 +13,14 @@ import (
// if we're converting from a set into a list of the same element type.)
func conversionCollectionToList(ety cty.Type, conv conversion) conversion {
return func(val cty.Value, path cty.Path) (cty.Value, error) {
if !val.Length().IsKnown() {
// If the input collection has an unknown length (which is true
// for a set containing unknown values) then our result must be
// an unknown list, because we can't predict how many elements
// the resulting list should have.
return cty.UnknownVal(cty.List(val.Type().ElementType())), nil
}
elems := make([]cty.Value, 0, val.LengthInt())
i := int64(0)
elemPath := append(path.Copy(), nil)
@ -156,34 +164,45 @@ func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
// given tuple type and return a set of the given element type.
//
// Will panic if the given tupleType isn't actually a tuple type.
func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
func conversionTupleToSet(tupleType cty.Type, setEty cty.Type, unsafe bool) conversion {
tupleEtys := tupleType.TupleElementTypes()
if len(tupleEtys) == 0 {
// Empty tuple short-circuit
return func(val cty.Value, path cty.Path) (cty.Value, error) {
return cty.SetValEmpty(listEty), nil
return cty.SetValEmpty(setEty), nil
}
}
if listEty == cty.DynamicPseudoType {
if setEty == cty.DynamicPseudoType {
// This is a special case where the caller wants us to find
// a suitable single type that all elements can convert to, if
// possible.
listEty, _ = unify(tupleEtys, unsafe)
if listEty == cty.NilType {
setEty, _ = unify(tupleEtys, unsafe)
if setEty == cty.NilType {
return nil
}
// If the set element type after unification is still the dynamic
// type, the only way this can result in a valid set is if all values
// are of dynamic type
if setEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}
elemConvs := make([]conversion, len(tupleEtys))
for i, tupleEty := range tupleEtys {
if tupleEty.Equals(listEty) {
if tupleEty.Equals(setEty) {
// no conversion required
continue
}
elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
elemConvs[i] = getConversion(tupleEty, setEty, unsafe)
if elemConvs[i] == nil {
// If any of our element conversions are impossible, then the our
// whole conversion is impossible.
@ -244,6 +263,17 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
if listEty == cty.NilType {
return nil
}
// If the list element type after unification is still the dynamic
// type, the only way this can result in a valid list is if all values
// are of dynamic type
if listEty == cty.DynamicPseudoType {
for _, tupleEty := range tupleEtys {
if !tupleEty.Equals(cty.DynamicPseudoType) {
return nil
}
}
}
}
elemConvs := make([]conversion, len(tupleEtys))
@ -265,6 +295,7 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
// element conversions in elemConvs
return func(val cty.Value, path cty.Path) (cty.Value, error) {
elems := make([]cty.Value, 0, len(elemConvs))
elemTys := make([]cty.Type, 0, len(elems))
elemPath := append(path.Copy(), nil)
i := int64(0)
it := val.ElementIterator()
@ -284,10 +315,15 @@ func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) co
}
}
elems = append(elems, val)
elemTys = append(elemTys, val.Type())
i++
}
elems, err := conversionUnifyListElements(elems, elemPath, unsafe)
if err != nil {
return cty.NilVal, err
}
return cty.ListVal(elems), nil
}
}
@ -430,6 +466,16 @@ func conversionMapToObject(mapType cty.Type, objType cty.Type, unsafe bool) conv
elems[name.AsString()] = val
}
for name, aty := range objectAtys {
if _, exists := elems[name]; !exists {
if optional := objType.AttributeOptional(name); optional {
elems[name] = cty.NullVal(aty)
} else {
return cty.NilVal, path.NewErrorf("map has no element for required attribute %q", name)
}
}
}
return cty.ObjectVal(elems), nil
}
}
@ -441,6 +487,7 @@ func conversionUnifyCollectionElements(elems map[string]cty.Value, path cty.Path
}
unifiedType, _ := unify(elemTypes, unsafe)
if unifiedType == cty.NilType {
return nil, path.NewErrorf("collection elements cannot be unified")
}
unifiedElems := make(map[string]cty.Value)
@ -486,3 +533,37 @@ func conversionCheckMapElementTypes(elems map[string]cty.Value, path cty.Path) e
return nil
}
func conversionUnifyListElements(elems []cty.Value, path cty.Path, unsafe bool) ([]cty.Value, error) {
elemTypes := make([]cty.Type, len(elems))
for i, elem := range elems {
elemTypes[i] = elem.Type()
}
unifiedType, _ := unify(elemTypes, unsafe)
if unifiedType == cty.NilType {
return nil, path.NewErrorf("collection elements cannot be unified")
}
ret := make([]cty.Value, len(elems))
elemPath := append(path.Copy(), nil)
for i, elem := range elems {
if elem.Type().Equals(unifiedType) {
ret[i] = elem
continue
}
conv := getConversion(elem.Type(), unifiedType, unsafe)
if conv == nil {
}
elemPath[len(elemPath)-1] = cty.IndexStep{
Key: cty.NumberIntVal(int64(i)),
}
val, err := conv(elem, elemPath)
if err != nil {
return nil, err
}
ret[i] = val
}
return ret, nil
}

View File

@ -11,17 +11,29 @@ import (
// type, meaning that each attribute of the output type has a corresponding
// attribute in the input type where a recursive conversion is available.
//
// If the "out" type has any optional attributes, those attributes may be
// absent in the "in" type, in which case null values will be used in their
// place in the result.
//
// Shallow object conversions work the same for both safe and unsafe modes,
// but the safety flag is passed on to recursive conversions and may thus
// limit the above definition of "subset".
func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion {
inAtys := in.AttributeTypes()
outAtys := out.AttributeTypes()
outOptionals := out.OptionalAttributes()
attrConvs := make(map[string]conversion)
for name, outAty := range outAtys {
inAty, exists := inAtys[name]
if !exists {
if _, optional := outOptionals[name]; optional {
// If it's optional then we'll skip inserting an
// attribute conversion and then deal with inserting
// the default value in our overall conversion logic
// later.
continue
}
// No conversion is available, then.
return nil
}
@ -71,6 +83,13 @@ func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion {
attrVals[name] = val
}
for name := range outOptionals {
if _, exists := attrVals[name]; !exists {
wantTy := outAtys[name]
attrVals[name] = cty.NullVal(wantTy)
}
}
return cty.ObjectVal(attrVals), nil
}
}

View File

@ -78,10 +78,16 @@ func mismatchMessageObjects(got, want cty.Type) string {
for name, wantAty := range wantAtys {
gotAty, exists := gotAtys[name]
if !exists {
missingAttrs = append(missingAttrs, name)
if !want.AttributeOptional(name) {
missingAttrs = append(missingAttrs, name)
}
continue
}
if gotAty.Equals(wantAty) {
continue // exact match, so no problem
}
// We'll now try to convert these attributes in isolation and
// see if we have a nested conversion error to report.
// We'll try an unsafe conversion first, and then fall back on

View File

@ -244,19 +244,21 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
return cty.UnknownVal(expectedType), nil
}
if val.IsMarked() && !spec.AllowMarked {
unwrappedVal, marks := val.Unmark()
// In order to avoid additional overhead on applications that
// are not using marked values, we copy the given args only
// if we encounter a marked value we need to unmark. However,
// as a consequence we end up doing redundant copying if multiple
// marked values need to be unwrapped. That seems okay because
// argument lists are generally small.
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
if !spec.AllowMarked {
unwrappedVal, marks := val.UnmarkDeep()
if len(marks) > 0 {
// In order to avoid additional overhead on applications that
// are not using marked values, we copy the given args only
// if we encounter a marked value we need to unmark. However,
// as a consequence we end up doing redundant copying if multiple
// marked values need to be unwrapped. That seems okay because
// argument lists are generally small.
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
}
}
}
@ -266,13 +268,15 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
if !val.IsKnown() && !spec.AllowUnknown {
return cty.UnknownVal(expectedType), nil
}
if val.IsMarked() && !spec.AllowMarked {
unwrappedVal, marks := val.Unmark()
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[len(posArgs)+i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
if !spec.AllowMarked {
unwrappedVal, marks := val.UnmarkDeep()
if len(marks) > 0 {
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[len(posArgs)+i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
}
}
}
}

View File

@ -138,6 +138,13 @@ var ElementFunc = function.New(&function.Spec{
},
Type: func(args []cty.Value) (cty.Type, error) {
list := args[0]
index := args[1]
if index.IsKnown() {
if index.LessThan(cty.NumberIntVal(0)).True() {
return cty.DynamicPseudoType, fmt.Errorf("cannot use element function with a negative index")
}
}
listTy := list.Type()
switch {
case listTy.IsListType():
@ -173,6 +180,10 @@ var ElementFunc = function.New(&function.Spec{
return cty.DynamicVal, fmt.Errorf("invalid index: %s", err)
}
if args[1].LessThan(cty.NumberIntVal(0)).True() {
return cty.DynamicVal, fmt.Errorf("cannot use element function with a negative index")
}
if !args[0].IsKnown() {
return cty.UnknownVal(retType), nil
}
@ -240,6 +251,10 @@ var CoalesceListFunc = function.New(&function.Spec{
return cty.UnknownVal(retType), nil
}
if arg.IsNull() {
continue
}
if arg.LengthInt() > 0 {
return arg, nil
}
@ -492,6 +507,11 @@ var FlattenFunc = function.New(&function.Spec{
// We can flatten lists with unknown values, as long as they are not
// lists themselves.
func flattener(flattenList cty.Value) ([]cty.Value, bool) {
if !flattenList.Length().IsKnown() {
// If we don't know the length of what we're flattening then we can't
// predict the length of our result yet either.
return nil, false
}
out := make([]cty.Value, 0)
for it := flattenList.ElementIterator(); it.Next(); {
_, val := it.Element()
@ -742,16 +762,10 @@ var MergeFunc = function.New(&function.Spec{
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := make(map[string]cty.Value)
// if all inputs are null, return a null value rather than an object
// with null attributes
allNull := true
for _, arg := range args {
if arg.IsNull() {
continue
} else {
allNull = false
}
for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element()
outputMap[k.AsString()] = v
@ -759,9 +773,10 @@ var MergeFunc = function.New(&function.Spec{
}
switch {
case allNull:
return cty.NullVal(retType), nil
case retType.IsMapType():
if len(outputMap) == 0 {
return cty.MapValEmpty(retType.ElementType()), nil
}
return cty.MapVal(outputMap), nil
case retType.IsObjectType(), retType.Equals(cty.DynamicPseudoType):
return cty.ObjectVal(outputMap), nil
@ -869,6 +884,10 @@ var SetProductFunc = function.New(&function.Spec{
total := 1
for _, arg := range args {
if !arg.Length().IsKnown() {
return cty.UnknownVal(retType), nil
}
// Because of our type checking function, we are guaranteed that
// all of the arguments are known, non-null values of types that
// support LengthInt.
@ -1016,7 +1035,8 @@ func sliceIndexes(args []cty.Value) (int, int, bool, error) {
var startIndex, endIndex, length int
var startKnown, endKnown, lengthKnown bool
if args[0].Type().IsTupleType() || args[0].IsKnown() { // if it's a tuple then we always know the length by the type, but lists must be known
// If it's a tuple then we always know the length by the type, but collections might be unknown or have unknown length
if args[0].Type().IsTupleType() || args[0].Length().IsKnown() {
length = args[0].LengthInt()
lengthKnown = true
}

View File

@ -362,9 +362,14 @@ func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err er
for i := 1; i < len(data); i++ {
if data[i] == esc {
if (i + 1) == len(data) {
// We need at least one more byte to decide if this is an
// escape or a terminator.
return 0, nil, nil
if atEOF {
// We have a closing quote and are at the end of our input
return len(data), data, nil
} else {
// We need at least one more byte to decide if this is an
// escape or a terminator.
return 0, nil, nil
}
}
if data[i+1] == esc {
i++ // doubled-up quotes are an escape sequence

View File

@ -85,7 +85,7 @@ var FormatListFunc = function.New(&function.Spec{
argTy := arg.Type()
switch {
case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull():
if !argTy.IsTupleType() && !arg.IsKnown() {
if !argTy.IsTupleType() && !(arg.IsKnown() && arg.Length().IsKnown()) {
// We can't iterate this one at all yet then, so we can't
// yet produce a result.
unknowns[i] = true

View File

@ -12,6 +12,7 @@ var JSONEncodeFunc = function.New(&function.Spec{
Name: "val",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.String),
@ -24,6 +25,10 @@ var JSONEncodeFunc = function.New(&function.Spec{
return cty.UnknownVal(retType), nil
}
if val.IsNull() {
return cty.StringVal("null"), nil
}
buf, err := json.Marshal(val, val.Type())
if err != nil {
return cty.NilVal, err

View File

@ -44,7 +44,7 @@ var SetUnionFunc = function.New(&function.Spec{
Type: setOperationReturnType,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Union(s2)
}),
}, true),
})
var SetIntersectionFunc = function.New(&function.Spec{
@ -63,7 +63,7 @@ var SetIntersectionFunc = function.New(&function.Spec{
Type: setOperationReturnType,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Intersection(s2)
}),
}, false),
})
var SetSubtractFunc = function.New(&function.Spec{
@ -82,7 +82,7 @@ var SetSubtractFunc = function.New(&function.Spec{
Type: setOperationReturnType,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Subtract(s2)
}),
}, false),
})
var SetSymmetricDifferenceFunc = function.New(&function.Spec{
@ -100,8 +100,8 @@ var SetSymmetricDifferenceFunc = function.New(&function.Spec{
},
Type: setOperationReturnType,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Subtract(s2)
}),
return s1.SymmetricDifference(s2)
}, false),
})
// SetHasElement determines whether the given set contains the given value as an
@ -163,8 +163,23 @@ func SetSymmetricDifference(sets ...cty.Value) (cty.Value, error) {
func setOperationReturnType(args []cty.Value) (ret cty.Type, err error) {
var etys []cty.Type
for _, arg := range args {
etys = append(etys, arg.Type().ElementType())
ty := arg.Type().ElementType()
// Do not unify types for empty dynamic pseudo typed collections. These
// will always convert to any other concrete type.
if arg.IsKnown() && arg.LengthInt() == 0 && ty.Equals(cty.DynamicPseudoType) {
continue
}
etys = append(etys, ty)
}
// If all element types were skipped (due to being empty dynamic collections),
// the return type should also be a set of dynamic pseudo type.
if len(etys) == 0 {
return cty.Set(cty.DynamicPseudoType), nil
}
newEty, _ := convert.UnifyUnsafe(etys)
if newEty == cty.NilType {
return cty.NilType, fmt.Errorf("given sets must all have compatible element types")
@ -172,13 +187,21 @@ func setOperationReturnType(args []cty.Value) (ret cty.Type, err error) {
return cty.Set(newEty), nil
}
func setOperationImpl(f func(s1, s2 cty.ValueSet) cty.ValueSet) function.ImplFunc {
func setOperationImpl(f func(s1, s2 cty.ValueSet) cty.ValueSet, allowUnknowns bool) function.ImplFunc {
return func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
first := args[0]
first, err = convert.Convert(first, retType)
if err != nil {
return cty.NilVal, function.NewArgError(0, err)
}
if !allowUnknowns && !first.IsWhollyKnown() {
// This set function can produce a correct result only when all
// elements are known, because eventually knowing the unknown
// values may cause the result to have fewer known elements, or
// might cause a result with no unknown elements at all to become
// one with a different length.
return cty.UnknownVal(retType), nil
}
set := first.AsValueSet()
for i, arg := range args[1:] {
@ -186,6 +209,10 @@ func setOperationImpl(f func(s1, s2 cty.ValueSet) cty.ValueSet) function.ImplFun
if err != nil {
return cty.NilVal, function.NewArgError(i+1, err)
}
if !allowUnknowns && !arg.IsWhollyKnown() {
// (For the same reason as we did this check for "first" above.)
return cty.UnknownVal(retType), nil
}
argSet := arg.AsValueSet()
set = f(set, argSet)

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"sort"
)
// MarshalJSON is an implementation of json.Marshaler that allows Type
@ -52,6 +53,19 @@ func (t Type) MarshalJSON() ([]byte, error) {
}
buf.WriteString(`["object",`)
buf.Write(atysJSON)
if optionals := t.OptionalAttributes(); len(optionals) > 0 {
buf.WriteByte(',')
optionalNames := make([]string, 0, len(optionals))
for k := range optionals {
optionalNames = append(optionalNames, k)
}
sort.Strings(optionalNames)
optionalsJSON, err := json.Marshal(optionalNames)
if err != nil {
return nil, err
}
buf.Write(optionalsJSON)
}
buf.WriteRune(']')
return buf.Bytes(), nil
case typeTuple:
@ -148,7 +162,16 @@ func (t *Type) UnmarshalJSON(buf []byte) error {
if err != nil {
return err
}
*t = Object(atys)
if dec.More() {
var optionals []string
err = dec.Decode(&optionals)
if err != nil {
return err
}
*t = ObjectWithOptionalAttrs(atys, optionals)
} else {
*t = Object(atys)
}
case "tuple":
var etys []Type
err = dec.Decode(&etys)

View File

@ -10,7 +10,7 @@ import (
func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error {
if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized")
return path.NewErrorf("value has marks, so it cannot be serialized as JSON")
}
// If we're going to decode as DynamicPseudoType then we need to save

View File

@ -67,6 +67,23 @@ func (m ValueMarks) GoString() string {
return s.String()
}
// PathValueMarks is a structure that enables tracking marks
// and the paths where they are located in one type
type PathValueMarks struct {
Path Path
Marks ValueMarks
}
func (p PathValueMarks) Equal(o PathValueMarks) bool {
if !p.Path.Equals(o.Path) {
return false
}
if !p.Marks.Equal(o.Marks) {
return false
}
return true
}
// IsMarked returns true if and only if the receiving value carries at least
// one mark. A marked value cannot be used directly with integration methods
// without explicitly unmarking it (and retrieving the markings) first.
@ -174,6 +191,31 @@ func (val Value) Mark(mark interface{}) Value {
}
}
type applyPathValueMarksTransformer struct {
pvm []PathValueMarks
}
func (t *applyPathValueMarksTransformer) Enter(p Path, v Value) (Value, error) {
return v, nil
}
func (t *applyPathValueMarksTransformer) Exit(p Path, v Value) (Value, error) {
for _, path := range t.pvm {
if p.Equals(path.Path) {
return v.WithMarks(path.Marks), nil
}
}
return v, nil
}
// MarkWithPaths accepts a slice of PathValueMarks to apply
// markers to particular paths and returns the marked
// Value.
func (val Value) MarkWithPaths(pvm []PathValueMarks) Value {
ret, _ := TransformWithTransformer(val, &applyPathValueMarksTransformer{pvm})
return ret
}
// Unmark separates the marks of the receiving value from the value itself,
// removing a new unmarked value and a map (representing a set) of the marks.
//
@ -191,6 +233,24 @@ func (val Value) Unmark() (Value, ValueMarks) {
}, marks
}
type unmarkTransformer struct {
pvm []PathValueMarks
}
func (t *unmarkTransformer) Enter(p Path, v Value) (Value, error) {
unmarkedVal, marks := v.Unmark()
if len(marks) > 0 {
path := make(Path, len(p), len(p)+1)
copy(path, p)
t.pvm = append(t.pvm, PathValueMarks{path, marks})
}
return unmarkedVal, nil
}
func (t *unmarkTransformer) Exit(p Path, v Value) (Value, error) {
return v, nil
}
// UnmarkDeep is similar to Unmark, but it works with an entire nested structure
// rather than just the given value directly.
//
@ -198,17 +258,29 @@ func (val Value) Unmark() (Value, ValueMarks) {
// the returned marks set includes the superset of all of the marks encountered
// during the operation.
func (val Value) UnmarkDeep() (Value, ValueMarks) {
t := unmarkTransformer{}
ret, _ := TransformWithTransformer(val, &t)
marks := make(ValueMarks)
ret, _ := Transform(val, func(_ Path, v Value) (Value, error) {
unmarkedV, valueMarks := v.Unmark()
for m, s := range valueMarks {
for _, pvm := range t.pvm {
for m, s := range pvm.Marks {
marks[m] = s
}
return unmarkedV, nil
})
}
return ret, marks
}
// UnmarkDeepWithPaths is like UnmarkDeep, except it returns a slice
// of PathValueMarks rather than a superset of all marks. This allows
// a caller to know which marks are associated with which paths
// in the Value.
func (val Value) UnmarkDeepWithPaths() (Value, []PathValueMarks) {
t := unmarkTransformer{}
ret, _ := TransformWithTransformer(val, &t)
return ret, t.pvm
}
func (val Value) unmarkForce() Value {
unw, _ := val.Unmark()
return unw

View File

@ -2,11 +2,13 @@ package cty
import (
"fmt"
"sort"
)
type typeObject struct {
typeImplSigil
AttrTypes map[string]Type
AttrTypes map[string]Type
AttrOptional map[string]struct{}
}
// Object creates an object type with the given attribute types.
@ -14,14 +16,52 @@ type typeObject struct {
// After a map is passed to this function the caller must no longer access it,
// since ownership is transferred to this library.
func Object(attrTypes map[string]Type) Type {
return ObjectWithOptionalAttrs(attrTypes, nil)
}
// ObjectWithOptionalAttrs creates an object type where some of its attributes
// are optional.
//
// This function is EXPERIMENTAL. The behavior of the function or of any other
// functions working either directly or indirectly with a type created by
// this function is not currently considered as a compatibility constraint, and
// is subject to change even in minor-version releases of this module. Other
// modules that work with cty types and values may or may not support object
// types with optional attributes; if they do not, their behavior when
// receiving one may be non-ideal.
//
// Optional attributes are significant only when an object type is being used
// as a target type for conversion in the "convert" package. A value of an
// object type always has a value for each of the attributes in the attribute
// types table, with optional values replaced with null during conversion.
//
// All keys in the optional slice must also exist in the attrTypes map. If not,
// this function will panic.
//
// After a map or array is passed to this function the caller must no longer
// access it, since ownership is transferred to this library.
func ObjectWithOptionalAttrs(attrTypes map[string]Type, optional []string) Type {
attrTypesNorm := make(map[string]Type, len(attrTypes))
for k, v := range attrTypes {
attrTypesNorm[NormalizeString(k)] = v
}
var optionalSet map[string]struct{}
if len(optional) > 0 {
optionalSet = make(map[string]struct{}, len(optional))
for _, k := range optional {
k = NormalizeString(k)
if _, exists := attrTypesNorm[k]; !exists {
panic(fmt.Sprintf("optional contains undeclared attribute %q", k))
}
optionalSet[k] = struct{}{}
}
}
return Type{
typeObject{
AttrTypes: attrTypesNorm,
AttrTypes: attrTypesNorm,
AttrOptional: optionalSet,
},
}
}
@ -44,6 +84,11 @@ func (t typeObject) Equals(other Type) bool {
if !oty.Equals(ty) {
return false
}
_, opt := t.AttrOptional[attr]
_, oopt := ot.AttrOptional[attr]
if opt != oopt {
return false
}
}
return true
@ -66,6 +111,14 @@ func (t typeObject) GoString() string {
if len(t.AttrTypes) == 0 {
return "cty.EmptyObject"
}
if len(t.AttrOptional) > 0 {
opt := make([]string, len(t.AttrOptional))
for k := range t.AttrOptional {
opt = append(opt, k)
}
sort.Strings(opt)
return fmt.Sprintf("cty.ObjectWithOptionalAttrs(%#v, %#v)", t.AttrTypes, opt)
}
return fmt.Sprintf("cty.Object(%#v)", t.AttrTypes)
}
@ -133,3 +186,35 @@ func (t Type) AttributeTypes() map[string]Type {
}
panic("AttributeTypes on non-object Type")
}
// OptionalAttributes returns a map representing the set of attributes
// that are optional. Will panic if the receiver is not an object type
// (use IsObjectType to confirm).
//
// The returned map is part of the internal state of the type, and is provided
// for read access only. It is forbidden for any caller to modify the returned
// map.
func (t Type) OptionalAttributes() map[string]struct{} {
if ot, ok := t.typeImpl.(typeObject); ok {
return ot.AttrOptional
}
panic("OptionalAttributes on non-object Type")
}
// AttributeOptional returns true if the attribute of the given name is
// optional.
//
// Will panic if the receiver is not an object type (use IsObjectType to
// confirm) or if the object type has no such attribute (use HasAttribute to
// confirm).
func (t Type) AttributeOptional(name string) bool {
name = NormalizeString(name)
if ot, ok := t.typeImpl.(typeObject); ok {
if _, hasAttr := ot.AttrTypes[name]; !hasAttr {
panic("no such attribute")
}
_, exists := ot.AttrOptional[name]
return exists
}
panic("AttributeDefaultValue on non-object Type")
}

View File

@ -196,3 +196,9 @@ func (r pathSetRules) Equivalent(a, b interface{}) bool {
return true
}
// SameRules is true if both Rules instances are pathSetRules structs.
func (r pathSetRules) SameRules(other set.Rules) bool {
_, ok := other.(pathSetRules)
return ok
}

View File

@ -22,6 +22,10 @@ type Rules interface {
// though it is *not* required that two values with the same hash value
// be equivalent.
Equivalent(interface{}, interface{}) bool
// SameRules returns true if the instance is equivalent to another Rules
// instance.
SameRules(Rules) bool
}
// OrderedRules is an extension of Rules that can apply a partial order to

View File

@ -41,7 +41,7 @@ func NewSetFromSlice(rules Rules, vals []interface{}) Set {
}
func sameRules(s1 Set, s2 Set) bool {
return s1.rules == s2.rules
return s1.rules.SameRules(s2.rules)
}
func mustHaveSameRules(s1 Set, s2 Set) {
@ -53,7 +53,7 @@ func mustHaveSameRules(s1 Set, s2 Set) {
// HasRules returns true if and only if the receiving set has the given rules
// instance as its rules.
func (s Set) HasRules(rules Rules) bool {
return s.rules == rules
return s.rules.SameRules(rules)
}
// Rules returns the receiving set's rules instance.

View File

@ -65,6 +65,17 @@ func (r setRules) Equivalent(v1 interface{}, v2 interface{}) bool {
return eqv.v == true
}
// SameRules is only true if the other Rules instance is also a setRules struct,
// and the types are considered equal.
func (r setRules) SameRules(other set.Rules) bool {
rules, ok := other.(setRules)
if !ok {
return false
}
return r.Type.Equals(rules.Type)
}
// Less is an implementation of set.OrderedRules so that we can iterate over
// set elements in a consistent order, where such an order is possible.
func (r setRules) Less(v1, v2 interface{}) bool {

View File

@ -36,6 +36,9 @@ func (t typeImplSigil) isTypeImpl() typeImplSigil {
// Equals returns true if the other given Type exactly equals the receiver
// type.
func (t Type) Equals(other Type) bool {
if t == NilType || other == NilType {
return t == other
}
return t.typeImpl.Equals(other)
}
@ -87,7 +90,7 @@ func (t Type) HasDynamicTypes() bool {
case t.IsPrimitiveType():
return false
case t.IsCollectionType():
return false
return t.ElementType().HasDynamicTypes()
case t.IsObjectType():
attrTypes := t.AttributeTypes()
for _, at := range attrTypes {

View File

@ -5,7 +5,8 @@ package cty
type unknownType struct {
}
// Unknown is a special value that can be
// unknown is a special value that can be used as the internal value of a
// Value to create a placeholder for a value that isn't yet known.
var unknown interface{} = &unknownType{}
// UnknownVal returns an Value that represents an unknown value of the given

View File

@ -106,3 +106,37 @@ func (val Value) IsWhollyKnown() bool {
return true
}
}
// HasWhollyKnownType checks if the value is dynamic, or contains any nested
// DynamicVal. This implies that both the value is not known, and the final
// type may change.
func (val Value) HasWhollyKnownType() bool {
// a null dynamic type is known
if val.IsNull() {
return true
}
// an unknown DynamicPseudoType is a DynamicVal, but we don't want to
// check that value for equality here, since this method is used within the
// equality check.
if !val.IsKnown() && val.ty == DynamicPseudoType {
return false
}
if val.CanIterateElements() {
// if the value is not known, then we can look directly at the internal
// types
if !val.IsKnown() {
return !val.ty.HasDynamicTypes()
}
for it := val.ElementIterator(); it.Next(); {
_, ev := it.Element()
if !ev.HasWhollyKnownType() {
return false
}
}
}
return true
}

View File

@ -247,11 +247,6 @@ func SetVal(vals []Value) Value {
val = unmarkedVal
markSets = append(markSets, marks)
}
if val.ContainsMarked() {
// FIXME: Allow this, but unmark the values and apply the
// marking to the set itself instead.
panic("set cannot contain marked values")
}
if elementType == DynamicPseudoType {
elementType = val.ty
} else if val.ty != DynamicPseudoType && !elementType.Equals(val.ty) {

View File

@ -3,7 +3,6 @@ package cty
import (
"fmt"
"math/big"
"reflect"
"github.com/zclconf/go-cty/cty/set"
)
@ -133,9 +132,9 @@ func (val Value) Equals(other Value) Value {
case val.IsKnown() && !other.IsKnown():
switch {
case val.IsNull(), other.ty.HasDynamicTypes():
// If known is Null, we need to wait for the unkown value since
// If known is Null, we need to wait for the unknown value since
// nulls of any type are equal.
// An unkown with a dynamic type compares as unknown, which we need
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
case !val.ty.Equals(other.ty):
@ -148,9 +147,9 @@ func (val Value) Equals(other Value) Value {
case other.IsKnown() && !val.IsKnown():
switch {
case other.IsNull(), val.ty.HasDynamicTypes():
// If known is Null, we need to wait for the unkown value since
// If known is Null, we need to wait for the unknown value since
// nulls of any type are equal.
// An unkown with a dynamic type compares as unknown, which we need
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
case !other.ty.Equals(val.ty):
@ -171,7 +170,15 @@ func (val Value) Equals(other Value) Value {
return BoolVal(false)
}
if val.ty.HasDynamicTypes() || other.ty.HasDynamicTypes() {
// Check if there are any nested dynamic values making this comparison
// unknown.
if !val.HasWhollyKnownType() || !other.HasWhollyKnownType() {
// Even if we have dynamic values, we can still determine inequality if
// there is no way the types could later conform.
if val.ty.TestConformance(other.ty) != nil && other.ty.TestConformance(val.ty) != nil {
return BoolVal(false)
}
return UnknownVal(Bool)
}
@ -262,24 +269,26 @@ func (val Value) Equals(other Value) Value {
s2 := other.v.(set.Set)
equal := true
// Note that by our definition of sets it's never possible for two
// sets that contain unknown values (directly or indicrectly) to
// ever be equal, even if they are otherwise identical.
// FIXME: iterating both lists and checking each item is not the
// ideal implementation here, but it works with the primitives we
// have in the set implementation. Perhaps the set implementation
// can provide its own equality test later.
s1.EachValue(func(v interface{}) {
if !s2.Has(v) {
// Two sets are equal if all of their values are known and all values
// in one are also in the other.
for it := s1.Iterator(); it.Next(); {
rv := it.Value()
if rv == unknown { // "unknown" is the internal representation of unknown-ness
return UnknownVal(Bool)
}
if !s2.Has(rv) {
equal = false
}
})
s2.EachValue(func(v interface{}) {
if !s1.Has(v) {
}
for it := s2.Iterator(); it.Next(); {
rv := it.Value()
if rv == unknown { // "unknown" is the internal representation of unknown-ness
return UnknownVal(Bool)
}
if !s1.Has(rv) {
equal = false
}
})
}
result = equal
case ty.IsMapType():
@ -454,17 +463,32 @@ func (val Value) RawEquals(other Value) bool {
return true
}
return false
case ty.IsSetType():
s1 := val.v.(set.Set)
s2 := other.v.(set.Set)
// Since we're intentionally ignoring our rule that two unknowns
// are never equal, we can cheat here.
// (This isn't 100% right since e.g. it will fail if the set contains
// numbers that are infinite, which DeepEqual can't compare properly.
// We're accepting that limitation for simplicity here, since this
// function is here primarily for testing.)
return reflect.DeepEqual(s1, s2)
case ty.IsSetType():
// Convert the set values into a slice so that we can compare each
// value. This is safe because the underlying sets are ordered (see
// setRules in set_internals.go), and so the results are guaranteed to
// be in a consistent order for two equal sets
setList1 := val.AsValueSlice()
setList2 := other.AsValueSlice()
// If both physical sets have the same length and they have all of their
// _known_ values in common, we know that both sets also have the same
// number of unknown values.
if len(setList1) != len(setList2) {
return false
}
for i := range setList1 {
eq := setList1[i].RawEquals(setList2[i])
if !eq {
return false
}
}
// If we got here without returning false already then our sets are
// equal enough for RawEquals purposes.
return true
case ty.IsMapType():
ety := ty.typeImpl.(typeMap).ElementTypeT
@ -572,8 +596,25 @@ func (val Value) Multiply(other Value) Value {
return *shortCircuit
}
ret := new(big.Float)
// find the larger precision of the arguments
resPrec := val.v.(*big.Float).Prec()
otherPrec := other.v.(*big.Float).Prec()
if otherPrec > resPrec {
resPrec = otherPrec
}
// make sure we have enough precision for the product
ret := new(big.Float).SetPrec(512)
ret.Mul(val.v.(*big.Float), other.v.(*big.Float))
// now reduce the precision back to the greater argument, or the minimum
// required by the product.
minPrec := ret.MinPrec()
if minPrec > resPrec {
resPrec = minPrec
}
ret.SetPrec(resPrec)
return NumberVal(ret)
}
@ -645,11 +686,14 @@ func (val Value) Modulo(other Value) Value {
// FIXME: This is a bit clumsy. Should come back later and see if there's a
// more straightforward way to do this.
rat := val.Divide(other)
ratFloorInt := &big.Int{}
rat.v.(*big.Float).Int(ratFloorInt)
work := (&big.Float{}).SetInt(ratFloorInt)
ratFloorInt, _ := rat.v.(*big.Float).Int(nil)
// start with a copy of the original larger value so that we do not lose
// precision.
v := val.v.(*big.Float)
work := new(big.Float).Copy(v).SetInt(ratFloorInt)
work.Mul(other.v.(*big.Float), work)
work.Sub(val.v.(*big.Float), work)
work.Sub(v, work)
return NumberVal(work)
}
@ -947,8 +991,7 @@ func (val Value) HasElement(elem Value) Value {
// If the receiver is null then this function will panic.
//
// Note that Length is not supported for strings. To determine the length
// of a string, call AsString and take the length of the native Go string
// that is returned.
// of a string, use the Length function in funcs/stdlib.
func (val Value) Length() Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
@ -963,6 +1006,25 @@ func (val Value) Length() Value {
if !val.IsKnown() {
return UnknownVal(Number)
}
if val.Type().IsSetType() {
// The Length rules are a little different for sets because if any
// unknown values are present then the values they are standing in for
// may or may not be equal to other elements in the set, and thus they
// may or may not coalesce with other elements and produce fewer
// items in the resulting set.
storeLength := int64(val.v.(set.Set).Length())
if storeLength == 1 || val.IsWhollyKnown() {
// If our set is wholly known then we know its length.
//
// We also know the length if the physical store has only one
// element, even if that element is unknown, because there's
// nothing else in the set for it to coalesce with and a single
// unknown value cannot represent more than one known value.
return NumberIntVal(storeLength)
}
// Otherwise, we cannot predict the length.
return UnknownVal(Number)
}
return NumberIntVal(int64(val.LengthInt()))
}
@ -972,6 +1034,13 @@ func (val Value) Length() Value {
//
// This is an integration method provided for the convenience of code bridging
// into Go's type system.
//
// For backward compatibility with an earlier implementation error, LengthInt's
// result can disagree with Length's result for any set containing unknown
// values. Length can potentially indicate the set's length is unknown in that
// case, whereas LengthInt will return the maximum possible length as if the
// unknown values were each a placeholder for a value not equal to any other
// value in the set.
func (val Value) LengthInt() int {
val.assertUnmarked()
if val.Type().IsTupleType() {
@ -995,6 +1064,15 @@ func (val Value) LengthInt() int {
return len(val.v.([]interface{}))
case val.ty.IsSetType():
// NOTE: This is technically not correct in cases where the set
// contains unknown values, because in that case we can't know how
// many known values those unknown values are standing in for -- they
// might coalesce with other values once known.
//
// However, this incorrect behavior is preserved for backward
// compatibility with callers that were relying on LengthInt rather
// than calling Length. Instead of panicking when a set contains an
// unknown value, LengthInt returns the largest possible length.
return val.v.(set.Set).Length()
case val.ty.IsMapType():

View File

@ -61,6 +61,34 @@ func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
return nil
}
// Transformer is the interface used to optionally transform values in a
// possibly-complex structure. The Enter method is called before traversing
// through a given path, and the Exit method is called when traversal of a
// path is complete.
//
// Use Enter when you want to transform a complex value before traversal
// (preorder), and Exit when you want to transform a value after traversal
// (postorder).
//
// The path passed to the given function may not be used after that function
// returns, since its backing array is re-used for other calls.
type Transformer interface {
Enter(Path, Value) (Value, error)
Exit(Path, Value) (Value, error)
}
type postorderTransformer struct {
callback func(Path, Value) (Value, error)
}
func (t *postorderTransformer) Enter(p Path, v Value) (Value, error) {
return v, nil
}
func (t *postorderTransformer) Exit(p Path, v Value) (Value, error) {
return t.callback(p, v)
}
// Transform visits all of the values in a possibly-complex structure,
// calling a given function for each value which has an opportunity to
// replace that value.
@ -77,7 +105,7 @@ func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
// value constructor functions. An easy way to preserve invariants is to
// ensure that the transform function never changes the value type.
//
// The callback function my halt the walk altogether by
// The callback function may halt the walk altogether by
// returning a non-nil error. If the returned error is about the element
// currently being visited, it is recommended to use the provided path
// value to produce a PathError describing that context.
@ -86,10 +114,23 @@ func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
// returns, since its backing array is re-used for other calls.
func Transform(val Value, cb func(Path, Value) (Value, error)) (Value, error) {
var path Path
return transform(path, val, cb)
return transform(path, val, &postorderTransformer{cb})
}
func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value, error) {
// TransformWithTransformer allows the caller to more closely control the
// traversal used for transformation. See the documentation for Transformer for
// more details.
func TransformWithTransformer(val Value, t Transformer) (Value, error) {
var path Path
return transform(path, val, t)
}
func transform(path Path, val Value, t Transformer) (Value, error) {
val, err := t.Enter(path, val)
if err != nil {
return DynamicVal, err
}
ty := val.Type()
var newVal Value
@ -112,7 +153,7 @@ func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value
path := append(path, IndexStep{
Key: kv,
})
newEv, err := transform(path, ev, cb)
newEv, err := transform(path, ev, t)
if err != nil {
return DynamicVal, err
}
@ -143,7 +184,7 @@ func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value
path := append(path, IndexStep{
Key: kv,
})
newEv, err := transform(path, ev, cb)
newEv, err := transform(path, ev, t)
if err != nil {
return DynamicVal, err
}
@ -165,7 +206,7 @@ func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value
path := append(path, GetAttrStep{
Name: name,
})
newAV, err := transform(path, av, cb)
newAV, err := transform(path, av, t)
if err != nil {
return DynamicVal, err
}
@ -178,5 +219,9 @@ func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value
newVal = val
}
return cb(path, newVal)
newVal, err = t.Exit(path, newVal)
if err != nil {
return DynamicVal, err
}
return newVal, err
}