vendor: upgrade github.com/zclconf/go-cty

This includes:
- An additional check in the format stdlib function to fail if there are
  too many arguments given, rather than silently ignoring.
- Refinements for the type unification behavior to allow unification of
  object/tuple types into weaker map/list types when no other unification
  is possible.
- Improvements to the error messages for failed type conversions on
  collection and structural types to talk about mismatching element types
  where possible, rather than the outer value.
This commit is contained in:
Martin Atkins 2018-12-18 15:15:50 -08:00
parent 149ccd929e
commit 65c0826293
10 changed files with 481 additions and 39 deletions

2
go.mod
View File

@ -128,7 +128,7 @@ require (
github.com/xanzy/ssh-agent v0.2.0
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8
github.com/zclconf/go-cty v0.0.0-20181218225846-4fe1e489ee06
go.opencensus.io v0.17.0 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect

2
go.sum
View File

@ -324,6 +324,8 @@ github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 h1:Jpn2j6wHkC9wJv5i
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8 h1:imQXpaIqhN70pkpZ/a4ZMCiIi9CpaYE34f5/CAKWRs8=
github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v0.0.0-20181218225846-4fe1e489ee06 h1:J3bfEicd/d85VHC6bPhrKb+2jO+Uquiy2bnkhia6XBA=
github.com/zclconf/go-cty v0.0.0-20181218225846-4fe1e489ee06/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
go.opencensus.io v0.17.0 h1:2Cu88MYg+1LU+WVD+NWwYhyP0kKgRlN9QjWGaX0jKTE=
go.opencensus.io v0.17.0/go.mod h1:mp1VrMQxhlqqDpKvH4UcQUa4YwlzNmymAjPrDdfxNpI=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=

View File

@ -17,15 +17,14 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
// Wrap the conversion in some standard checks that we don't want to
// have to repeat in every conversion function.
return func(in cty.Value, path cty.Path) (cty.Value, error) {
if out == cty.DynamicPseudoType {
// Conversion to DynamicPseudoType always just passes through verbatim.
return in, nil
}
if !in.IsKnown() {
return cty.UnknownVal(out), nil
}
if in.IsNull() {
// If the output is a DynamicPseudoType, the type doesn't matter,
// so retain the type for proper null comparison.
if out == cty.DynamicPseudoType {
return in, nil
}
// We'll pass through nulls, albeit type converted, and let
// the caller deal with whatever handling they want to do in
// case null values are considered valid in some applications.
@ -65,6 +64,9 @@ func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion {
case out.IsObjectType() && in.IsObjectType():
return conversionObjectToObject(in, out, unsafe)
case out.IsTupleType() && in.IsTupleType():
return conversionTupleToTuple(in, out, unsafe)
case out.IsListType() && (in.IsListType() || in.IsSetType()):
inEty := in.ElementType()
outEty := out.ElementType()

View File

@ -0,0 +1,71 @@
package convert
import (
"github.com/zclconf/go-cty/cty"
)
// conversionTupleToTuple returns a conversion that will make the input
// tuple type conform to the output tuple type, if possible.
//
// Conversion is possible only if the two tuple types have the same number
// of elements and the corresponding elements by index can be converted.
//
// Shallow tuple conversions work the same for both safe and unsafe modes,
// but the safety flag is passed on to recursive conversions and may thus
// limit which element type conversions are possible.
func conversionTupleToTuple(in, out cty.Type, unsafe bool) conversion {
inEtys := in.TupleElementTypes()
outEtys := out.TupleElementTypes()
if len(inEtys) != len(outEtys) {
return nil // no conversion is possible
}
elemConvs := make([]conversion, len(inEtys))
for i, outEty := range outEtys {
inEty := inEtys[i]
if inEty.Equals(outEty) {
// No conversion needed, so we can leave this one nil.
continue
}
elemConvs[i] = getConversion(inEty, outEty, unsafe)
if elemConvs[i] == nil {
// If a recursive conversion isn't available, then our top-level
// configuration is impossible too.
return nil
}
}
// If we get here then a conversion is possible, using the element
// conversions given in elemConvs.
return func(val cty.Value, path cty.Path) (cty.Value, error) {
elemVals := make([]cty.Value, len(elemConvs))
path = append(path, nil)
pathStep := &path[len(path)-1]
i := 0
for it := val.ElementIterator(); it.Next(); i++ {
_, val := it.Element()
var err error
*pathStep = cty.IndexStep{
Key: cty.NumberIntVal(int64(i)),
}
conv := elemConvs[i]
if conv != nil {
val, err = conv(val, path)
if err != nil {
return cty.NilVal, err
}
}
elemVals[i] = val
}
return cty.TupleVal(elemVals), nil
}
}

View File

@ -47,6 +47,12 @@ func MismatchMessage(got, want cty.Type) string {
// find a common type to convert all of the object attributes to.
return "all map elements must have the same type"
case (got.IsTupleType() || got.IsObjectType()) && want.IsCollectionType():
return mismatchMessageCollectionsFromStructural(got, want)
case got.IsCollectionType() && want.IsCollectionType():
return mismatchMessageCollectionsFromCollections(got, want)
default:
// If we have nothing better to say, we'll just state what was required.
return want.FriendlyNameForConstraint() + " required"
@ -106,6 +112,7 @@ func mismatchMessageObjects(got, want cty.Type) string {
switch {
case len(missingAttrs) != 0:
sort.Strings(missingAttrs)
switch len(missingAttrs) {
case 1:
return fmt.Sprintf("attribute %q is required", missingAttrs[0])
@ -133,3 +140,81 @@ func mismatchMessageObjects(got, want cty.Type) string {
return "incorrect object attributes"
}
}
func mismatchMessageCollectionsFromStructural(got, want cty.Type) string {
// First some straightforward cases where the kind is just altogether wrong.
switch {
case want.IsListType() && !got.IsTupleType():
return want.FriendlyNameForConstraint() + " required"
case want.IsSetType() && !got.IsTupleType():
return want.FriendlyNameForConstraint() + " required"
case want.IsMapType() && !got.IsObjectType():
return want.FriendlyNameForConstraint() + " required"
}
// If the kinds are matched well enough then we'll move on to checking
// individual elements.
wantEty := want.ElementType()
switch {
case got.IsTupleType():
for i, gotEty := range got.TupleElementTypes() {
if gotEty.Equals(wantEty) {
continue // exact match, so no problem
}
if conv := getConversion(gotEty, wantEty, true); conv != nil {
continue // conversion is available, so no problem
}
return fmt.Sprintf("element %d: %s", i, MismatchMessage(gotEty, wantEty))
}
// If we get down here then something weird is going on but we'll
// return a reasonable fallback message anyway.
return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint())
case got.IsObjectType():
for name, gotAty := range got.AttributeTypes() {
if gotAty.Equals(wantEty) {
continue // exact match, so no problem
}
if conv := getConversion(gotAty, wantEty, true); conv != nil {
continue // conversion is available, so no problem
}
return fmt.Sprintf("element %q: %s", name, MismatchMessage(gotAty, wantEty))
}
// If we get down here then something weird is going on but we'll
// return a reasonable fallback message anyway.
return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint())
default:
// Should not be possible to get here since we only call this function
// with got as structural types, but...
return want.FriendlyNameForConstraint() + " required"
}
}
func mismatchMessageCollectionsFromCollections(got, want cty.Type) string {
// First some straightforward cases where the kind is just altogether wrong.
switch {
case want.IsListType() && !(got.IsListType() || got.IsSetType()):
return want.FriendlyNameForConstraint() + " required"
case want.IsSetType() && !(got.IsListType() || got.IsSetType()):
return want.FriendlyNameForConstraint() + " required"
case want.IsMapType() && !got.IsMapType():
return want.FriendlyNameForConstraint() + " required"
}
// If the kinds are matched well enough then we'll check the element types.
gotEty := got.ElementType()
wantEty := want.ElementType()
noun := "element type"
switch {
case want.IsListType():
noun = "list element type"
case want.IsSetType():
noun = "set element type"
case want.IsMapType():
noun = "map element type"
}
return fmt.Sprintf("incorrect %s: %s", noun, MismatchMessage(gotEty, wantEty))
}

View File

@ -21,6 +21,39 @@ func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
return cty.NilType, nil
}
// If all of the given types are of the same structural kind, we may be
// able to construct a new type that they can all be unified to, even if
// that is not one of the given types. We must try this before the general
// behavior below because in unsafe mode we can convert an object type to
// a subset of that type, which would be a much less useful conversion for
// unification purposes.
{
objectCt := 0
tupleCt := 0
dynamicCt := 0
for _, ty := range types {
switch {
case ty.IsObjectType():
objectCt++
case ty.IsTupleType():
tupleCt++
case ty == cty.DynamicPseudoType:
dynamicCt++
default:
break
}
}
switch {
case objectCt > 0 && (objectCt+dynamicCt) == len(types):
return unifyObjectTypes(types, unsafe, dynamicCt > 0)
case tupleCt > 0 && (tupleCt+dynamicCt) == len(types):
return unifyTupleTypes(types, unsafe, dynamicCt > 0)
case objectCt > 0 && tupleCt > 0:
// Can never unify object and tuple types since they have incompatible kinds
return cty.NilType, nil
}
}
prefOrder := sortTypes(types)
// sortTypes gives us an order where earlier items are preferable as
@ -58,9 +91,225 @@ Preferences:
return wantType, conversions
}
// TODO: For structural types, try to invent a new type that they
// can all be unified to, by unifying their respective attributes.
// If we fall out here, no unification is possible
return cty.NilType, nil
}
func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
// If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so
// we'll conservatively produce DynamicVal for these.
if hasDynamic {
return unifyAllAsDynamic(types)
}
// There are two different ways we can succeed here:
// - If all of the given object types have the same set of attribute names
// and the corresponding types are all unifyable, then we construct that
// type.
// - If the given object types have different attribute names or their
// corresponding types are not unifyable, we'll instead try to unify
// all of the attribute types together to produce a map type.
//
// Our unification behavior is intentionally stricter than our conversion
// behavior for subset object types because user intent is different with
// unification use-cases: it makes sense to allow {"foo":true} to convert
// to emptyobjectval, but unifying an object with an attribute with the
// empty object type should be an error because unifying to the empty
// object type would be suprising and useless.
firstAttrs := types[0].AttributeTypes()
for _, ty := range types[1:] {
thisAttrs := ty.AttributeTypes()
if len(thisAttrs) != len(firstAttrs) {
// If number of attributes is different then there can be no
// object type in common.
return unifyObjectTypesToMap(types, unsafe)
}
for name := range thisAttrs {
if _, ok := firstAttrs[name]; !ok {
// If attribute names don't exactly match then there can be
// no object type in common.
return unifyObjectTypesToMap(types, unsafe)
}
}
}
// If we get here then we've proven that all of the given object types
// have exactly the same set of attribute names, though the types may
// differ.
retAtys := make(map[string]cty.Type)
atysAcross := make([]cty.Type, len(types))
for name := range firstAttrs {
for i, ty := range types {
atysAcross[i] = ty.AttributeType(name)
}
retAtys[name], _ = unify(atysAcross, unsafe)
if retAtys[name] == cty.NilType {
// Cannot unify this attribute alone, which means that unification
// of everything down to a map type can't be possible either.
return cty.NilType, nil
}
}
retTy := cty.Object(retAtys)
conversions := make([]Conversion, len(types))
for i, ty := range types {
if ty.Equals(retTy) {
continue
}
if unsafe {
conversions[i] = GetConversionUnsafe(ty, retTy)
} else {
conversions[i] = GetConversion(ty, retTy)
}
if conversions[i] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap(types, unsafe)
}
}
return retTy, conversions
}
func unifyObjectTypesToMap(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
// This is our fallback case for unifyObjectTypes, where we see if we can
// construct a map type that can accept all of the attribute types.
var atys []cty.Type
for _, ty := range types {
for _, aty := range ty.AttributeTypes() {
atys = append(atys, aty)
}
}
ety, _ := unify(atys, unsafe)
if ety == cty.NilType {
return cty.NilType, nil
}
retTy := cty.Map(ety)
conversions := make([]Conversion, len(types))
for i, ty := range types {
if ty.Equals(retTy) {
continue
}
if unsafe {
conversions[i] = GetConversionUnsafe(ty, retTy)
} else {
conversions[i] = GetConversion(ty, retTy)
}
if conversions[i] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap(types, unsafe)
}
}
return retTy, conversions
}
func unifyTupleTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
// If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so
// we'll conservatively produce DynamicVal for these.
if hasDynamic {
return unifyAllAsDynamic(types)
}
// There are two different ways we can succeed here:
// - If all of the given tuple types have the same sequence of element types
// and the corresponding types are all unifyable, then we construct that
// type.
// - If the given tuple types have different element types or their
// corresponding types are not unifyable, we'll instead try to unify
// all of the elements types together to produce a list type.
firstEtys := types[0].TupleElementTypes()
for _, ty := range types[1:] {
thisEtys := ty.TupleElementTypes()
if len(thisEtys) != len(firstEtys) {
// If number of elements is different then there can be no
// tuple type in common.
return unifyTupleTypesToList(types, unsafe)
}
}
// If we get here then we've proven that all of the given tuple types
// have the same number of elements, though the types may differ.
retEtys := make([]cty.Type, len(firstEtys))
atysAcross := make([]cty.Type, len(types))
for idx := range firstEtys {
for tyI, ty := range types {
atysAcross[tyI] = ty.TupleElementTypes()[idx]
}
retEtys[idx], _ = unify(atysAcross, unsafe)
if retEtys[idx] == cty.NilType {
// Cannot unify this element alone, which means that unification
// of everything down to a map type can't be possible either.
return cty.NilType, nil
}
}
retTy := cty.Tuple(retEtys)
conversions := make([]Conversion, len(types))
for i, ty := range types {
if ty.Equals(retTy) {
continue
}
if unsafe {
conversions[i] = GetConversionUnsafe(ty, retTy)
} else {
conversions[i] = GetConversion(ty, retTy)
}
if conversions[i] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyTupleTypesToList(types, unsafe)
}
}
return retTy, conversions
}
func unifyTupleTypesToList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
// This is our fallback case for unifyTupleTypes, where we see if we can
// construct a list type that can accept all of the element types.
var etys []cty.Type
for _, ty := range types {
for _, ety := range ty.TupleElementTypes() {
etys = append(etys, ety)
}
}
ety, _ := unify(etys, unsafe)
if ety == cty.NilType {
return cty.NilType, nil
}
retTy := cty.List(ety)
conversions := make([]Conversion, len(types))
for i, ty := range types {
if ty.Equals(retTy) {
continue
}
if unsafe {
conversions[i] = GetConversionUnsafe(ty, retTy)
} else {
conversions[i] = GetConversion(ty, retTy)
}
if conversions[i] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap(types, unsafe)
}
}
return retTy, conversions
}
func unifyAllAsDynamic(types []cty.Type) (cty.Type, []Conversion) {
conversions := make([]Conversion, len(types))
for i := range conversions {
conversions[i] = func(cty.Value) (cty.Value, error) {
return cty.DynamicVal, nil
}
}
return cty.DynamicPseudoType, conversions
}

View File

@ -11,9 +11,10 @@ import (
"unicode/utf8"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// line 20 "format_fsm.go"
// line 21 "format_fsm.go"
var _formatfsm_actions []byte = []byte{
0, 1, 0, 1, 1, 1, 2, 1, 4,
1, 5, 1, 6, 1, 7, 1, 8,
@ -86,15 +87,16 @@ const formatfsm_error int = 0
const formatfsm_en_main int = 8
// line 19 "format_fsm.rl"
// line 20 "format_fsm.rl"
func formatFSM(format string, a []cty.Value) (string, error) {
var buf bytes.Buffer
data := format
nextArg := 1 // arg numbers are 1-based
var verb formatVerb
highestArgIdx := 0 // zero means "none", since arg numbers are 1-based
// line 153 "format_fsm.rl"
// line 159 "format_fsm.rl"
// Ragel state
p := 0 // "Pointer" into data
@ -109,12 +111,12 @@ func formatFSM(format string, a []cty.Value) (string, error) {
_ = te
_ = eof
// line 121 "format_fsm.go"
// line 123 "format_fsm.go"
{
cs = formatfsm_start
}
// line 126 "format_fsm.go"
// line 128 "format_fsm.go"
{
var _klen int
var _trans int
@ -195,7 +197,7 @@ func formatFSM(format string, a []cty.Value) (string, error) {
_acts++
switch _formatfsm_actions[_acts-1] {
case 0:
// line 29 "format_fsm.rl"
// line 31 "format_fsm.rl"
verb = formatVerb{
ArgNum: nextArg,
@ -205,12 +207,12 @@ func formatFSM(format string, a []cty.Value) (string, error) {
ts = p
case 1:
// line 38 "format_fsm.rl"
// line 40 "format_fsm.rl"
buf.WriteByte(data[p])
case 4:
// line 49 "format_fsm.rl"
// line 51 "format_fsm.rl"
// We'll try to slurp a whole UTF-8 sequence here, to give the user
// better feedback.
@ -218,85 +220,89 @@ func formatFSM(format string, a []cty.Value) (string, error) {
return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p)
case 5:
// line 56 "format_fsm.rl"
// line 58 "format_fsm.rl"
verb.Sharp = true
case 6:
// line 59 "format_fsm.rl"
// line 61 "format_fsm.rl"
verb.Zero = true
case 7:
// line 62 "format_fsm.rl"
// line 64 "format_fsm.rl"
verb.Minus = true
case 8:
// line 65 "format_fsm.rl"
// line 67 "format_fsm.rl"
verb.Plus = true
case 9:
// line 68 "format_fsm.rl"
// line 70 "format_fsm.rl"
verb.Space = true
case 10:
// line 72 "format_fsm.rl"
// line 74 "format_fsm.rl"
verb.ArgNum = 0
case 11:
// line 75 "format_fsm.rl"
// line 77 "format_fsm.rl"
verb.ArgNum = (10 * verb.ArgNum) + (int(data[p]) - '0')
case 12:
// line 79 "format_fsm.rl"
// line 81 "format_fsm.rl"
verb.HasWidth = true
case 13:
// line 82 "format_fsm.rl"
// line 84 "format_fsm.rl"
verb.Width = 0
case 14:
// line 85 "format_fsm.rl"
// line 87 "format_fsm.rl"
verb.Width = (10 * verb.Width) + (int(data[p]) - '0')
case 15:
// line 89 "format_fsm.rl"
// line 91 "format_fsm.rl"
verb.HasPrec = true
case 16:
// line 92 "format_fsm.rl"
// line 94 "format_fsm.rl"
verb.Prec = 0
case 17:
// line 95 "format_fsm.rl"
// line 97 "format_fsm.rl"
verb.Prec = (10 * verb.Prec) + (int(data[p]) - '0')
case 18:
// line 99 "format_fsm.rl"
// line 101 "format_fsm.rl"
verb.Mode = rune(data[p])
te = p + 1
verb.Raw = data[ts:te]
verb.Offset = ts
if verb.ArgNum > highestArgIdx {
highestArgIdx = verb.ArgNum
}
err := formatAppend(&verb, &buf, a)
if err != nil {
return buf.String(), err
}
nextArg = verb.ArgNum + 1
// line 324 "format_fsm.go"
// line 330 "format_fsm.go"
}
}
@ -319,22 +325,22 @@ func formatFSM(format string, a []cty.Value) (string, error) {
__acts++
switch _formatfsm_actions[__acts-1] {
case 2:
// line 42 "format_fsm.rl"
// line 44 "format_fsm.rl"
case 3:
// line 45 "format_fsm.rl"
// line 47 "format_fsm.rl"
return buf.String(), fmt.Errorf("invalid format string starting at offset %d", p)
case 4:
// line 49 "format_fsm.rl"
// line 51 "format_fsm.rl"
// We'll try to slurp a whole UTF-8 sequence here, to give the user
// better feedback.
r, _ := utf8.DecodeRuneInString(data[p:])
return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p)
// line 363 "format_fsm.go"
// line 369 "format_fsm.go"
}
}
}
@ -344,7 +350,7 @@ func formatFSM(format string, a []cty.Value) (string, error) {
}
}
// line 171 "format_fsm.rl"
// line 177 "format_fsm.rl"
// If we fall out here without being in a final state then we've
// encountered something that the scanner can't match, which should
@ -354,5 +360,15 @@ func formatFSM(format string, a []cty.Value) (string, error) {
return buf.String(), fmt.Errorf("extraneous characters beginning at offset %d", p)
}
if highestArgIdx < len(a) {
// Extraneous args are an error, to more easily detect mistakes
firstBad := highestArgIdx + 1
if highestArgIdx == 0 {
// Custom error message for this case
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; no verbs in format string")
}
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; only %d used by format string", highestArgIdx)
}
return buf.String(), nil
}

View File

@ -12,6 +12,7 @@ import (
"unicode/utf8"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
%%{
@ -23,6 +24,7 @@ func formatFSM(format string, a []cty.Value) (string, error) {
data := format
nextArg := 1 // arg numbers are 1-based
var verb formatVerb
highestArgIdx := 0 // zero means "none", since arg numbers are 1-based
%%{
@ -102,6 +104,10 @@ func formatFSM(format string, a []cty.Value) (string, error) {
verb.Raw = data[ts:te]
verb.Offset = ts
if verb.ArgNum > highestArgIdx {
highestArgIdx = verb.ArgNum
}
err := formatAppend(&verb, &buf, a)
if err != nil {
return buf.String(), err
@ -178,5 +184,15 @@ func formatFSM(format string, a []cty.Value) (string, error) {
return buf.String(), fmt.Errorf("extraneous characters beginning at offset %d", p)
}
if highestArgIdx < len(a) {
// Extraneous args are an error, to more easily detect mistakes
firstBad := highestArgIdx+1
if highestArgIdx == 0 {
// Custom error message for this case
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; no verbs in format string")
}
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; only %d used by format string", highestArgIdx)
}
return buf.String(), nil
}

View File

@ -276,7 +276,8 @@ func unmarshalObject(dec *msgpack.Decoder, atys map[string]cty.Type, path cty.Pa
case length == 0:
return cty.ObjectVal(nil), nil
case length != len(atys):
return cty.DynamicVal, path.NewErrorf("an object with %d attributes is required", len(atys))
return cty.DynamicVal, path.NewErrorf("an object with %d attributes is required (%d given)",
len(atys), length)
}
vals := make(map[string]cty.Value, length)

2
vendor/modules.txt vendored
View File

@ -480,7 +480,7 @@ github.com/vmihailenco/msgpack/codes
github.com/xanzy/ssh-agent
# github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/xlab/treeprint
# github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8
# github.com/zclconf/go-cty v0.0.0-20181218225846-4fe1e489ee06
github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/gocty
github.com/zclconf/go-cty/cty/convert