├── Readme.md ├── interpolate.go └── interpolate_test.go /Readme.md: -------------------------------------------------------------------------------- 1 | # interpolate 2 | 3 | > **Note** 4 | > Segment has paused maintenance on this project, but may return it to an active status in the future. Issues and pull requests from external contributors are not being considered, although internal contributions may appear from time to time. The project remains available under its open source license for anyone to use. 5 | 6 | Simple interpolation templates ex: `Hello {name.first}`. 7 | 8 | ## Usage 9 | 10 | #### type Node 11 | 12 | ```go 13 | type Node interface { 14 | Value(v interface{}) (string, error) 15 | } 16 | ``` 17 | 18 | Node represents a literal or interpolated node. 19 | 20 | #### type Template 21 | 22 | ```go 23 | type Template struct { 24 | } 25 | ``` 26 | 27 | Template represents a series of literal and interpolated nodes. 28 | 29 | #### func New 30 | 31 | ```go 32 | func New(s string) (*Template, error) 33 | ``` 34 | New template from the given string. 35 | 36 | #### func (*Template) Eval 37 | 38 | ```go 39 | func (t *Template) Eval(v interface{}) (string, error) 40 | ``` 41 | Eval evalutes the given value against the template, returning an error if 42 | there's a path mismatch. 43 | -------------------------------------------------------------------------------- /interpolate.go: -------------------------------------------------------------------------------- 1 | package interpolate 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // TODO: unicode 10 | // TODO: more types 11 | 12 | // States. 13 | const ( 14 | sLit = iota 15 | sVar 16 | ) 17 | 18 | // Literal node. 19 | type literal struct { 20 | text string 21 | } 22 | 23 | // Value of literal node. 24 | func (n *literal) Value(v interface{}) (string, error) { 25 | return n.text, nil 26 | } 27 | 28 | // Variable node. 29 | type variable struct { 30 | path []string 31 | } 32 | 33 | // Get value from interface at the variable's path. 34 | func (n *variable) get(v interface{}) interface{} { 35 | for _, key := range n.path { 36 | if m, ok := v.(map[string]interface{}); ok { 37 | v = m[key] 38 | } else { 39 | return nil 40 | } 41 | } 42 | return v 43 | } 44 | 45 | // Value of interpolated variable. 46 | func (n *variable) Value(v interface{}) (string, error) { 47 | v = n.get(v) 48 | switch v.(type) { 49 | case string: 50 | return v.(string), nil 51 | default: 52 | path := strings.Join(n.path, ".") 53 | return "", fmt.Errorf("invalid value at path %v", path) 54 | } 55 | } 56 | 57 | // Node represents a literal or interpolated node. 58 | type Node interface { 59 | Value(v interface{}) (string, error) 60 | } 61 | 62 | // Template represents a series of literal and interpolated nodes. 63 | type Template struct { 64 | nodes []Node 65 | } 66 | 67 | // New template from the given string. 68 | func New(s string) (*Template, error) { 69 | tmpl := new(Template) 70 | state := sLit 71 | buf := new(bytes.Buffer) 72 | 73 | for i := 0; i < len(s); i++ { 74 | switch state { 75 | case sLit: 76 | switch s[i] { 77 | case '{': 78 | tmpl.nodes = append(tmpl.nodes, &literal{buf.String()}) 79 | state = sVar 80 | buf = new(bytes.Buffer) 81 | default: 82 | buf.WriteByte(s[i]) 83 | } 84 | case sVar: 85 | switch s[i] { 86 | case '}': 87 | path := strings.Split(buf.String(), ".") 88 | tmpl.nodes = append(tmpl.nodes, &variable{path}) 89 | state = sLit 90 | buf = new(bytes.Buffer) 91 | default: 92 | buf.WriteByte(s[i]) 93 | } 94 | } 95 | } 96 | 97 | if state == sVar { 98 | return nil, fmt.Errorf("missing '}'") 99 | } 100 | 101 | if state == sLit && buf.Len() > 0 { 102 | tmpl.nodes = append(tmpl.nodes, &literal{buf.String()}) 103 | } 104 | 105 | return tmpl, nil 106 | } 107 | 108 | // Eval evalutes the given value against the template, 109 | // returning an error if there's a path mismatch. 110 | func (t *Template) Eval(v interface{}) (string, error) { 111 | var buf bytes.Buffer 112 | 113 | for _, node := range t.nodes { 114 | ret, err := node.Value(v) 115 | if err != nil { 116 | return "", err 117 | } 118 | buf.WriteString(ret) 119 | } 120 | 121 | return buf.String(), nil 122 | } 123 | -------------------------------------------------------------------------------- /interpolate_test.go: -------------------------------------------------------------------------------- 1 | package interpolate 2 | 3 | import "github.com/bmizerany/assert" 4 | import "testing" 5 | 6 | func check(err error) { 7 | if err != nil { 8 | panic(err) 9 | } 10 | } 11 | 12 | func TestParseBroken(t *testing.T) { 13 | _, err := New(`Hello {name`) 14 | assert.Equal(t, `missing '}'`, err.Error()) 15 | } 16 | 17 | func TestEval(t *testing.T) { 18 | tmpl, err := New(`Hello {name}`) 19 | assert.Equal(t, nil, err) 20 | 21 | s, err := tmpl.Eval(map[string]interface{}{"name": "Tobi"}) 22 | assert.Equal(t, nil, err) 23 | assert.Equal(t, `Hello Tobi`, s) 24 | } 25 | 26 | func TestEvalMany(t *testing.T) { 27 | tmpl, err := New(`Hello {first} {last}`) 28 | assert.Equal(t, nil, err) 29 | 30 | s, err := tmpl.Eval(map[string]interface{}{ 31 | "first": "Tobi", 32 | "last": "Ferret", 33 | }) 34 | 35 | assert.Equal(t, nil, err) 36 | assert.Equal(t, `Hello Tobi Ferret`, s) 37 | } 38 | 39 | func TestEvalManyNested(t *testing.T) { 40 | tmpl, err := New(`Hello {name.first} {name.last} you are {color}`) 41 | assert.Equal(t, nil, err) 42 | 43 | s, err := tmpl.Eval(map[string]interface{}{ 44 | "name": map[string]interface{}{ 45 | "first": "Tobi", 46 | "last": "Ferret", 47 | }, 48 | "color": "Albino", 49 | }) 50 | 51 | assert.Equal(t, nil, err) 52 | assert.Equal(t, `Hello Tobi Ferret you are Albino`, s) 53 | } 54 | 55 | func TestEvalTooShort(t *testing.T) { 56 | tmpl, err := New(`Hello {name.first} {name.last} you are {color}`) 57 | assert.Equal(t, nil, err) 58 | 59 | _, err = tmpl.Eval(map[string]interface{}{ 60 | "name": "Tobi Ferret", 61 | "color": "Albino", 62 | }) 63 | 64 | assert.Equal(t, `invalid value at path name.first`, err.Error()) 65 | } 66 | 67 | func TestEvalTooLong(t *testing.T) { 68 | tmpl, err := New(`Hello {name.first.whatever.stuff.here.whoop} {name.last} you are {color}`) 69 | assert.Equal(t, nil, err) 70 | 71 | s, err := tmpl.Eval(map[string]interface{}{ 72 | "name": map[string]interface{}{ 73 | "first": "Tobi", 74 | "last": "Ferret", 75 | }, 76 | "color": "Albino", 77 | }) 78 | 79 | assert.Equal(t, "", s) 80 | assert.Equal(t, `invalid value at path name.first.whatever.stuff.here.whoop`, err.Error()) 81 | } 82 | 83 | func TestEvalTrailingLit(t *testing.T) { 84 | tmpl, err := New(`stream:project:{projectId}:ingress`) 85 | assert.Equal(t, nil, err) 86 | 87 | s, err := tmpl.Eval(map[string]interface{}{"projectId": "1234"}) 88 | assert.Equal(t, nil, err) 89 | assert.Equal(t, `stream:project:1234:ingress`, s) 90 | } 91 | 92 | func BenchmarkParse(t *testing.B) { 93 | for i := 0; i < t.N; i++ { 94 | New(`Hello {name.first} {name.last} you are {color}`) 95 | } 96 | } 97 | 98 | func BenchmarkEval(t *testing.B) { 99 | tmpl, _ := New(`Hello {name.first} {name.last} you are {color}`) 100 | 101 | v := map[string]interface{}{ 102 | "name": map[string]interface{}{ 103 | "first": "Tobi", 104 | "last": "Ferret", 105 | }, 106 | "color": "Albino", 107 | } 108 | 109 | for i := 0; i < t.N; i++ { 110 | tmpl.Eval(v) 111 | } 112 | } 113 | --------------------------------------------------------------------------------