template: add tests, fix up easy holes
should cope with double-braced blocks now (treating them the same as single-braced. templateEmail now returns an error, which should not be seen as catastrophic, but reports likely mistakes.
This commit is contained in:
+65
-34
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
func truthy(val interface{}) bool {
|
func truthy(val interface{}) bool {
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
@@ -18,43 +21,72 @@ func truthy(val interface{}) bool {
|
|||||||
// Variables should be written as {varName}.
|
// Variables should be written as {varName}.
|
||||||
// If statements should be written as {if (!)varName}...{endif}.
|
// If statements should be written as {if (!)varName}...{endif}.
|
||||||
// Strings are true if != "", ints are true if != 0.
|
// Strings are true if != "", ints are true if != 0.
|
||||||
func templateEmail(content string, variables []string, conditionals []string, values map[string]interface{}) string {
|
// Errors returned are likely warnings only.
|
||||||
|
func templateEmail(content string, variables []string, conditionals []string, values map[string]interface{}) (string, error) {
|
||||||
|
// minimum length for templatable content (albeit just "{}" -> "")
|
||||||
|
if len(content) < 2 {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
ifStart, ifEnd := -1, -1
|
ifStart, ifEnd := -1, -1
|
||||||
ifTrue := false
|
ifTrue := false
|
||||||
invalidIf := false
|
invalidIf := false
|
||||||
previousEnd := -2
|
previousEnd := -2
|
||||||
cStart, cEnd := -1, -1
|
blockRawStart := -1
|
||||||
|
blockContentStart, blockContentEnd := -1, -1
|
||||||
varStart, varEnd := -1, -1
|
varStart, varEnd := -1, -1
|
||||||
varName := ""
|
varName := ""
|
||||||
out := ""
|
out := ""
|
||||||
|
var err error = nil
|
||||||
|
|
||||||
|
oob := func(i int) bool { return i < 0 || i >= len(content) }
|
||||||
|
|
||||||
for i, c := range content {
|
for i, c := range content {
|
||||||
if c == '{' {
|
if c == '{' {
|
||||||
cStart = i + 1
|
blockContentStart = i + 1
|
||||||
for content[cStart] == ' ' {
|
blockRawStart = i
|
||||||
cStart++
|
if content[i+1] == '{' {
|
||||||
|
err = fmt.Errorf(`double braces ("{{") at position %d, use single brace only`, i)
|
||||||
|
blockContentStart++
|
||||||
}
|
}
|
||||||
if content[cStart:cStart+3] == "if " {
|
for !oob(blockContentStart) && content[blockContentStart] == ' ' {
|
||||||
varStart = cStart + 3
|
blockContentStart++
|
||||||
|
}
|
||||||
|
if oob(blockContentStart) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !oob(blockContentStart+3) && content[blockContentStart:blockContentStart+3] == "if " {
|
||||||
|
varStart = blockContentStart + 3
|
||||||
for content[varStart] == ' ' {
|
for content[varStart] == ' ' {
|
||||||
varStart++
|
varStart++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ifStart == -1 {
|
if ifStart == -1 && (oob(i-1) || content[i-1] != '{') {
|
||||||
out += content[previousEnd+2 : i]
|
out += content[previousEnd+2 : i]
|
||||||
}
|
}
|
||||||
if content[cStart:cStart+5] != "endif" || invalidIf {
|
if invalidIf || oob(blockContentStart+5) || content[blockContentStart:blockContentStart+5] != "endif" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ifEnd = i - 1
|
ifEnd = i - 1
|
||||||
if ifTrue {
|
if ifTrue {
|
||||||
out += templateEmail(content[ifStart:ifEnd+1], variables, conditionals, values)
|
toAppend, subErr := templateEmail(content[ifStart:ifEnd+1], variables, conditionals, values)
|
||||||
|
out += toAppend
|
||||||
|
if subErr != nil {
|
||||||
|
err = subErr
|
||||||
|
}
|
||||||
ifTrue = false
|
ifTrue = false
|
||||||
}
|
}
|
||||||
} else if c == '}' {
|
} else if c == '}' {
|
||||||
|
doubleBraced := !oob(i+1) && content[i+1] == '}'
|
||||||
|
if doubleBraced {
|
||||||
|
err = fmt.Errorf(`double braces ("}}") at position %d, use single brace only`, i)
|
||||||
|
}
|
||||||
|
if !oob(i-1) && content[i-1] == '}' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if varStart != -1 {
|
if varStart != -1 {
|
||||||
ifStart = i + 1
|
ifStart = i + 1
|
||||||
varEnd = i - 1
|
varEnd = i - 1
|
||||||
for content[varEnd] == ' ' {
|
for !oob(varEnd) && content[varEnd] == ' ' {
|
||||||
varEnd--
|
varEnd--
|
||||||
}
|
}
|
||||||
varName = content[varStart : varEnd+1]
|
varName = content[varStart : varEnd+1]
|
||||||
@@ -63,14 +95,8 @@ func templateEmail(content string, variables []string, conditionals []string, va
|
|||||||
positive = false
|
positive = false
|
||||||
varName = varName[1:]
|
varName = varName[1:]
|
||||||
}
|
}
|
||||||
validVar := false
|
|
||||||
wrappedVarName := "{" + varName + "}"
|
wrappedVarName := "{" + varName + "}"
|
||||||
for _, v := range conditionals {
|
validVar := slices.Contains(conditionals, wrappedVarName)
|
||||||
if v == wrappedVarName {
|
|
||||||
validVar = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if validVar {
|
if validVar {
|
||||||
ifTrue = positive == truthy(values[varName])
|
ifTrue = positive == truthy(values[varName])
|
||||||
} else {
|
} else {
|
||||||
@@ -79,27 +105,26 @@ func templateEmail(content string, variables []string, conditionals []string, va
|
|||||||
}
|
}
|
||||||
varStart, varEnd = -1, -1
|
varStart, varEnd = -1, -1
|
||||||
}
|
}
|
||||||
cEnd = i - 1
|
blockContentEnd = i - 1
|
||||||
for content[cEnd] == ' ' {
|
for content[blockContentEnd] == ' ' {
|
||||||
cEnd--
|
blockContentEnd--
|
||||||
}
|
}
|
||||||
previousEnd = i - 1
|
previousEnd = i - 1
|
||||||
if content[cEnd-4:cEnd+1] == "endif" && !invalidIf {
|
// Skip the extra brace
|
||||||
|
if doubleBraced {
|
||||||
|
previousEnd++
|
||||||
|
}
|
||||||
|
if !oob(blockContentEnd-4) && !oob(blockContentEnd+1) && content[blockContentEnd-4:blockContentEnd+1] == "endif" && !invalidIf {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
validVar := false
|
varName = content[blockContentStart : blockContentEnd+1]
|
||||||
varName = content[cStart : cEnd+1]
|
blockContentStart, blockContentEnd = -1, -1
|
||||||
cStart, cEnd = -1, -1
|
blockRawStart = -1
|
||||||
if ifStart != -1 {
|
if ifStart != -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wrappedVarName := "{" + varName + "}"
|
wrappedVarName := "{" + varName + "}"
|
||||||
for _, v := range variables {
|
validVar := slices.Contains(variables, wrappedVarName)
|
||||||
if v == wrappedVarName {
|
|
||||||
validVar = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !validVar {
|
if !validVar {
|
||||||
out += wrappedVarName
|
out += wrappedVarName
|
||||||
continue
|
continue
|
||||||
@@ -107,11 +132,17 @@ func templateEmail(content string, variables []string, conditionals []string, va
|
|||||||
out += fmt.Sprint(values[varName])
|
out += fmt.Sprint(values[varName])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if blockContentStart != -1 && blockContentEnd == -1 {
|
||||||
|
err = fmt.Errorf(`incomplete block (single "{") near position %d`, blockContentStart)
|
||||||
|
// Include the brace, maybe the user wants it.
|
||||||
|
previousEnd = blockRawStart - 2
|
||||||
|
}
|
||||||
if previousEnd+1 != len(content)-1 {
|
if previousEnd+1 != len(content)-1 {
|
||||||
out += content[previousEnd+2:]
|
out += content[previousEnd+2:]
|
||||||
|
|
||||||
}
|
}
|
||||||
if out == "" {
|
if out == "" {
|
||||||
return content
|
return content, err
|
||||||
}
|
}
|
||||||
return out
|
return out, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In == Out when nothing is meant to be templated.
|
||||||
|
func TestBlankTemplate(t *testing.T) {
|
||||||
|
in := `Success, user! Your account has been created. Log in at myAccountURL with your username to get started.`
|
||||||
|
|
||||||
|
out, err := templateEmail(in, []string{}, []string{}, map[string]any{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != in {
|
||||||
|
t.Fatalf(`returned string doesn't match input: "%+v" != "%+v"`, out, in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConditional(isTrue bool, t *testing.T) {
|
||||||
|
in := `Success, {username}! Your account has been created. {if myCondition}Log in at {myAccountURL} with username {username} to get started.{endif}`
|
||||||
|
|
||||||
|
vars := []string{"{username}", "{myAccountURL}", "{myCondition}"}
|
||||||
|
conds := vars
|
||||||
|
vals := map[string]any{
|
||||||
|
"username": "TemplateUsername",
|
||||||
|
"myAccountURL": "TemplateURL",
|
||||||
|
"myCondition": isTrue,
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := templateEmail(in, vars, conds, vals)
|
||||||
|
|
||||||
|
target := ""
|
||||||
|
if isTrue {
|
||||||
|
target = `Success, {username}! Your account has been created. Log in at {myAccountURL} with username {username} to get started.`
|
||||||
|
} else {
|
||||||
|
target = `Success, {username}! Your account has been created. `
|
||||||
|
}
|
||||||
|
|
||||||
|
target = strings.ReplaceAll(target, "{username}", vals["username"].(string))
|
||||||
|
target = strings.ReplaceAll(target, "{myAccountURL}", vals["myAccountURL"].(string))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != target {
|
||||||
|
t.Fatalf(`returned string doesn't match desired output: "%+v" != "%+v"`, out, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConditionalTrue(t *testing.T) {
|
||||||
|
testConditional(true, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConditionalFalse(t *testing.T) {
|
||||||
|
testConditional(false, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template mistakenly double-braced values, but return a warning.
|
||||||
|
func TestTemplateDoubleBraceGracefulHandling(t *testing.T) {
|
||||||
|
in := `Success, {{username}}! Your account has been created. Log in at {myAccountURL} with username {username} to get started.`
|
||||||
|
|
||||||
|
vars := []string{"{username}", "{myAccountURL}"}
|
||||||
|
vals := map[string]any{
|
||||||
|
"username": "TemplateUsername",
|
||||||
|
"myAccountURL": "TemplateURL",
|
||||||
|
}
|
||||||
|
|
||||||
|
target := strings.ReplaceAll(in, "{{username}}", vals["username"].(string))
|
||||||
|
target = strings.ReplaceAll(target, "{username}", vals["username"].(string))
|
||||||
|
target = strings.ReplaceAll(target, "{myAccountURL}", vals["myAccountURL"].(string))
|
||||||
|
|
||||||
|
out, err := templateEmail(in, vars, []string{}, vals)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("no error when given double-braced variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != target {
|
||||||
|
t.Fatalf(`returned string doesn't match desired output: "%+v" != "%+v"`, out, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVarAtAnyPosition(t *testing.T) {
|
||||||
|
in := `Success, user! Your account has been created. Log in at myAccountURL with your username to get started.`
|
||||||
|
vars := []string{"{username}", "{myAccountURL}"}
|
||||||
|
vals := map[string]any{
|
||||||
|
"username": "TemplateUsername",
|
||||||
|
"myAccountURL": "TemplateURL",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range in {
|
||||||
|
newIn := in[0:i] + vars[0] + in[i:]
|
||||||
|
|
||||||
|
target := strings.ReplaceAll(newIn, vars[0], vals["username"].(string))
|
||||||
|
|
||||||
|
out, err := templateEmail(newIn, vars, []string{}, vals)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != target {
|
||||||
|
t.Fatalf(`returned string doesn't match desired output: "%+v" != "%+v"`, out, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncompleteBlock(t *testing.T) {
|
||||||
|
in := `Success, user! Your account has been created. Log in at myAccountURL with your username to get started.`
|
||||||
|
for i := range in {
|
||||||
|
newIn := in[0:i] + "{" + in[i:]
|
||||||
|
|
||||||
|
out, err := templateEmail(newIn, []string{"a"}, []string{"a"}, map[string]any{"a": "a"})
|
||||||
|
|
||||||
|
if out != newIn {
|
||||||
|
t.Fatalf(`returned string for position %d/%d doesn't match desired output: "%+v" != "%+v"`, i+1, len(newIn), out, newIn)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("no error when given incomplete block with brace at position %d/%d", i+1, len(newIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user