From 228c915d7beec729adb6b65afa874fee203075b8 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 3 Jul 2024 19:17:46 -0500 Subject: [PATCH 1/2] Move code to generate templating into flows.Template --- flows/actions/send_msg.go | 67 ++++++++++++--------------------------- flows/template.go | 28 ++++++++++++++++ 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/flows/actions/send_msg.go b/flows/actions/send_msg.go index 3c7ac9a93..1a2c1fcab 100644 --- a/flows/actions/send_msg.go +++ b/flows/actions/send_msg.go @@ -85,13 +85,29 @@ func (a *SendMsgAction) Execute(run flows.Run, step flows.Step, logModifier flow for _, dest := range destinations { urn := dest.URN.URN() channelRef := assets.NewChannelReference(dest.Channel.UUID(), dest.Channel.Name()) - var msg *flows.MsgOut + if template != nil { locales := []i18n.Locale{run.Session().MergedEnvironment().DefaultLocale(), run.Session().Environment().DefaultLocale()} - templateTranslation := template.FindTranslation(dest.Channel, locales) - if templateTranslation != nil { - msg = a.getTemplateMsg(run, urn, channelRef, templateTranslation, unsendableReason, logEvent) + translation := template.FindTranslation(dest.Channel, locales) + if translation != nil { + // TODO in future we won't be localizing template variables + localizedVariables, _ := run.GetTextArray(uuids.UUID(a.UUID()), "template_variables", a.TemplateVariables, nil) + + // evaluate the variables + evaluatedVariables := make([]string, len(localizedVariables)) + for i, varExp := range localizedVariables { + v, _ := run.EvaluateTemplate(varExp, logEvent) + evaluatedVariables[i] = v + } + + templating := template.Templating(translation, evaluatedVariables) + + // the message we return is an approximate preview of what the channel will send using the template + preview := translation.Preview(templating.Variables) + locale := translation.Locale() + + msg = flows.NewMsgOut(urn, channelRef, preview.Text, preview.Attachments, preview.QuickReplies, templating, flows.NilMsgTopic, locale, unsendableReason) } } @@ -111,46 +127,3 @@ func (a *SendMsgAction) Execute(run flows.Run, step flows.Step, logModifier flow return nil } - -// for message actions that specify a template, this generates a mesage with templating information and content that can -// be used as a preview -func (a *SendMsgAction) getTemplateMsg(run flows.Run, urn urns.URN, channelRef *assets.ChannelReference, translation *flows.TemplateTranslation, unsendableReason flows.UnsendableReason, logEvent flows.EventCallback) *flows.MsgOut { - // localize and evaluate the variables - localizedVariables, _ := run.GetTextArray(uuids.UUID(a.UUID()), "template_variables", a.TemplateVariables, nil) - evaluatedVariables := make([]string, len(localizedVariables)) - for i, varExp := range localizedVariables { - v, _ := run.EvaluateTemplate(varExp, logEvent) - evaluatedVariables[i] = v - } - - // cross-reference with asset to get variable types and filter out invalid values - variables := make([]*flows.TemplatingVariable, len(translation.Variables())) - for i, v := range translation.Variables() { - // we pad out any missing variables with empty values - value := "" - if i < len(evaluatedVariables) { - value = evaluatedVariables[i] - } - - variables[i] = &flows.TemplatingVariable{Type: v.Type(), Value: value} - } - - // 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 - preview := translation.Preview(variables) - locale := translation.Locale() - templating := flows.NewMsgTemplating(a.Template, components, variables) - - return flows.NewMsgOut(urn, channelRef, preview.Text, preview.Attachments, preview.QuickReplies, templating, flows.NilMsgTopic, locale, unsendableReason) -} diff --git a/flows/template.go b/flows/template.go index 82b0f4c16..bbec515de 100644 --- a/flows/template.go +++ b/flows/template.go @@ -50,6 +50,34 @@ func (t *Template) FindTranslation(channel *Channel, locales []i18n.Locale) *Tem return candidates[match] } +func (t *Template) Templating(tt *TemplateTranslation, vars []string) *MsgTemplating { + // cross-reference with asset to get variable types and filter out invalid values + variables := make([]*TemplatingVariable, len(tt.Variables())) + for i, v := range tt.Variables() { + // we pad out any missing variables with empty values + value := "" + if i < len(vars) { + value = vars[i] + } + + variables[i] = &TemplatingVariable{Type: v.Type(), Value: value} + } + + // create a list of components that have variables + components := make([]*TemplatingComponent, 0, len(tt.Components())) + for _, comp := range tt.Components() { + if len(comp.Variables()) > 0 { + components = append(components, &TemplatingComponent{ + Type: comp.Type(), + Name: comp.Name(), + Variables: comp.Variables(), + }) + } + } + + return NewMsgTemplating(t.Reference(), components, variables) +} + // TemplateTranslation represents a single translation for a template type TemplateTranslation struct { assets.TemplateTranslation From 1a52cdc1fb59424dca58e86b22fc893fdbba2195 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 4 Jul 2024 15:19:55 +0000 Subject: [PATCH 2/2] More tests --- flows/template.go | 5 +- flows/template_test.go | 376 +++++++++++++++++++++++++++++++---------- 2 files changed, 289 insertions(+), 92 deletions(-) diff --git a/flows/template.go b/flows/template.go index bbec515de..e04df3e6e 100644 --- a/flows/template.go +++ b/flows/template.go @@ -50,16 +50,15 @@ func (t *Template) FindTranslation(channel *Channel, locales []i18n.Locale) *Tem return candidates[match] } +// Templating generates a templating object for the passed in translation and variables func (t *Template) Templating(tt *TemplateTranslation, vars []string) *MsgTemplating { - // cross-reference with asset to get variable types and filter out invalid values + // cross-reference with asset to get variable types and pad out any missing variables variables := make([]*TemplatingVariable, len(tt.Variables())) for i, v := range tt.Variables() { - // we pad out any missing variables with empty values value := "" if i < len(vars) { value = vars[i] } - variables[i] = &TemplatingVariable{Type: v.Type(), Value: value} } diff --git a/flows/template_test.go b/flows/template_test.go index 42350976f..f57fc1397 100644 --- a/flows/template_test.go +++ b/flows/template_test.go @@ -59,127 +59,325 @@ func TestFindTranslation(t *testing.T) { assert.Equal(t, (*assets.TemplateReference)(nil), (*flows.Template)(nil).Reference()) } -func TestTemplateTranslationPreview(t *testing.T) { +func TestTemplating(t *testing.T) { + channel := flows.NewChannel(static.NewChannel("79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "WhatsApp", "1234", []string{"whatsapp"}, nil, nil)) + tcs := []struct { - translation []byte - variables []*flows.TemplatingVariable - expected *flows.MsgContent + template []byte + variables []string + expectedTemplating *flows.MsgTemplating + expectedPreview *flows.MsgContent }{ { // 0: empty translation - translation: []byte(`{ - "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, - "locale": "eng", - "components": [], - "variables": [] + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [], + "variables": [] + } + ] }`), - variables: []*flows.TemplatingVariable{}, - expected: &flows.MsgContent{}, + variables: []string{}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{}, + Variables: []*flows.TemplatingVariable{}, + }, + expectedPreview: &flows.MsgContent{}, }, { // 1: body only - translation: []byte(`{ - "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, - "locale": "eng", - "components": [ - { - "name": "body", - "type": "body/text", - "content": "Hi {{1}}, who's a good {{2}}?", - "variables": {"1": 0, "2": 1} + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "body", + "type": "body/text", + "content": "Hi {{1}}, who's a good {{2}}?", + "variables": {"1": 0, "2": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] } - ], - "variables": [ - {"type": "text"}, - {"type": "text"} ] }`), - variables: []*flows.TemplatingVariable{{Type: "text", Value: "Chef"}, {Type: "text", Value: "boy"}}, - expected: &flows.MsgContent{Text: "Hi Chef, who's a good boy?"}, + variables: []string{"Chef", "boy"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "body", + Type: "body/text", + Variables: map[string]int{"1": 0, "2": 1}, + }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "text", Value: "Chef"}, + {Type: "text", Value: "boy"}, + }, + }, + expectedPreview: &flows.MsgContent{Text: "Hi Chef, who's a good boy?"}, }, { // 2: multiple text component types - translation: []byte(`{ - "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, - "locale": "eng", - "components": [ - { - "name": "header", - "type": "header/text", - "content": "Header {{1}}", - "variables": {"1": 0} + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "header", + "type": "header/text", + "content": "Header {{1}}", + "variables": {"1": 0} + }, + { + "name": "body", + "type": "body/text", + "content": "Body {{1}}", + "variables": {"1": 1} + }, + { + "name": "footer", + "type": "footer/text", + "content": "Footer {{1}}", + "variables": {"1": 2} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"}, + {"type": "text"} + ] + } + ] + }`), + variables: []string{"A", "B", "C"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "header", + Type: "header/text", + Variables: map[string]int{"1": 0}, }, { - "name": "body", - "type": "body/text", - "content": "Body {{1}}", - "variables": {"1": 1} + Name: "body", + Type: "body/text", + Variables: map[string]int{"1": 1}, }, { - "name": "footer", - "type": "footer/text", - "content": "Footer {{1}}", - "variables": {"1": 2} + Name: "footer", + Type: "footer/text", + Variables: map[string]int{"1": 2}, + }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "text", Value: "A"}, + {Type: "text", Value: "B"}, + {Type: "text", Value: "C"}, + }, + }, + expectedPreview: &flows.MsgContent{Text: "Header A\n\nBody B\n\nFooter C"}, + }, + { // 3: buttons become quick replies + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "button.1", + "type": "button/quick_reply", + "content": "{{1}}", + "variables": {"1": 0} + }, + { + "name": "button.2", + "type": "button/quick_reply", + "content": "{{1}}", + "variables": {"1": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] } - ], - "variables": [ - {"type": "text"}, - {"type": "text"}, - {"type": "text"} ] }`), - variables: []*flows.TemplatingVariable{{Type: "text", Value: "A"}, {Type: "text", Value: "B"}, {Type: "text", Value: "C"}}, - expected: &flows.MsgContent{Text: "Header A\n\nBody B\n\nFooter C"}, + variables: []string{"Yes", "No"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "button.1", + Type: "button/quick_reply", + Variables: map[string]int{"1": 0}, + }, + { + Name: "button.2", + Type: "button/quick_reply", + Variables: map[string]int{"1": 1}, + }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "text", Value: "Yes"}, + {Type: "text", Value: "No"}, + }, + }, + expectedPreview: &flows.MsgContent{QuickReplies: []string{"Yes", "No"}}, }, - { // 3: buttons become quick replies - translation: []byte(`{ - "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, - "locale": "eng", - "components": [ - { - "name": "button.1", - "type": "button/quick_reply", - "content": "{{1}}", - "variables": {"1": 0} + { // 4: header image becomes an attachment + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "header", + "type": "header/image", + "content": "{{1}}", + "variables": {"1": 0} + } + ], + "variables": [ + {"type": "image"} + ] + } + ] + }`), + variables: []string{"image/jpeg:http://example.com/test.jpg"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "header", + Type: "header/image", + Variables: map[string]int{"1": 0}, }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "image", Value: "image/jpeg:http://example.com/test.jpg"}, + }, + }, + expectedPreview: &flows.MsgContent{Attachments: []utils.Attachment{"image/jpeg:http://example.com/test.jpg"}}, + }, + { // 5: missing variables padded with empty + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ { - "name": "button.2", - "type": "button/quick_reply", - "content": "{{1}}", - "variables": {"1": 1} + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "body", + "type": "body/text", + "content": "Hi {{1}}, who's a good {{2}}?", + "variables": {"1": 0, "2": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] } - ], - "variables": [ - {"type": "text"}, - {"type": "text"} ] }`), - variables: []*flows.TemplatingVariable{{Type: "text", Value: "Yes"}, {Type: "text", Value: "No"}}, - expected: &flows.MsgContent{QuickReplies: []string{"Yes", "No"}}, + variables: []string{"Chef"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "body", + Type: "body/text", + Variables: map[string]int{"1": 0, "2": 1}, + }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "text", Value: "Chef"}, + {Type: "text", Value: ""}, + }, + }, + expectedPreview: &flows.MsgContent{Text: "Hi Chef, who's a good ?"}, }, - { // 4: header image becomes an attachment - translation: []byte(`{ - "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, - "locale": "eng", - "components": [ - { - "name": "header", - "type": "header/image", - "content": "{{1}}", - "variables": {"1": 0} + { // 6: excess variables ignored + template: []byte(`{ + "uuid": "4c01c732-e644-421c-af15-f5606c3e05f0", + "name": "greeting", + "translations": [ + { + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "body", + "type": "body/text", + "content": "Hi {{1}}, who's a good {{2}}?", + "variables": {"1": 0, "2": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] } - ], - "variables": [ - {"type": "image"} ] }`), - variables: []*flows.TemplatingVariable{{Type: "image", Value: "image/jpeg:http://example.com/test.jpg"}}, - expected: &flows.MsgContent{Attachments: []utils.Attachment{"image/jpeg:http://example.com/test.jpg"}}, + variables: []string{"Chef", "boy", "dog"}, + expectedTemplating: &flows.MsgTemplating{ + Template: assets.NewTemplateReference("4c01c732-e644-421c-af15-f5606c3e05f0", "greeting"), + Components: []*flows.TemplatingComponent{ + { + Name: "body", + Type: "body/text", + Variables: map[string]int{"1": 0, "2": 1}, + }, + }, + Variables: []*flows.TemplatingVariable{ + {Type: "text", Value: "Chef"}, + {Type: "text", Value: "boy"}, + }, + }, + expectedPreview: &flows.MsgContent{Text: "Hi Chef, who's a good boy?"}, }, } for i, tc := range tcs { - trans := &static.TemplateTranslation{} - jsonx.MustUnmarshal(tc.translation, trans) + tplAsset := &static.Template{} + jsonx.MustUnmarshal(tc.template, tplAsset) - actual := flows.NewTemplateTranslation(trans).Preview(tc.variables) - assert.Equal(t, tc.expected, actual, "%d: preview mismatch", i) + tpl := flows.NewTemplate(tplAsset) + trans := tpl.FindTranslation(channel, []i18n.Locale{"eng"}) + if assert.NotNil(t, trans, "%d: translation not found", i) { + // check templating + templating := tpl.Templating(trans, tc.variables) + + if assert.Equal(t, tc.expectedTemplating, templating, "%d: preview mismatch", i) { + actualPreview := flows.NewTemplateTranslation(trans).Preview(templating.Variables) + assert.Equal(t, tc.expectedPreview, actualPreview, "%d: preview mismatch", i) + } + } } }