Skip to content
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

Merge, flatten, length, lookup functions: more precise handling of marks #98

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 70 additions & 32 deletions cty/function/stdlib/collection.go
Expand Up @@ -111,6 +111,7 @@ var LengthFunc = function.New(&function.Spec{
Name: "collection",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
Expand Down Expand Up @@ -470,8 +471,9 @@ var ChunklistFunc = function.New(&function.Spec{
var FlattenFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
Name: "list",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
Expand All @@ -484,7 +486,8 @@ var FlattenFunc = function.New(&function.Spec{
return cty.NilType, errors.New("can only flatten lists, sets and tuples")
}

retVal, known := flattener(args[0])
// marks are attached to values, so ignore while determining type
retVal, _, known := flattener(args[0])
if !known {
return cty.DynamicPseudoType, nil
}
Expand All @@ -497,46 +500,58 @@ var FlattenFunc = function.New(&function.Spec{
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList := args[0]
if inputList.LengthInt() == 0 {
return cty.EmptyTupleVal, nil

if unmarked, marks := inputList.Unmark(); unmarked.LengthInt() == 0 {
return cty.EmptyTupleVal.WithMarks(marks), nil
}

out, known := flattener(inputList)
out, markses, known := flattener(inputList)
if !known {
return cty.UnknownVal(retType), nil
return cty.UnknownVal(retType).WithMarks(markses...), nil
}

return cty.TupleVal(out), nil
return cty.TupleVal(out).WithMarks(markses...), nil
},
})

// Flatten until it's not a cty.List, and return whether the value is known.
// We can flatten lists with unknown values, as long as they are not
// lists themselves.
func flattener(flattenList cty.Value) ([]cty.Value, bool) {
func flattener(flattenList cty.Value) ([]cty.Value, []cty.ValueMarks, bool) {
var markses []cty.ValueMarks
flattenList, flattenListMarks := flattenList.Unmark()
if len(flattenListMarks) > 0 {
markses = append(markses, flattenListMarks)
}
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
return nil, markses, false
}
out := make([]cty.Value, 0)
isKnown := true
for it := flattenList.ElementIterator(); it.Next(); {
_, val := it.Element()
if val.Type().IsListType() || val.Type().IsSetType() || val.Type().IsTupleType() {
if !val.IsKnown() {
return out, false
isKnown = false
_, unknownMarks := val.Unmark()
markses = append(markses, unknownMarks)
continue
}

res, known := flattener(val)
if !known {
return res, known
res, resMarks, known := flattener(val)
markses = append(markses, resMarks...)
if known {
out = append(out, res...)
} else {
isKnown = false
}
out = append(out, res...)
} else {
out = append(out, val)
}
}
return out, true
return out, markses, isKnown
}

// KeysFunc is a function that takes a map and returns a sorted list of the map keys.
Expand Down Expand Up @@ -618,16 +633,19 @@ var KeysFunc = function.New(&function.Spec{
var LookupFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "inputMap",
Type: cty.DynamicPseudoType,
Name: "inputMap",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
{
Name: "key",
Type: cty.String,
Name: "key",
Type: cty.String,
AllowMarked: true,
},
{
Name: "default",
Type: cty.DynamicPseudoType,
Name: "default",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
Expand All @@ -639,7 +657,8 @@ var LookupFunc = function.New(&function.Spec{
return cty.DynamicPseudoType, nil
}

key := args[1].AsString()
keyVal, _ := args[1].Unmark()
key := keyVal.AsString()
if ty.HasAttribute(key) {
return args[0].GetAttr(key).Type(), nil
} else if len(args) == 3 {
Expand All @@ -661,28 +680,39 @@ var LookupFunc = function.New(&function.Spec{
}
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// leave default value marked
defaultVal := args[2]

mapVar := args[0]
lookupKey := args[1].AsString()
var markses []cty.ValueMarks

// unmark collection, retain marks to reapply later
mapVar, mapMarks := args[0].Unmark()
markses = append(markses, mapMarks)

// include marks on the key in the result
keyVal, keyMarks := args[1].Unmark()
if len(keyMarks) > 0 {
markses = append(markses, keyMarks)
}
lookupKey := keyVal.AsString()

if !mapVar.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
return cty.UnknownVal(retType).WithMarks(markses...), nil
}

if mapVar.Type().IsObjectType() {
if mapVar.Type().HasAttribute(lookupKey) {
return mapVar.GetAttr(lookupKey), nil
return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil
}
} else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True {
return mapVar.Index(cty.StringVal(lookupKey)), nil
return mapVar.Index(cty.StringVal(lookupKey)).WithMarks(markses...), nil
}

defaultVal, err = convert.Convert(defaultVal, retType)
if err != nil {
return cty.NilVal, err
}
return defaultVal, nil
return defaultVal.WithMarks(markses...), nil
},
})

Expand All @@ -699,6 +729,7 @@ var MergeFunc = function.New(&function.Spec{
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowNull: true,
AllowMarked: true,
},
Type: func(args []cty.Value) (cty.Type, error) {
// empty args is accepted, so assume an empty object since we have no
Expand All @@ -724,6 +755,8 @@ var MergeFunc = function.New(&function.Spec{
if !ty.IsMapType() && !ty.IsObjectType() {
return cty.NilType, fmt.Errorf("arguments must be maps or objects, got %#v", ty.FriendlyName())
}
// marks are attached to values, so ignore while determining type
arg, _ = arg.Unmark()

switch {
case ty.IsObjectType() && !arg.IsNull():
Expand Down Expand Up @@ -773,11 +806,16 @@ 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)
var markses []cty.ValueMarks // remember any marked maps/objects we find

for _, arg := range args {
if arg.IsNull() {
continue
}
arg, argMarks := arg.Unmark()
if len(argMarks) > 0 {
markses = append(markses, argMarks)
}
for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element()
outputMap[k.AsString()] = v
Expand All @@ -787,11 +825,11 @@ var MergeFunc = function.New(&function.Spec{
switch {
case retType.IsMapType():
if len(outputMap) == 0 {
return cty.MapValEmpty(retType.ElementType()), nil
return cty.MapValEmpty(retType.ElementType()).WithMarks(markses...), nil
}
return cty.MapVal(outputMap), nil
return cty.MapVal(outputMap).WithMarks(markses...), nil
case retType.IsObjectType(), retType.Equals(cty.DynamicPseudoType):
return cty.ObjectVal(outputMap), nil
return cty.ObjectVal(outputMap).WithMarks(markses...), nil
default:
panic(fmt.Sprintf("unexpected return type: %#v", retType))
}
Expand Down