forked from SparkPost/gosparkpost
-
Notifications
You must be signed in to change notification settings - Fork 0
/
macros.go
222 lines (196 loc) · 5.5 KB
/
macros.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package gosparkpost
import (
"regexp"
"strings"
"github.com/pkg/errors"
)
// Macro enables user-defined functions to run on Template.Content before sending to the SparkPost API.
// This enables e.g. external content, locale-specific date formatting, and so on.
type Macro struct {
Name string
Func func(string) string
}
const (
StaticToken = iota
MacroToken
)
// TokenType differentiates between static content and macros that require extra processing.
type TokenType int
// ContentToken represents a piece of content, with one of the types defined above.
type ContentToken struct {
Type TokenType
Text string
}
var wordChars = regexp.MustCompile(`^\w+$`)
// RegisterMacro associates a Macro with a Client.
// As with all changes to the Client, this is only safe to call before any potential concurrency.
// Everything between the Macro Name and the closing delimiter will be passed to the Func as a single string argument.
func (c *Client) RegisterMacro(m *Macro) error {
if m == nil {
return errors.New(`can't add nil Macro`)
} else if !wordChars.MatchString(m.Name) {
return errors.New(`Macro names must only contain \w characters`)
} else if m.Func == nil {
return errors.New(`Macro must have non-nil Func field`)
}
if c.macros == nil {
c.macros = map[string]Macro{}
}
c.macros[m.Name] = *m
return nil
}
// Apply substitutes top-level string values from the Recipient's SubstitutionData and Metadata
// (in that order) for placeholders in the provided string. Nested substitution blocks will not
// be interpreted, meaning that they will be passed along to the API.
func (r *Recipient) Apply(in string) (string, error) {
if r == nil {
return in, nil
}
tokens, err := Tokenize(in)
if err != nil {
return "", err
}
chunks := make([]string, len(tokens))
addr, err := ParseAddress(r.Address)
if err != nil {
return "", errors.Wrap(err, "parsing recipient address")
}
var sub, meta map[string]interface{}
var ok bool
if r.SubstitutionData != nil {
if sub, ok = r.SubstitutionData.(map[string]interface{}); !ok {
switch itype := r.SubstitutionData.(type) {
default:
return "", errors.Errorf("unexpected substitution data type [%T] for recipient %s", itype, addr.Email)
}
}
}
if r.Metadata != nil {
if meta, ok = r.Metadata.(map[string]interface{}); !ok {
switch itype := r.Metadata.(type) {
default:
return "", errors.Errorf("unexpected metadata type [%T] for recipient %s", itype, addr.Email)
}
}
}
for idx, token := range tokens {
switch token.Type {
case StaticToken:
chunks[idx] = token.Text
case MacroToken:
key := strings.TrimSpace(strings.Trim(token.Text, "{}"))
for _, subst := range []map[string]interface{}{sub, meta} {
if ival, ok := subst[key]; ok {
switch val := ival.(type) {
case string:
chunks[idx] = val
default:
chunks[idx] = token.Text
}
break
}
}
}
}
if len(chunks) == 1 {
return chunks[0], nil
}
return strings.Join(chunks, ""), nil
}
// ApplyMacros runs all Macros registered with the Client against the provided string, returning the result.
// If a Recipient is provided, substitution is performed on the macro parameter before the macro runs.
// Any placeholders not handled by a macro are left intact.
func (c *Client) ApplyMacros(in string, r *Recipient) (string, error) {
if c.macros == nil {
// if no macros are defined, this is a no-op
return in, nil
}
tokens, err := Tokenize(in)
if err != nil {
return "", err
}
chunks := make([]string, len(tokens))
for idx, token := range tokens {
switch token.Type {
case StaticToken:
chunks[idx] = token.Text
case MacroToken:
body := strings.TrimSpace(strings.Trim(token.Text, "{}"))
// split off macro name
atoms := strings.SplitN(body, " ", 2)
if m, ok := c.macros[atoms[0]]; ok {
var params string
if len(atoms) == 2 {
params = atoms[1]
} else {
params = ""
}
if r != nil {
params, err = r.Apply(params)
if err != nil {
return "", err
}
}
chunks[idx] = m.Func(params)
} else {
// no client macro matches this block, pass it through
chunks[idx] = token.Text
}
}
}
if len(chunks) == 1 {
return chunks[0], nil
}
return strings.Join(chunks, ""), nil
}
// Tokenize splits a string that may contain Handlebars-style template code into
// (you guessed it) tokens for further processing. Called by Client.ApplyMacros
// and Recipient.Apply internally. Unless those functions do not meet your specific
// needs, this function should not need to be called directly.
func Tokenize(str string) (out []ContentToken, err error) {
strlen := len(str)
for {
open := strings.Index(str, "{{")
if open >= 0 && open < strlen {
if open > 0 {
// we have a macro, make a token with the static text leading up to it
out = append(out, ContentToken{Text: str[:open]})
str = str[open:]
strlen -= open
} else {
// Do nothing if macro starts at index 0,
// otherwise we end up with blank StaticTokens
}
} else {
break
}
// advance to the end of the macro
curlies := 0
var last int
for last = 0; last < strlen; last++ {
switch str[last] {
case '{':
curlies++
case '}':
curlies--
}
if curlies == 0 {
last++
break
}
}
if curlies != 0 {
return nil, errors.Errorf("mismatched curly braces near %q", str)
}
out = append(out, ContentToken{
Type: MacroToken,
Text: str[:last],
})
str = str[last:]
strlen -= last
}
if strlen > 0 {
out = append(out, ContentToken{Text: str})
}
return out, nil
}