Skip to content

Commit

Permalink
Convert quick replies into structs
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Feb 13, 2025
1 parent 36eca28 commit abb7591
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 22 deletions.
4 changes: 2 additions & 2 deletions flows/actions/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ func (a *baseAction) evaluateMessage(run flows.Run, languages []i18n.Language, a

// localize and evaluate the quick replies
translatedQuickReplies, qrsLang := run.GetTextArray(uuids.UUID(a.UUID()), "quick_replies", actionQuickReplies, languages)
evaluatedQuickReplies := make([]string, 0, len(translatedQuickReplies))
evaluatedQuickReplies := make([]flows.QuickReply, 0, len(translatedQuickReplies))
for _, qr := range translatedQuickReplies {
evaluatedQuickReply, _ := run.EvaluateTemplate(qr, logEvent)
if evaluatedQuickReply == "" {
logEvent(events.NewErrorf("quick reply evaluated to empty string, skipping"))
continue
}
evaluatedQuickReplies = append(evaluatedQuickReplies, stringsx.TruncateEllipsis(evaluatedQuickReply, flows.MaxQuickReplyLength))
evaluatedQuickReplies = append(evaluatedQuickReplies, flows.QuickReply{Text: stringsx.TruncateEllipsis(evaluatedQuickReply, flows.MaxQuickReplyLength)})
}

// although it's possible for the different parts of the message to have different languages, we want to resolve
Expand Down
2 changes: 1 addition & 1 deletion flows/events/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ func TestEventMarshaling(t *testing.T) {
&flows.MsgContent{
Text: "Hi there",
Attachments: []utils.Attachment{"image/jpeg:http://s3.amazon.com/bucket/test.jpg"},
QuickReplies: []string{"yes", "no"},
QuickReplies: []flows.QuickReply{{Text: "yes"}, {Text: "no"}},
},
nil,
flows.MsgTopicAgent,
Expand Down
29 changes: 26 additions & 3 deletions flows/msg.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package flows

import (
"encoding/json"
"fmt"
"slices"

"github.com/go-playground/validator/v10"
"github.com/nyaruka/gocommon/i18n"
"github.com/nyaruka/gocommon/jsonx"
"github.com/nyaruka/gocommon/urns"
"github.com/nyaruka/gocommon/uuids"
"github.com/nyaruka/goflow/assets"
Expand Down Expand Up @@ -66,7 +68,7 @@ type MsgIn struct {
type MsgOut struct {
BaseMsg

QuickReplies_ []string `json:"quick_replies,omitempty"`
QuickReplies_ []QuickReply `json:"quick_replies,omitempty"`
Templating_ *MsgTemplating `json:"templating,omitempty"`
Topic_ MsgTopic `json:"topic,omitempty"`
Locale_ i18n.Locale `json:"locale,omitempty"`
Expand Down Expand Up @@ -157,7 +159,7 @@ func (m *MsgIn) ExternalID() string { return m.ExternalID_ }
func (m *MsgIn) SetExternalID(id string) { m.ExternalID_ = id }

// QuickReplies returns the quick replies of this outgoing message
func (m *MsgOut) QuickReplies() []string { return m.QuickReplies_ }
func (m *MsgOut) QuickReplies() []QuickReply { return m.QuickReplies_ }

// Templating returns the templating to use to send this message (if any)
func (m *MsgOut) Templating() *MsgTemplating { return m.Templating_ }
Expand Down Expand Up @@ -194,11 +196,32 @@ func NewMsgTemplating(template *assets.TemplateReference, components []*Templati
return &MsgTemplating{Template: template, Components: components, Variables: variables}
}

type QuickReply struct {
Text string `json:"text"`
}

func (q QuickReply) MarshalJSON() ([]byte, error) {
// TODO for now we always marshal as a string but once everything can unmarshal as a struct we can change this
return json.Marshal(q.Text)
}

func (q *QuickReply) UnmarshalJSON(d []byte) error {
// if we have a string we unmarshal it into the text field
if len(d) > 2 && d[0] == '"' && d[len(d)-1] == '"' {
return jsonx.Unmarshal(d, &q.Text)
}

// alias our type so we don't end up here again
type alias QuickReply

return jsonx.Unmarshal(d, (*alias)(q))
}

// MsgContent is message content in a particular language
type MsgContent struct {
Text string `json:"text"`
Attachments []utils.Attachment `json:"attachments,omitempty"`
QuickReplies []string `json:"quick_replies,omitempty"`
QuickReplies []QuickReply `json:"quick_replies,omitempty"`
}

func (c *MsgContent) Empty() bool {
Expand Down
27 changes: 22 additions & 5 deletions flows/msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TestMsgContent(t *testing.T) {
assert.True(t, (&flows.MsgContent{}).Empty())
assert.False(t, (&flows.MsgContent{Text: "hi"}).Empty())
assert.False(t, (&flows.MsgContent{Attachments: []utils.Attachment{"image:https://test.jpg"}}).Empty())
assert.False(t, (&flows.MsgContent{QuickReplies: []string{"Ok"}}).Empty())
assert.False(t, (&flows.MsgContent{QuickReplies: []flows.QuickReply{{Text: "Ok"}}}).Empty())
}

func TestBroadcastTranslations(t *testing.T) {
Expand Down Expand Up @@ -182,24 +182,24 @@ func TestBroadcastTranslations(t *testing.T) {
{ // 4: merges content from different translations
env: envs.NewBuilder().WithAllowedLanguages("eng", "spa").WithDefaultCountry("US").Build(),
translations: flows.BroadcastTranslations{
"eng": &flows.MsgContent{Text: "Hello", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hello.jpg"}, QuickReplies: []string{"Yes", "No"}},
"eng": &flows.MsgContent{Text: "Hello", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hello.jpg"}, QuickReplies: []flows.QuickReply{{Text: "Yes"}, {Text: "No"}}},
"spa": &flows.MsgContent{Text: "Hola"},
},
baseLanguage: "eng",
contactLanguage: "spa",
expectedContent: &flows.MsgContent{Text: "Hola", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hello.jpg"}, QuickReplies: []string{"Yes", "No"}},
expectedContent: &flows.MsgContent{Text: "Hola", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hello.jpg"}, QuickReplies: []flows.QuickReply{{Text: "Yes"}, {Text: "No"}}},
expectedLocale: "spa-US",
},
{ // 5: merges content from different translations
env: envs.NewBuilder().WithAllowedLanguages("eng", "spa").WithDefaultCountry("US").Build(),
translations: flows.BroadcastTranslations{
"eng": &flows.MsgContent{QuickReplies: []string{"Yes", "No"}},
"eng": &flows.MsgContent{QuickReplies: []flows.QuickReply{{Text: "Yes"}, {Text: "No"}}},
"spa": &flows.MsgContent{Attachments: []utils.Attachment{"image/jpeg:https://example.com/hola.jpg"}},
"kin": &flows.MsgContent{Text: "Muraho"},
},
baseLanguage: "kin",
contactLanguage: "spa",
expectedContent: &flows.MsgContent{Text: "Muraho", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hola.jpg"}, QuickReplies: []string{"Yes", "No"}},
expectedContent: &flows.MsgContent{Text: "Muraho", Attachments: []utils.Attachment{"image/jpeg:https://example.com/hola.jpg"}, QuickReplies: []flows.QuickReply{{Text: "Yes"}, {Text: "No"}}},
expectedLocale: "kin-US",
},
}
Expand Down Expand Up @@ -260,3 +260,20 @@ func TestMsgTemplating(t *testing.T) {
]
}`), marshaled, "JSON mismatch")
}

func TestQuickReplies(t *testing.T) {
// can unmarshal from a string
qr1 := flows.QuickReply{}
jsonx.MustUnmarshal([]byte(`"Yes"`), &qr1)
assert.Equal(t, flows.QuickReply{Text: "Yes"}, qr1)

// can unmarshal from a struct
qr2 := flows.QuickReply{}
jsonx.MustUnmarshal([]byte(`{"text": "No"}`), &qr2)
assert.Equal(t, flows.QuickReply{Text: "No"}, qr2)

// always marshals as a string for now
assert.Equal(t, []byte(`"Yes"`), jsonx.MustMarshal(qr1))
assert.Equal(t, []byte(`"No"`), jsonx.MustMarshal(qr2))
assert.Equal(t, []byte(`["Yes","No"]`), jsonx.MustMarshal([]flows.QuickReply{qr1, qr2}))
}
16 changes: 8 additions & 8 deletions flows/runs/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func TestTranslation(t *testing.T) {
msgAction []byte
expectedText string
expectedAttachments []utils.Attachment
expectedQuickReplies []string
expectedQuickReplies []flows.QuickReply
}{
{
description: "contact language is valid and is flow base language, msg action has all fields",
Expand All @@ -364,7 +364,7 @@ func TestTranslation(t *testing.T) {
"image/jpeg:http://media.com/hello.jpg",
"audio/mp4:http://media.com/hello.m4a",
},
expectedQuickReplies: []string{"yes", "no"},
expectedQuickReplies: []flows.QuickReply{{Text: "yes"}, {Text: "no"}},
},
{
description: "contact language is valid and translations exist, msg action has all fields",
Expand All @@ -375,7 +375,7 @@ func TestTranslation(t *testing.T) {
expectedAttachments: []utils.Attachment{
"audio/mp4:http://media.com/hola.m4a",
},
expectedQuickReplies: []string{"si"},
expectedQuickReplies: []flows.QuickReply{{Text: "si"}},
},
{
description: "contact language is allowed but no translations exist, msg action has all fields",
Expand All @@ -387,7 +387,7 @@ func TestTranslation(t *testing.T) {
"image/jpeg:http://media.com/hello.jpg",
"audio/mp4:http://media.com/hello.m4a",
},
expectedQuickReplies: []string{"yes", "no"},
expectedQuickReplies: []flows.QuickReply{{Text: "yes"}, {Text: "no"}},
},
{
description: "contact language is not allowed and translations exist, msg action has all fields",
Expand All @@ -399,7 +399,7 @@ func TestTranslation(t *testing.T) {
"image/jpeg:http://media.com/hello.jpg",
"audio/mp4:http://media.com/hello.m4a",
},
expectedQuickReplies: []string{"yes", "no"},
expectedQuickReplies: []flows.QuickReply{{Text: "yes"}, {Text: "no"}},
},
{
description: "contact language is valid and is flow base language, msg action only has text",
Expand All @@ -408,7 +408,7 @@ func TestTranslation(t *testing.T) {
msgAction: msgAction2,
expectedText: "Hello",
expectedAttachments: []utils.Attachment{},
expectedQuickReplies: []string{},
expectedQuickReplies: []flows.QuickReply{},
},
{
description: "contact language is valid and translations exist, msg action only has text",
Expand All @@ -419,7 +419,7 @@ func TestTranslation(t *testing.T) {
expectedAttachments: []utils.Attachment{
"audio/mp4:http://media.com/hola.m4a",
},
expectedQuickReplies: []string{"si"},
expectedQuickReplies: []flows.QuickReply{{Text: "si"}},
},
{
description: "attachments and quick replies translations are single empty strings and should be ignored",
Expand All @@ -431,7 +431,7 @@ func TestTranslation(t *testing.T) {
"image/jpeg:http://media.com/hello.jpg",
"audio/mp4:http://media.com/hello.m4a",
},
expectedQuickReplies: []string{"yes", "no"},
expectedQuickReplies: []flows.QuickReply{{Text: "yes"}, {Text: "no"}},
},
}

Expand Down
4 changes: 2 additions & 2 deletions flows/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (t *TemplateTranslation) Asset() assets.TemplateTranslation { return t.Temp
func (t *TemplateTranslation) Preview(vars []*TemplatingVariable) *MsgContent {
var text []string
var attachments []utils.Attachment
var quickReplies []string
var quickReplies []QuickReply

for _, comp := range t.Components() {
content := comp.Content()
Expand All @@ -112,7 +112,7 @@ func (t *TemplateTranslation) Preview(vars []*TemplatingVariable) *MsgContent {
if comp.Type() == "header/text" || comp.Type() == "body/text" || comp.Type() == "footer/text" {
text = append(text, content)
} else if strings.HasPrefix(comp.Type(), "button/") {
quickReplies = append(quickReplies, stringsx.TruncateEllipsis(content, MaxQuickReplyLength))
quickReplies = append(quickReplies, QuickReply{Text: stringsx.TruncateEllipsis(content, MaxQuickReplyLength)})
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion flows/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func TestTemplating(t *testing.T) {
{Type: "text", Value: "No"},
},
},
expectedPreview: &flows.MsgContent{QuickReplies: []string{"Yes", "No"}},
expectedPreview: &flows.MsgContent{QuickReplies: []flows.QuickReply{{Text: "Yes"}, {Text: "No"}}},
},
{ // 4: header image becomes an attachment
template: []byte(`{
Expand Down

0 comments on commit abb7591

Please sign in to comment.