-
Notifications
You must be signed in to change notification settings - Fork 20
/
migrate.go
211 lines (171 loc) · 5.81 KB
/
migrate.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
package expressions
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/nyaruka/goflow/envs"
"github.com/nyaruka/goflow/excellent"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/definition/legacy/gen"
"github.com/antlr/antlr4/runtime/Go/antlr"
)
// ContextTopLevels are the allowed top-level identifiers in legacy expressions, i.e. @contact.bar is valid but @foo.bar isn't
var ContextTopLevels = []string{"channel", "child", "contact", "date", "extra", "flow", "parent", "step"}
var functionReturnTypes = map[string]string{
"abs": "number",
"datetime_add": "datetime",
"datetime_from_parts": "datetime",
"datetime": "datetime",
"date": "date",
"format_date": "date",
"max": "number",
"mean": "number",
"min": "number",
"mod": "number",
"now": "datetime",
"sum": "number",
"rand": "number",
"round": "number",
"round_down": "number",
"round_up": "number",
"time": "time",
"time_from_parts": "time",
"today": "date",
}
// MigrateOptions are options for how expressions are migrated
type MigrateOptions struct {
DefaultToSelf bool
URLEncode bool
RawDates bool
}
var defaultOptions = &MigrateOptions{DefaultToSelf: false, URLEncode: false, RawDates: false}
// MigrateTemplate will take a legacy expression and translate it to the new syntax
func MigrateTemplate(template string, options *MigrateOptions) (string, error) {
if options == nil {
options = defaultOptions
}
return migrateLegacyTemplateAsString(template, options)
}
func migrateLegacyTemplateAsString(template string, options *MigrateOptions) (string, error) {
var buf bytes.Buffer
scanner := excellent.NewXScanner(strings.NewReader(template), ContextTopLevels)
scanner.SetUnescapeBody(false)
errors := excellent.NewTemplateErrors()
for tokenType, token := scanner.Scan(); tokenType != excellent.EOF; tokenType, token = scanner.Scan() {
switch tokenType {
case excellent.BODY:
buf.WriteString(token)
case excellent.IDENTIFIER:
value := MigrateContextReference(token, options.RawDates)
var errorAs string
if options.DefaultToSelf {
errorAs = "@" + token
}
// optionally wrap expression so that it is URL encoded or defaults to itself on error
buf.WriteString(wrapRawExpression(value, errorAs, options.URLEncode))
case excellent.EXPRESSION:
// special case of @("") which was a common workaround for the editor requiring a
// non-empty string, but is no longer needed and can be replaced by an empty string
if token == `""` {
continue
}
value, err := migrateExpression(nil, token, options)
if err != nil {
errors.Add(fmt.Sprintf("@(%s)", token), err.Error())
buf.WriteString("@(")
buf.WriteString(token)
buf.WriteString(")")
} else {
var errorAs string
if options.DefaultToSelf {
errorAs = "@(" + token + ")"
}
// optionally wrap expression so that it is URL encoded or defaults to itself on error
buf.WriteString(wrapRawExpression(value, errorAs, options.URLEncode))
}
}
}
if errors.HasErrors() {
return buf.String(), errors
}
return buf.String(), nil
}
// migrates an old expression into a new format expression
func migrateExpression(env envs.Environment, expression string, options *MigrateOptions) (string, error) {
errListener := excellent.NewErrorListener(expression)
input := antlr.NewInputStream(expression)
lexer := gen.NewExcellent1Lexer(input)
stream := antlr.NewCommonTokenStream(lexer, 0)
p := gen.NewExcellent1Parser(stream)
p.RemoveErrorListeners()
p.AddErrorListener(errListener)
// speed up parsing
p.GetInterpreter().SetPredictionMode(antlr.PredictionModeSLL)
tree := p.Parse()
// if we ran into errors parsing, return the first one
if len(errListener.Errors()) > 0 {
return "", errListener.Errors()[0]
}
visitor := newLegacyVisitor(env, options)
value := visitor.Visit(tree)
err, isErr := value.(error)
// did our evaluation result in an error? return that
if isErr {
return "", err
}
// all is good, return our value
return value.(string), nil
}
var functionCallRegex = regexp.MustCompile(`^(\w+)\(`)
func inferType(operand string) string {
// if we have an integer literal, we're a number
_, numErr := strconv.Atoi(operand)
if numErr == nil {
return "number"
}
// if this looks like a function call, lookup its return type
matches := functionCallRegex.FindStringSubmatch(operand)
if matches != nil {
return functionReturnTypes[matches[1]]
}
return ""
}
var identifierRegex = regexp.MustCompile(`^\pL+[\pL\pN_.]*$`)
func isValidIdentifier(expression string) bool {
if !identifierRegex.MatchString(expression) {
return false
}
for _, topLevel := range flows.RunContextTopLevels {
if strings.HasPrefix(expression, topLevel+".") || expression == topLevel {
return true
}
}
return false
}
// takes a raw expression and wraps it for inclusion in a template, e.g. now() -> @(now())
func wrapRawExpression(expression string, errorAs string, urlEncode bool) string {
if errorAs != "" {
expression = fmt.Sprintf(`if(is_error(%s), %s, %s)`, expression, strconv.Quote(errorAs), expression)
}
if urlEncode {
expression = wrap(expression, "url_encode")
}
if !isValidIdentifier(expression) {
expression = "(" + expression + ")"
}
return "@" + expression
}
func wrap(expression, funcName string) string {
return fmt.Sprintf("%s(%s)", funcName, expression)
}
// MigrateStringLiteral migrates a string literal (legacy expressions use Excel "" escaping)
func MigrateStringLiteral(s string) string {
// strip surrounding quotes
s = s[1 : len(s)-1]
// replace any escaped quotes
s = strings.Replace(s, `""`, `\"`, -1)
// re-quote
return `"` + s + `"`
}