Skip to content

Commit

Permalink
Rework the format of MsgTemplating
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Apr 15, 2024
1 parent f46bb80 commit 7690e94
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 114 deletions.
58 changes: 31 additions & 27 deletions flows/actions/send_msg.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package actions

import (
"fmt"
"strings"

"github.com/nyaruka/gocommon/i18n"
Expand All @@ -10,7 +11,6 @@ import (
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/events"
"github.com/nyaruka/goflow/utils"
)

func init() {
Expand Down Expand Up @@ -141,63 +141,67 @@ func (a *SendMsgAction) Execute(run flows.Run, step flows.Step, logModifier flow
// for message actions that specidy a template, this generates the template message where the message content should be
// considered just a preview of how the template will be evaluated by the channel
func (a *SendMsgAction) getTemplateMsg(run flows.Run, urn urns.URN, channelRef *assets.ChannelReference, translation *flows.TemplateTranslation, unsendableReason flows.UnsendableReason, logEvent flows.EventCallback) *flows.MsgOut {
// start by localizing and evaluating the param values - for now these are per-component
evaluatedParams := make(map[string][]string)

// start by localizing and evaluating the param values
totalParams := 0
for _, comp := range a.Templating.Components {
localizedCompParams, _ := run.GetTextArray(comp.UUID, "params", comp.Params, nil)
evaluatedCompParams := make([]string, len(localizedCompParams))
for i, variable := range localizedCompParams {
sub, _ := run.EvaluateTemplate(variable, logEvent)
evaluatedCompParams[i] = sub
totalParams++
}

evaluatedParams[comp.Name] = evaluatedCompParams
}

// next we cross reference with params defined in the template translation to get types
// Turn that into a single list of variable values using the order of components on translation.
// Eventually this is what will be stored in the flow definition.
evaluatedVariables := make([]string, 0, totalParams)
for _, comp := range translation.Components() {
evaluatedVariables = append(evaluatedVariables, evaluatedParams[comp.Name()]...)
}

// cross-reference with asset to get variable types
variables := make([]*flows.TemplatingVariable, len(translation.Variables()))
for i, v := range translation.Variables() {
if i < len(evaluatedVariables) {
variables[i] = &flows.TemplatingVariable{Type: v.Type(), Value: evaluatedVariables[i]}
} else {
variables[i] = &flows.TemplatingVariable{Type: v.Type(), Value: ""}

Check warning on line 171 in flows/actions/send_msg.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/send_msg.go#L171

Added line #L171 was not covered by tests
}
}

// create a list of components that have variables
components := make([]*flows.TemplatingComponent, 0, len(translation.Components()))
for _, comp := range translation.Components() {
if len(comp.Variables()) > 0 {
components = append(components, &flows.TemplatingComponent{Type: comp.Type(), Name: comp.Name(), Variables: comp.Variables()})
}
}

// the message we return is an approximate preview of what the channel will send using the template
var previewParts []string
var previewQRs []string

variables := translation.Variables()

for _, comp := range translation.Components() {
varMap := comp.Variables()
paramValues := evaluatedParams[comp.Name()]
params := make([]flows.TemplatingParam, len(varMap))

for i, varName := range utils.SortedKeys(varMap) {
v := variables[varMap[varName]]
if i < len(paramValues) {
params[i] = flows.TemplatingParam{Type: v.Type(), Value: paramValues[i]}
} else {
params[i] = flows.TemplatingParam{Type: v.Type(), Value: ""}
}
previewContent := comp.Content()
for key, index := range comp.Variables() {
previewContent = strings.ReplaceAll(previewContent, fmt.Sprintf("{{%s}}", key), variables[index].Value)
}

compTemplating := &flows.TemplatingComponent{Type: comp.Type(), Name: comp.Name(), Params: params}
previewContent := compTemplating.Preview(comp)

if previewContent != "" {
if comp.Type() == "header" || comp.Type() == "body" || comp.Type() == "footer" {
previewParts = append(previewParts, previewContent)
} else if strings.HasPrefix(comp.Type(), "button/") {
previewQRs = append(previewQRs, stringsx.TruncateEllipsis(previewContent, maxQuickReplyLength))
}
}

if len(params) > 0 {
components = append(components, compTemplating)
}
}

previewText := strings.Join(previewParts, "\n\n")

locale := translation.Locale()
templating := flows.NewMsgTemplating(a.Templating.Template, translation.Namespace(), components)
templating := flows.NewMsgTemplating(a.Templating.Template, translation.Namespace(), components, variables)

return flows.NewMsgOut(urn, channelRef, previewText, nil, previewQRs, templating, flows.NilMsgTopic, locale, unsendableReason)
}
85 changes: 49 additions & 36 deletions flows/actions/testdata/send_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,16 +463,20 @@
{
"type": "body",
"name": "body",
"params": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "boy"
}
]
"variables": {
"1": 0,
"2": 1
}
}
],
"variables": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "boy"
}
]
},
Expand Down Expand Up @@ -560,16 +564,20 @@
{
"type": "body",
"name": "body",
"params": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "niño"
}
]
"variables": {
"1": 0,
"2": 1
}
}
],
"variables": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "niño"
}
]
},
Expand Down Expand Up @@ -896,26 +904,31 @@
{
"type": "body",
"name": "body",
"params": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "niño"
}
]
"variables": {
"1": 0,
"2": 1
}
},
{
"type": "button/quick_reply",
"name": "button.0",
"params": [
{
"type": "text",
"value": "Sip"
}
]
"variables": {
"1": 2
}
}
],
"variables": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "niño"
},
{
"type": "text",
"value": "Sip"
}
]
},
Expand Down
45 changes: 10 additions & 35 deletions flows/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"slices"
"strings"

"github.com/go-playground/validator/v10"
"github.com/nyaruka/gocommon/i18n"
Expand Down Expand Up @@ -170,53 +168,30 @@ func (m *MsgOut) Locale() i18n.Locale { return m.Locale_ }
// UnsendableReason returns the reason this message can't be sent (if any)
func (m *MsgOut) UnsendableReason() UnsendableReason { return m.UnsendableReason_ }

type TemplatingParam struct {
type TemplatingVariable struct {
Type string `json:"type"`
Value string `json:"value"`
}

type TemplatingComponent struct {
Type string `json:"type"`
Name string `json:"name"`
Params []TemplatingParam `json:"params"`
}

var templateVariableRegex = regexp.MustCompile(`{{(\d+)}}`)

// Preview returns the content of the given template component rendered using these templating params
func (tc *TemplatingComponent) Preview(c assets.TemplateComponent) string {
content := c.Content()

for i, p := range tc.Params {
content = strings.ReplaceAll(content, fmt.Sprintf("{{%d}}", i+1), p.Value)
}

content = templateVariableRegex.ReplaceAllString(content, "")

return content
Type string `json:"type"`
Name string `json:"name"`
Variables map[string]int `json:"variables"`
}

// MsgTemplating represents any substituted message template that should be applied when sending this message
type MsgTemplating struct {
Template_ *assets.TemplateReference `json:"template"`
Namespace_ string `json:"namespace"`
Components_ []*TemplatingComponent `json:"components,omitempty"`
Template *assets.TemplateReference `json:"template"`
Namespace string `json:"namespace"`
Components []*TemplatingComponent `json:"components,omitempty"`
Variables []*TemplatingVariable `json:"variables,omitempty"`
}

// NewMsgTemplating creates and returns a new msg template
func NewMsgTemplating(template *assets.TemplateReference, namespace string, components []*TemplatingComponent) *MsgTemplating {
return &MsgTemplating{Template_: template, Namespace_: namespace, Components_: components}
func NewMsgTemplating(template *assets.TemplateReference, namespace string, components []*TemplatingComponent, variables []*TemplatingVariable) *MsgTemplating {
return &MsgTemplating{Template: template, Namespace: namespace, Components: components, Variables: variables}
}

// Template returns the template this msg template is for
func (t *MsgTemplating) Template() *assets.TemplateReference { return t.Template_ }

// Namespace returns the namespace that should be for the template
func (t *MsgTemplating) Namespace() string { return t.Namespace_ }

// Components returns the components that should be used for the templates
func (t *MsgTemplating) Components() []*TemplatingComponent { return t.Components_ }

// BroadcastTranslation is the broadcast content in a particular language
type BroadcastTranslation struct {
Text string `json:"text"`
Expand Down
39 changes: 23 additions & 16 deletions flows/msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,17 @@ func TestMsgTemplating(t *testing.T) {

templateRef := assets.NewTemplateReference("61602f3e-f603-4c70-8a8f-c477505bf4bf", "Affirmation")

msgTemplating := flows.NewMsgTemplating(templateRef, "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", []*flows.TemplatingComponent{{Type: "body", Name: "body", Params: []flows.TemplatingParam{{Type: "text", Value: "Ryan Lewis"}, {Type: "text", Value: "boy"}}}})
msgTemplating := flows.NewMsgTemplating(
templateRef,
"0162a7f4_dfe4_4c96_be07_854d5dba3b2b",
[]*flows.TemplatingComponent{{Type: "body", Name: "body", Variables: map[string]int{"1": 0, "2": 1}}},
[]*flows.TemplatingVariable{{Type: "text", Value: "Ryan Lewis"}, {Type: "text", Value: "boy"}},
)

assert.Equal(t, templateRef, msgTemplating.Template())
assert.Equal(t, "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", msgTemplating.Namespace())
assert.Equal(t, []*flows.TemplatingComponent{{Type: "body", Name: "body", Params: []flows.TemplatingParam{{Type: "text", Value: "Ryan Lewis"}, {Type: "text", Value: "boy"}}}}, msgTemplating.Components())
assert.Equal(t, templateRef, msgTemplating.Template)
assert.Equal(t, "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", msgTemplating.Namespace)
assert.Equal(t, []*flows.TemplatingComponent{{Type: "body", Name: "body", Variables: map[string]int{"1": 0, "2": 1}}}, msgTemplating.Components)
assert.Equal(t, []*flows.TemplatingVariable{{Type: "text", Value: "Ryan Lewis"}, {Type: "text", Value: "boy"}}, msgTemplating.Variables)

// test marshaling our msg
marshaled, err := jsonx.Marshal(msgTemplating)
Expand All @@ -183,22 +189,23 @@ func TestMsgTemplating(t *testing.T) {
{
"type": "body",
"name": "body",
"params":[
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "boy"
}
]
"variables": {"1": 0, "2": 1}
}
],
"variables": [
{
"type": "text",
"value": "Ryan Lewis"
},
{
"type": "text",
"value": "boy"
}
]
}`), marshaled, "JSON mismatch")
}

func TestTemplatingComponentPreview(t *testing.T) {
/*func TestTemplatingComponentPreview(t *testing.T) {
tcs := []struct {
templating *flows.TemplatingComponent
component assets.TemplateComponent
Expand Down Expand Up @@ -235,4 +242,4 @@ func TestTemplatingComponentPreview(t *testing.T) {
actualContent := tc.templating.Preview(tc.component)
assert.Equal(t, tc.expected, actualContent, "content mismatch in test %d", i)
}
}
}*/

0 comments on commit 7690e94

Please sign in to comment.