core: allow distinguishing between empty lists and strings

Had to handle a lot of implicit leaning on a few properties of the old
representation:

 * Old representation allowed plain strings to be treated as lists
   without problem (i.e. shoved into strings.Split), now strings need to
   be checked whether they are a list before they are treated as one
   (i.e. shoved into StringList(s).Slice()).
 * Tested behavior of 0 and 1 length lists in formatlist() was a side
   effect of the representation. Needs to be special cased now to
   maintain the behavior.
 * Found a pretty old context test failure that was wrong in several
   different ways. It's covered by TestContext2Apply_multiVar so I
   removed it.
This commit is contained in:
Paul Hinze 2015-06-25 17:55:51 -05:00
parent 7238b3b4af
commit e88aeede9b
7 changed files with 79 additions and 71 deletions

View File

@ -56,10 +56,11 @@ func interpolationFuncConcat() ast.Function {
if IsStringList(argument) { if IsStringList(argument) {
isDeprecated = false isDeprecated = false
finalList = append(finalList, StringList(argument).Slice()...)
} else {
finalList = append(finalList, argument)
} }
finalList = append(finalList, argument)
// Deprecated concat behaviour // Deprecated concat behaviour
b.WriteString(argument) b.WriteString(argument)
} }
@ -131,11 +132,26 @@ func interpolationFuncFormatList() ast.Function {
if !ok { if !ok {
continue continue
} }
parts := StringList(s).Slice() if !IsStringList(s) {
if len(parts) <= 1 {
continue continue
} }
parts := StringList(s).Slice()
// 0 or 1 length lists are treated as scalars and repeated
switch len(parts) {
case 0:
varargs[i-1] = ""
continue
case 1:
varargs[i-1] = parts[0]
continue
}
// otherwise the list is sent down to be indexed
varargs[i-1] = parts varargs[i-1] = parts
// Check length
if n == 0 { if n == 0 {
// first list we've seen // first list we've seen
n = len(parts) n = len(parts)

View File

@ -221,7 +221,8 @@ func TestInterpolateFuncJoin(t *testing.T) {
}, },
{ {
`${join(",", "foo")}`, fmt.Sprintf(`${join(",", "%s")}`,
NewStringList([]string{"foo"}).String()),
"foo", "foo",
false, false,
}, },
@ -354,9 +355,15 @@ func TestInterpolateFuncSplit(t *testing.T) {
true, true,
}, },
{
`${split(",", "")}`,
NewStringList([]string{""}).String(),
false,
},
{ {
`${split(",", "foo")}`, `${split(",", "foo")}`,
"foo", NewStringList([]string{"foo"}).String(),
false, false,
}, },
@ -524,7 +531,8 @@ func TestInterpolateFuncElement(t *testing.T) {
}, },
{ {
`${element("foo", "0")}`, fmt.Sprintf(`${element("%s", "0")}`,
NewStringList([]string{"foo"}).String()),
"foo", "foo",
false, false,
}, },

View File

@ -144,7 +144,7 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
// set if it is computed. This behavior is different if we're // set if it is computed. This behavior is different if we're
// splitting (in a SliceElem) or not. // splitting (in a SliceElem) or not.
remove := false remove := false
if w.loc == reflectwalk.SliceElem { if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal) {
parts := StringList(replaceVal).Slice() parts := StringList(replaceVal).Slice()
for _, p := range parts { for _, p := range parts {
if p == UnknownVariableValue { if p == UnknownVariableValue {
@ -265,10 +265,15 @@ func (w *interpolationWalker) splitSlice() {
continue continue
} }
// Split on the delimiter if IsStringList(sv) {
for _, p := range StringList(sv).Slice() { for _, p := range StringList(sv).Slice() {
result = append(result, p) result = append(result, p)
}
continue
} }
// Not a string list, so just set it
result = append(result, sv)
} }
// Our slice is now done, we have to replace the slice now // Our slice is now done, we have to replace the slice now

View File

@ -183,7 +183,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
} }
if !reflect.DeepEqual(tc.Input, tc.Output) { if !reflect.DeepEqual(tc.Input, tc.Output) {
t.Fatalf("%d: bad:\n\n%#v", i, tc.Input) t.Fatalf("%d: bad:\n\nexpected:%#v\ngot:%#v", i, tc.Output, tc.Input)
} }
} }
} }

View File

@ -1,37 +1,48 @@
package config package config
import "strings" import (
"fmt"
"strings"
)
// StringList represents the "poor man's list" that terraform uses // StringList represents the "poor man's list" that terraform uses
// internally // internally
type StringList string type StringList string
// This is the delimiter used to recognize and split StringLists // This is the delimiter used to recognize and split StringLists
const StringListDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6` //
// It plays two semantic roles:
// * It introduces a list
// * It terminates each element
//
// Example representations:
// [] => SLD
// [""] => SLDSLD
// [" "] => SLD SLD
// ["foo"] => SLDfooSLD
// ["foo", "bar"] => SLDfooSLDbarSLD
// ["", ""] => SLDSLDSLD
const stringListDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6`
// Build a StringList from a slice // Build a StringList from a slice
func NewStringList(parts []string) StringList { func NewStringList(parts []string) StringList {
// FOR NOW: // We have to special case the empty list representation
return StringList(strings.Join(parts, StringListDelim)) if len(parts) == 0 {
// EVENTUALLY: return StringList(stringListDelim)
// var sl StringList }
// for _, p := range parts { return StringList(fmt.Sprintf("%s%s%s",
// sl = sl.Append(p) stringListDelim,
// } strings.Join(parts, stringListDelim),
// return sl stringListDelim,
} ))
// Returns a new StringList with the item appended
func (sl StringList) Append(s string) StringList {
// FOR NOW:
return StringList(strings.Join(append(sl.Slice(), s), StringListDelim))
// EVENTUALLY:
// return StringList(fmt.Sprintf("%s%s%s", sl, s, StringListDelim))
} }
// Returns an element at the index, wrapping around the length of the string // Returns an element at the index, wrapping around the length of the string
// when index > list length // when index > list length
func (sl StringList) Element(index int) string { func (sl StringList) Element(index int) string {
if sl.Length() == 0 {
return ""
}
return sl.Slice()[index%sl.Length()] return sl.Slice()[index%sl.Length()]
} }
@ -42,17 +53,17 @@ func (sl StringList) Length() int {
// Returns a slice of strings as represented by this StringList // Returns a slice of strings as represented by this StringList
func (sl StringList) Slice() []string { func (sl StringList) Slice() []string {
parts := strings.Split(string(sl), StringListDelim) parts := strings.Split(string(sl), stringListDelim)
// FOR NOW: switch len(parts) {
if sl.String() == "" { case 0, 1:
return []string{} return []string{}
} else { case 2:
return parts return []string{""}
} }
// EVENTUALLY:
// StringLists always have a trailing StringListDelim // strip empty elements generated by leading and trailing delimiters
// return parts[:len(parts)-1] return parts[1 : len(parts)-1]
} }
func (sl StringList) String() string { func (sl StringList) String() string {
@ -61,5 +72,5 @@ func (sl StringList) String() string {
// Determines if a given string represents a StringList // Determines if a given string represents a StringList
func IsStringList(s string) bool { func IsStringList(s string) bool {
return strings.Contains(s, StringListDelim) return strings.Contains(s, stringListDelim)
} }

View File

@ -1672,29 +1672,6 @@ func TestContext2Plan_provider(t *testing.T) {
} }
} }
func TestContext2Plan_varMultiCountOne(t *testing.T) {
m := testModule(t, "plan-var-multi-count-one")
p := testProvider("aws")
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanVarMultiCountOneStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
func TestContext2Plan_varListErr(t *testing.T) { func TestContext2Plan_varListErr(t *testing.T) {
m := testModule(t, "plan-var-list-err") m := testModule(t, "plan-var-list-err")
p := testProvider("aws") p := testProvider("aws")

View File

@ -1,9 +0,0 @@
resource "aws_instance" "foo" {
num = "2"
count = 1
}
resource "aws_instance" "bar" {
foo = "${aws_instance.foo.*.num}"
}