diff --git a/examples/app.go b/examples/app.go index 142dc8f..5e01fca 100644 --- a/examples/app.go +++ b/examples/app.go @@ -38,10 +38,10 @@ func makeTemplate() *cf.Template { LoadBalancerPort: cf.String("443"), Protocol: cf.String("SSL"), SSLCertificateId: cf.Join("", - *cf.String("arn:aws:iam::"), - *cf.Ref("AWS::AccountID").String(), - *cf.String(":server-certificate/"), - *cf.Ref("DnsName").String()).String(), + cf.String("arn:aws:iam::"), + cf.Ref("AWS::AccountID"), + cf.String(":server-certificate/"), + cf.Ref("DnsName")), }, }, Policies: &cf.ElasticLoadBalancingPolicyList{ @@ -58,11 +58,11 @@ func makeTemplate() *cf.Template { }, }, Subnets: cf.StringList( - *cf.Ref("VpcSubnetA").String(), - *cf.Ref("VpcSubnetB").String(), - *cf.Ref("VpcSubnetC").String(), + cf.Ref("VpcSubnetA"), + cf.Ref("VpcSubnetB"), + cf.Ref("VpcSubnetC"), ), - SecurityGroups: cf.StringList(*cf.Ref("LoadBalancerSecurityGroup").String()), + SecurityGroups: cf.StringList(cf.Ref("LoadBalancerSecurityGroup")), }) return t diff --git a/examples/app_old.go b/examples/app_old.go new file mode 100644 index 0000000..e1a291e --- /dev/null +++ b/examples/app_old.go @@ -0,0 +1,66 @@ +// This program emits a cloudformation document for `app` to stdout +package main + +import cf "github.com/crewjam/go-cloudformation" + +// makeTemplateTheOldWay is an implementation of makeTemplate that uses the +// older pre-Stringable syntax. If this file builds, then maybe we haven't broken +// backcompat. +func makeTemplateTheOldWay() *cf.Template { + t := cf.NewTemplate() + t.Description = "example production infrastructure" + t.Parameters["DnsName"] = cf.Parameter{ + Description: "The top level DNS name for the infrastructure", + Type: "String", + Default: "preview.example.io", + } + + t.AddResource("ServerLoadBalancer", cf.ElasticLoadBalancingLoadBalancer{ + ConnectionDrainingPolicy: &cf.ElasticLoadBalancingConnectionDrainingPolicy{ + Enabled: cf.Bool(true), + Timeout: cf.Integer(30), + }, + CrossZone: cf.Bool(true), + HealthCheck: &cf.ElasticLoadBalancingHealthCheck{ + HealthyThreshold: cf.String("2"), + Interval: cf.String("60"), + Target: cf.String("HTTP:80/"), + Timeout: cf.String("5"), + UnhealthyThreshold: cf.String("2"), + }, + Listeners: &cf.ElasticLoadBalancingListenerList{ + cf.ElasticLoadBalancingListener{ + InstancePort: cf.String("8000"), + InstanceProtocol: cf.String("TCP"), + LoadBalancerPort: cf.String("443"), + Protocol: cf.String("SSL"), + SSLCertificateId: cf.Join("", + *cf.String("arn:aws:iam::"), + *cf.Ref("AWS::AccountID").String(), + *cf.String(":server-certificate/"), + *cf.Ref("DnsName").String()).String(), + }, + }, + Policies: &cf.ElasticLoadBalancingPolicyList{ + cf.ElasticLoadBalancingPolicy{ + PolicyName: cf.String("EnableProxyProtocol"), + PolicyType: cf.String("ProxyProtocolPolicyType"), + Attributes: []map[string]interface{}{ + map[string]interface{}{ + "Name": "ProxyProtocol", + "Value": "true", + }, + }, + InstancePorts: []int{8000}, + }, + }, + Subnets: cf.StringList( + *cf.Ref("VpcSubnetA").String(), + *cf.Ref("VpcSubnetB").String(), + *cf.Ref("VpcSubnetC").String(), + ), + SecurityGroups: cf.StringList(*cf.Ref("LoadBalancerSecurityGroup").String()), + }) + + return t +} diff --git a/func_base64.go b/func_base64.go index f9b5249..d6f53b3 100644 --- a/func_base64.go +++ b/func_base64.go @@ -1,8 +1,8 @@ package cloudformation // Base64 represents the Fn::Base64 function called over value. -func Base64(value StringExpr) Base64Func { - return Base64Func{Value: value} +func Base64(value Stringable) *StringExpr { + return Base64Func{Value: *value.String()}.String() } // Base64Func represents an invocation of Fn::Base64. @@ -20,4 +20,5 @@ func (f Base64Func) String() *StringExpr { return &StringExpr{Func: f} } +var _ Stringable = Base64Func{} // Base64Func must implement Stringable var _ StringFunc = Base64Func{} // Base64Func must implement StringFunc diff --git a/func_base64_test.go b/func_base64_test.go index 8a74f40..932c1eb 100644 --- a/func_base64_test.go +++ b/func_base64_test.go @@ -29,6 +29,22 @@ func (testSuite *Base64FuncTest) TestRef(c *C) { ]]}}` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(Stringable).String(), DeepEquals, Base64(Join("", + String("#!/bin/bash -xe\n"), + String("yum update -y aws-cfn-bootstrap\n"), + String("# Install the files and packages from the metadata\n"), + String("/opt/aws/bin/cfn-init -v "), + String(" --stack "), Ref("AWS::StackName").String(), + String(" --resource LaunchConfig "), + String(" --region "), Ref("AWS::Region").String(), String("\n"), + String("# Signal the status from cfn-init\n"), + String("/opt/aws/bin/cfn-signal -e $? "), + String(" --stack "), Ref("AWS::StackName").String(), + String(" --resource WebServerGroup "), + String(" --region "), Ref("AWS::Region").String(), String("\n"), + ))) + + // old way still compiles c.Assert(f.(StringFunc).String(), DeepEquals, Base64(*Join("", *String("#!/bin/bash -xe\n"), *String("yum update -y aws-cfn-bootstrap\n"), diff --git a/func_findinmap.go b/func_findinmap.go index 9880b71..70754a2 100644 --- a/func_findinmap.go +++ b/func_findinmap.go @@ -3,12 +3,12 @@ package cloudformation import "encoding/json" // FindInMap returns a new instance of FindInMapFunc. -func FindInMap(mapName string, topLevelKey StringExpr, secondLevelKey StringExpr) FindInMapFunc { +func FindInMap(mapName string, topLevelKey Stringable, secondLevelKey Stringable) *StringExpr { return FindInMapFunc{ MapName: mapName, - TopLevelKey: topLevelKey, - SecondLevelKey: secondLevelKey, - } + TopLevelKey: *topLevelKey.String(), + SecondLevelKey: *secondLevelKey.String(), + }.String() } // FindInMapFunc represents an invocation of the Fn::FindInMap intrinsic. @@ -55,4 +55,5 @@ func (f FindInMapFunc) String() *StringExpr { return &StringExpr{Func: f} } +var _ Stringable = FindInMapFunc{} // FindInMapFunc must implement Stringable var _ StringFunc = FindInMapFunc{} // FindInMapFunc must implement StringFunc diff --git a/func_findinmap_test.go b/func_findinmap_test.go index 8d065c5..7530310 100644 --- a/func_findinmap_test.go +++ b/func_findinmap_test.go @@ -15,6 +15,12 @@ func (testSuite *FindInMapFuncTest) TestBasics(c *C) { { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] }` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(StringFunc).String(), DeepEquals, FindInMap( + "AWSRegionArch2AMI", Ref("AWS::Region"), + FindInMap("AWSInstanceType2Arch", Ref("InstanceType"), + String("Arch")))) + + // old way c.Assert(f.(StringFunc).String(), DeepEquals, FindInMap( "AWSRegionArch2AMI", *Ref("AWS::Region").String(), *FindInMap("AWSInstanceType2Arch", *Ref("InstanceType").String(), diff --git a/func_getatt.go b/func_getatt.go index dd6188b..dfa01cf 100644 --- a/func_getatt.go +++ b/func_getatt.go @@ -3,8 +3,8 @@ package cloudformation import "encoding/json" // GetAtt returns a new instance of GetAttFunc. -func GetAtt(resource, name string) GetAttFunc { - return GetAttFunc{Resource: resource, Name: name} +func GetAtt(resource, name string) *StringExpr { + return GetAttFunc{Resource: resource, Name: name}.String() } // GetAttFunc represents an invocation of the Fn::GetAtt intrinsic. diff --git a/func_getatt_test.go b/func_getatt_test.go index 2536eb4..0505fb2 100644 --- a/func_getatt_test.go +++ b/func_getatt_test.go @@ -14,6 +14,9 @@ func (testSuite *GetAttFuncTest) TestBasics(c *C) { inputBuf := `{"Fn::GetAtt" : ["MySQLDatabase", "Endpoint.Address"]}` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(StringFunc).String(), DeepEquals, + GetAtt("MySQLDatabase", "Endpoint.Address")) + // old way c.Assert(f.(StringFunc).String(), DeepEquals, GetAtt("MySQLDatabase", "Endpoint.Address").String()) diff --git a/func_getazs.go b/func_getazs.go index b9999a2..e8de49c 100644 --- a/func_getazs.go +++ b/func_getazs.go @@ -1,8 +1,8 @@ package cloudformation // GetAZs returns a new instance of GetAZsFunc. -func GetAZs(region StringExpr) GetAZsFunc { - return GetAZsFunc{Region: region} +func GetAZs(region Stringable) *StringListExpr { + return GetAZsFunc{Region: *region.String()}.StringList() } // GetAZsFunc represents an invocation of the Fn::GetAZs intrinsic. diff --git a/func_getazs_test.go b/func_getazs_test.go index ec49596..425608b 100644 --- a/func_getazs_test.go +++ b/func_getazs_test.go @@ -14,6 +14,10 @@ func (testSuite *GetAZsFuncTest) TestBasics(c *C) { inputBuf := `{"Fn::GetAZs" : {"Ref": "AWS::Region"}}` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(StringListFunc).StringList(), DeepEquals, + GetAZs(Ref("AWS::Region"))) + + // old way c.Assert(f.(StringListFunc).StringList(), DeepEquals, GetAZs(*Ref("AWS::Region").String()).StringList()) diff --git a/func_if.go b/func_if.go index 650af2e..8ec516b 100644 --- a/func_if.go +++ b/func_if.go @@ -6,24 +6,24 @@ import "reflect" // If returns a new instance of IfFunc for the provided string expressions. // // See also: IfList -func If(condition string, valueIfTrue, valueIfFalse StringExpr) IfFunc { +func If(condition string, valueIfTrue, valueIfFalse Stringable) IfFunc { return IfFunc{ list: false, Condition: condition, - ValueIfTrue: valueIfTrue, - ValueIfFalse: valueIfFalse, + ValueIfTrue: *valueIfTrue.String(), + ValueIfFalse: *valueIfFalse.String(), } } // IfList returns a new instance of IfFunc for the provided string list expressions. // // See also: If -func IfList(condition string, valueIfTrue, valueIfFalse StringListExpr) IfFunc { +func IfList(condition string, valueIfTrue, valueIfFalse StringListable) IfFunc { return IfFunc{ list: true, Condition: condition, - ValueIfTrue: valueIfTrue, - ValueIfFalse: valueIfFalse, + ValueIfTrue: *valueIfTrue.StringList(), + ValueIfFalse: *valueIfFalse.StringList(), } } diff --git a/func_join.go b/func_join.go index ef4e62e..e6bd583 100644 --- a/func_join.go +++ b/func_join.go @@ -3,8 +3,8 @@ package cloudformation import "encoding/json" // Join returns a new instance of JoinFunc that joins items with separator. -func Join(separator string, items ...StringExpr) JoinFunc { - return JoinFunc{Separator: separator, Items: items} +func Join(separator string, items ...Stringable) *StringExpr { + return JoinFunc{Separator: separator, Items: *StringList(items...)}.String() } // JoinFunc represents an invocation of the Fn::Join intrinsic. @@ -16,7 +16,7 @@ func Join(separator string, items ...StringExpr) JoinFunc { // See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html type JoinFunc struct { Separator string - Items []StringExpr + Items StringListExpr } // MarshalJSON returns a JSON representation of the object diff --git a/func_join_test.go b/func_join_test.go index 3b9605a..358d377 100644 --- a/func_join_test.go +++ b/func_join_test.go @@ -14,6 +14,10 @@ func (testSuite *JoinFuncTest) TestRef(c *C) { inputBuf := `{"Fn::Join":["x",["y",{"Ref":"foo"},"1"]]}` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(StringFunc).String(), DeepEquals, + Join("x", String("y"), Ref("foo"), String("1"))) + + // old way c.Assert(f.(StringFunc).String(), DeepEquals, Join("x", *String("y"), *Ref("foo").String(), *String("1")).String()) diff --git a/func_select.go b/func_select.go index 22e4e4e..6708aa2 100644 --- a/func_select.go +++ b/func_select.go @@ -2,9 +2,20 @@ package cloudformation import "encoding/json" -// Select returns a new instance of SelectFunc chooses among items via selector. -func Select(selector string, items ...StringExpr) SelectFunc { - return SelectFunc{Selector: selector, Items: StringListExpr{Literal: items}} +type selectArg interface{} + +// Select returns a new instance of SelectFunc chooses among items via selector. If you +func Select(selector string, items ...interface{}) *StringExpr { + if len(items) == 1 { + if itemList, ok := items[0].(StringListable); ok { + return SelectFunc{Selector: selector, Items: *itemList.StringList()}.String() + } + } + stringableItems := make([]Stringable, len(items)) + for i, item := range items { + stringableItems[i] = item.(Stringable) + } + return SelectFunc{Selector: selector, Items: *StringList(stringableItems...)}.String() } // SelectFunc represents an invocation of the Fn::Select intrinsic. diff --git a/func_select_test.go b/func_select_test.go index 3961c2d..6a44abd 100644 --- a/func_select_test.go +++ b/func_select_test.go @@ -14,6 +14,10 @@ func (testSuite *SelectFuncTest) TestRef(c *C) { inputBuf := `{"Fn::Select":["2",{"Fn::GetAZs":{"Ref":"AWS::Region"}}]}` f, err := unmarshalFunc([]byte(inputBuf)) c.Assert(err, IsNil) + c.Assert(f.(StringFunc).String(), DeepEquals, + Select("2", GetAZs(Ref("AWS::Region")))) + + // old way c.Assert(f.(StringFunc).String(), DeepEquals, SelectFunc{Selector: "2", Items: *GetAZs(*Ref("AWS::Region").String()).StringList()}.String()) diff --git a/string.go b/string.go index 9e2456d..51594c7 100644 --- a/string.go +++ b/string.go @@ -2,6 +2,12 @@ package cloudformation import "encoding/json" +// Stringable is an interface that describes structures that are convertable +// to a *StringExpr. +type Stringable interface { + String() *StringExpr +} + // StringExpr is a string expression. If the value is computed then // Func will be non-nil. If it is a literal string then Literal gives // the value. Typically instances of this function are created by @@ -19,6 +25,11 @@ type StringExpr struct { Literal string } +// String implements Stringable +func (x StringExpr) String() *StringExpr { + return &x +} + // MarshalJSON returns a JSON representation of the object func (x StringExpr) MarshalJSON() ([]byte, error) { if x.Func != nil { @@ -43,7 +54,7 @@ func (x *StringExpr) UnmarshalJSON(data []byte) error { // function actually works in the boolean context funcCall, err2 := unmarshalFunc(data) if err2 == nil { - stringFunc, ok := funcCall.(StringFunc) + stringFunc, ok := funcCall.(Stringable) if ok { x.Func = stringFunc return nil diff --git a/string_list.go b/string_list.go index e204d5f..ae19c9a 100644 --- a/string_list.go +++ b/string_list.go @@ -5,6 +5,12 @@ import ( "fmt" ) +// StringListable is an interface that describes structures that are convertable +// to a *StringListExpr. +type StringListable interface { + StringList() *StringListExpr +} + // StringListExpr is a string expression. If the value is computed then // Func will be non-nil. If it is a literal string then Literal gives // the value. Typically instances of this function are created by @@ -19,7 +25,12 @@ import ( // type StringListExpr struct { Func StringListFunc - Literal []StringExpr + Literal []*StringExpr +} + +// StringList implements StringListable +func (x StringListExpr) StringList() *StringListExpr { + return &x } // MarshalJSON returns a JSON representation of the object @@ -32,7 +43,7 @@ func (x StringListExpr) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets the object from the provided JSON representation func (x *StringListExpr) UnmarshalJSON(data []byte) error { - var v []StringExpr + var v []*StringExpr err := json.Unmarshal(data, &v) if err == nil { x.Func = nil @@ -60,6 +71,10 @@ func (x *StringListExpr) UnmarshalJSON(data []byte) error { } // StringList returns a new StringListExpr representing the literal value v. -func StringList(v ...StringExpr) *StringListExpr { - return &StringListExpr{Literal: v} +func StringList(v ...Stringable) *StringListExpr { + rv := &StringListExpr{Literal: []*StringExpr{}} + for _, item := range v { + rv.Literal = append(rv.Literal, item.String()) + } + return rv } diff --git a/string_list_test.go b/string_list_test.go index 77513c6..3f66d51 100644 --- a/string_list_test.go +++ b/string_list_test.go @@ -23,6 +23,10 @@ func (testSuite *StringListTest) TestStringList(c *C) { c.Assert(err, IsNil) c.Assert(v.A, IsNil) + c.Assert(v.B, DeepEquals, StringList(String("one"))) + c.Assert(v.C, DeepEquals, StringList(String("two"), Ref("foo"))) + + // old way still works c.Assert(v.B, DeepEquals, StringList(*String("one"))) c.Assert(v.C, DeepEquals, StringList(*String("two"), *Ref("foo").String())) @@ -35,7 +39,8 @@ func (testSuite *StringListTest) TestStringList(c *C) { inputBuf = `{"A":{"Fn::GetAZs":""}}` err = json.Unmarshal([]byte(inputBuf), &v) c.Assert(err, IsNil) - c.Assert(v.A, DeepEquals, GetAZs(*String("")).StringList()) + c.Assert(v.A, DeepEquals, GetAZs(String(""))) + c.Assert(v.A, DeepEquals, GetAZs(*String("")).StringList()) // old way still works buf, err = json.Marshal(v) c.Assert(err, IsNil) c.Assert(string(buf), Equals, inputBuf) @@ -56,5 +61,4 @@ func (testSuite *StringListTest) TestStringList(c *C) { inputBuf = `{"A": {"Fn::Base64": "hello"}}` err = json.Unmarshal([]byte(inputBuf), &v) c.Assert(err, ErrorMatches, ".* is not a StringListFunc") - } diff --git a/string_test.go b/string_test.go index d4a74f7..891f4fb 100644 --- a/string_test.go +++ b/string_test.go @@ -48,5 +48,4 @@ func (testSuite *StringTest) TestString(c *C) { inputBuf = `{"A": {"Fn::Missing": "hello"}}` err = json.Unmarshal([]byte(inputBuf), &v) c.Assert(err, ErrorMatches, "unknown function Fn::Missing") - }