Skip to content

Commit

Permalink
create interfaces Stringable, StringListable that represent objects r…
Browse files Browse the repository at this point in the history
…epresentable as StringExpr and StringListExpr respectively.

This reduces typing considerably because we can reply on the golang
type system to perform conversions implicitly. We get to replace 
some instances of:

    var foo Stringable = *Ref(“foo”).String() 

with:

    var foo Stringable = Ref(“foo”)
  • Loading branch information
crewjam committed Jan 8, 2016
1 parent cfb8f58 commit a148b24
Show file tree
Hide file tree
Showing 19 changed files with 183 additions and 38 deletions.
16 changes: 8 additions & 8 deletions examples/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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
Expand Down
66 changes: 66 additions & 0 deletions examples/app_old.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 3 additions & 2 deletions func_base64.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
16 changes: 16 additions & 0 deletions func_base64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
9 changes: 5 additions & 4 deletions func_findinmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions func_findinmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions func_getatt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions func_getatt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
4 changes: 2 additions & 2 deletions func_getazs.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 4 additions & 0 deletions func_getazs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
12 changes: 6 additions & 6 deletions func_if.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down
6 changes: 3 additions & 3 deletions func_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions func_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
17 changes: 14 additions & 3 deletions func_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions func_select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
13 changes: 12 additions & 1 deletion string.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
23 changes: 19 additions & 4 deletions string_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit a148b24

Please sign in to comment.