From e1ee1e46524c9031990123d25c72e4300f29bd51 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 15 Mar 2018 10:58:46 -0500 Subject: [PATCH 1/5] Break dependency between legacy tests and definitions package --- flows/actions/send_broadcast.go | 2 +- flows/definition/flow.go | 22 +++++++++++----------- flows/definition/legacy.go | 28 ++++++++++++++-------------- flows/definition/legacy_test.go | 17 ++++++++--------- flows/definition/localization.go | 19 +++++++++++++++---- flows/interfaces.go | 8 ++++---- flows/runs/run.go | 2 +- 7 files changed, 54 insertions(+), 44 deletions(-) diff --git a/flows/actions/send_broadcast.go b/flows/actions/send_broadcast.go index d63b32396..040743dd7 100644 --- a/flows/actions/send_broadcast.go +++ b/flows/actions/send_broadcast.go @@ -53,7 +53,7 @@ func (a *SendBroadcastAction) Execute(run flows.FlowRun, step flows.Step, log fl } translations := make(map[utils.Language]*events.BroadcastTranslation) - languages := append(utils.LanguageList{run.Flow().Language()}, run.Flow().Translations().Languages()...) + languages := append(utils.LanguageList{run.Flow().Language()}, run.Flow().Localization().Languages()...) // evaluate the broadcast in each language we have translations for for _, language := range languages { diff --git a/flows/definition/flow.go b/flows/definition/flow.go index e61b0aeed..bada0c4b6 100644 --- a/flows/definition/flow.go +++ b/flows/definition/flow.go @@ -14,7 +14,7 @@ type flow struct { language utils.Language expireAfterMinutes int - translations flows.FlowTranslations + localization flows.Localization nodes []flows.Node nodeMap map[flows.NodeUUID]flows.Node @@ -25,7 +25,7 @@ func (f *flow) Name() string { return f.name } func (f *flow) Language() utils.Language { return f.language } func (f *flow) ExpireAfterMinutes() int { return f.expireAfterMinutes } func (f *flow) Nodes() []flows.Node { return f.nodes } -func (f *flow) Translations() flows.FlowTranslations { return f.translations } +func (f *flow) Localization() flows.Localization { return f.localization } func (f *flow) GetNode(uuid flows.NodeUUID) flows.Node { return f.nodeMap[uuid] } // Validates that structurally we are sane. IE, all required fields are present and @@ -77,12 +77,12 @@ var _ utils.VariableResolver = (*flow)(nil) //------------------------------------------------------------------------------------------ type flowEnvelope struct { - UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` - Name string `json:"name" validate:"required"` - Language utils.Language `json:"language"` - ExpireAfterMinutes int `json:"expire_after_minutes"` - Localization flowTranslations `json:"localization"` - Nodes []*node `json:"nodes"` + UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` + Name string `json:"name" validate:"required"` + Language utils.Language `json:"language"` + ExpireAfterMinutes int `json:"expire_after_minutes"` + Localization localization `json:"localization"` + Nodes []*node `json:"nodes"` // only for writing out, optional Metadata map[string]interface{} `json:"_ui,omitempty"` @@ -100,7 +100,7 @@ func ReadFlow(data json.RawMessage) (flows.Flow, error) { f.name = envelope.Name f.language = envelope.Language f.expireAfterMinutes = envelope.ExpireAfterMinutes - f.translations = envelope.Localization + f.localization = envelope.Localization f.nodes = make([]flows.Node, len(envelope.Nodes)) f.nodeMap = make(map[flows.NodeUUID]flows.Node) @@ -146,8 +146,8 @@ func (f *flow) MarshalJSON() ([]byte, error) { fe.Language = f.language fe.ExpireAfterMinutes = f.expireAfterMinutes - if f.translations != nil { - fe.Localization = *f.translations.(*flowTranslations) + if f.localization != nil { + fe.Localization = *f.localization.(*localization) } fe.Nodes = make([]*node, len(f.nodes)) diff --git a/flows/definition/legacy.go b/flows/definition/legacy.go index 615339e7f..cb24dd4da 100644 --- a/flows/definition/legacy.go +++ b/flows/definition/legacy.go @@ -286,7 +286,7 @@ type wardTest struct { type localizations map[utils.Language]flows.Action -func addTranslationMap(baseLanguage utils.Language, translations *flowTranslations, mapped map[utils.Language]string, uuid utils.UUID, key string) string { +func addTranslationMap(baseLanguage utils.Language, translations *localization, mapped map[utils.Language]string, uuid utils.UUID, key string) string { var inBaseLanguage string for language, item := range mapped { expression, _ := legacy.MigrateTemplate(item, legacy.ExtraAsFunction) @@ -300,7 +300,7 @@ func addTranslationMap(baseLanguage utils.Language, translations *flowTranslatio return inBaseLanguage } -func addTranslationMultiMap(baseLanguage utils.Language, translations *flowTranslations, mapped map[utils.Language][]string, uuid utils.UUID, key string) []string { +func addTranslationMultiMap(baseLanguage utils.Language, translations *localization, mapped map[utils.Language][]string, uuid utils.UUID, key string) []string { var inBaseLanguage []string for language, items := range mapped { expressions := make([]string, len(items)) @@ -317,7 +317,7 @@ func addTranslationMultiMap(baseLanguage utils.Language, translations *flowTrans return inBaseLanguage } -func addTranslation(translations *flowTranslations, lang utils.Language, itemUUID utils.UUID, propKey string, translation []string) { +func addTranslation(translations *localization, lang utils.Language, itemUUID utils.UUID, propKey string, translation []string) { // ensure we have a translation set for this language langTranslations, found := (*translations)[lang] if !found { @@ -339,7 +339,7 @@ func addTranslation(translations *flowTranslations, lang utils.Language, itemUUI // // [{"eng": "yes", "fra": "oui"}, {"eng": "no", "fra": "non"}] becomes {"eng": ["yes", "no"], "fra": ["oui", "non"]} // -func transformTranslations(items []map[utils.Language]string) map[utils.Language][]string { +func TransformTranslations(items []map[utils.Language]string) map[utils.Language][]string { // re-organize into a map of arrays transformed := make(map[utils.Language][]string) @@ -385,7 +385,7 @@ var testTypeMappings = map[string]string{ } // migrates the given legacy action to a new action -func migrateAction(baseLanguage utils.Language, a legacyAction, translations *flowTranslations) (flows.Action, error) { +func migrateAction(baseLanguage utils.Language, a legacyAction, translations *localization) (flows.Action, error) { switch a.Type { case "add_label": labels := make([]*flows.LabelReference, len(a.Labels)) @@ -488,7 +488,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *fl return nil, err } - quickReplies = transformTranslations(legacyQuickReplies) + quickReplies = TransformTranslations(legacyQuickReplies) } migratedText := addTranslationMap(baseLanguage, translations, msg, utils.UUID(a.UUID), "text") @@ -613,7 +613,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *fl } // migrates the given legacy rule to a router case -func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r legacyRule, translations *flowTranslations) (routers.Case, error) { +func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r legacyRule, translations *localization) (routers.Case, error) { category := r.Category[baseLanguage] newType, _ := testTypeMappings[r.Test.Type] @@ -727,7 +727,7 @@ type categoryName struct { order int } -func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *flowTranslations) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { +func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *localization) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { // find our discrete categories categoryMap := make(map[string]categoryName) @@ -807,7 +807,7 @@ type fieldConfig struct { } // migrates the given legacy rulset to a node with a router -func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *flowTranslations) (*node, error) { +func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localization) (*node, error) { node := &node{} node.uuid = r.UUID @@ -917,7 +917,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *flowTran } // migrates the given legacy actionset to a node with a set of migrated actions and a single exit -func migateActionSet(lang utils.Language, a legacyActionSet, translations *flowTranslations) (*node, error) { +func migateActionSet(lang utils.Language, a legacyActionSet, translations *localization) (*node, error) { node := &node{ uuid: a.UUID, actions: make([]flows.Action, len(a.Actions)), @@ -966,7 +966,7 @@ func ReadLegacyFlow(data json.RawMessage) (*LegacyFlow, error) { f.language = envelope.BaseLanguage f.expireAfterMinutes = envelope.Metadata.Expires - translations := &flowTranslations{} + translations := &localization{} f.nodes = make([]flows.Node, len(envelope.ActionSets)+len(envelope.RuleSets)) for i := range envelope.ActionSets { @@ -994,7 +994,7 @@ func ReadLegacyFlow(data json.RawMessage) (*LegacyFlow, error) { } } - f.translations = translations + f.localization = translations f.envelope = envelope return f, err @@ -1009,8 +1009,8 @@ func (f *LegacyFlow) MarshalJSON() ([]byte, error) { fe.Language = f.language fe.ExpireAfterMinutes = f.expireAfterMinutes - if f.translations != nil { - fe.Localization = *f.translations.(*flowTranslations) + if f.localization != nil { + fe.Localization = *f.localization.(*localization) } fe.Nodes = make([]*node, len(f.nodes)) diff --git a/flows/definition/legacy_test.go b/flows/definition/legacy_test.go index 06bda9a84..2352059bf 100644 --- a/flows/definition/legacy_test.go +++ b/flows/definition/legacy_test.go @@ -1,4 +1,4 @@ -package definition +package definition_test import ( "encoding/json" @@ -12,6 +12,7 @@ import ( "github.com/buger/jsonparser" "github.com/nyaruka/goflow/flows" + "github.com/nyaruka/goflow/flows/definition" "github.com/nyaruka/goflow/flows/routers" "github.com/nyaruka/goflow/utils" "github.com/stretchr/testify/assert" @@ -243,15 +244,14 @@ func TestRuleSetMigration(t *testing.T) { } } -func readLegacyTestFlows(flowsJSON string) ([]*LegacyFlow, error) { +func readLegacyTestFlows(flowsJSON string) ([]*definition.LegacyFlow, error) { var legacyFlows []json.RawMessage json.Unmarshal(json.RawMessage(flowsJSON), &legacyFlows) - return ReadLegacyFlows(legacyFlows) + return definition.ReadLegacyFlows(legacyFlows) } -func checkFlowLocalization(t *testing.T, flow *LegacyFlow, expectedLocalizationRaw json.RawMessage, substitutionSource json.RawMessage) { - actualLocalization := *flow.translations.(*flowTranslations) - actualLocalizationRaw, _ := json.Marshal(actualLocalization) +func checkFlowLocalization(t *testing.T, flow *definition.LegacyFlow, expectedLocalizationRaw json.RawMessage, substitutionSource json.RawMessage) { + actualLocalizationRaw, _ := json.Marshal(flow.Localization()) actualLocalizationJSON := formatJSON(actualLocalizationRaw) // Because localization keys are UUIDs and some of those may be generated during migration, ordering of localized @@ -270,8 +270,7 @@ func checkFlowLocalization(t *testing.T, flow *LegacyFlow, expectedLocalizationR }) // unmarshal and re-marchal expected JSON to get ordering correct after substitutions - expectedLocalization := &flowTranslations{} - json.Unmarshal(json.RawMessage(expectedLocalizationStr), expectedLocalization) + expectedLocalization, _ := definition.ReadLocalization(json.RawMessage(expectedLocalizationStr)) expectedLocalizationRaw, _ = json.Marshal(expectedLocalization) expectedLocalizationJSON := formatJSON(expectedLocalizationRaw) @@ -310,5 +309,5 @@ func TestTranslations(t *testing.T) { assert.Equal(t, map[utils.Language][]string{ "eng": {"Yes", "No", "Maybe", "Never"}, "fra": {"Oui", "Non", "", "Jamas"}, - }, transformTranslations(translations)) + }, definition.TransformTranslations(translations)) } diff --git a/flows/definition/localization.go b/flows/definition/localization.go index d7be167c6..5bbf2d205 100644 --- a/flows/definition/localization.go +++ b/flows/definition/localization.go @@ -1,6 +1,8 @@ package definition import ( + "encoding/json" + "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/utils" ) @@ -22,10 +24,10 @@ func (t *languageTranslations) GetTextArray(uuid utils.UUID, key string) []strin return nil } -// flowTranslations are our top level container for all the translations for a language -type flowTranslations map[utils.Language]*languageTranslations +// our top level container for all the translations for all languages +type localization map[utils.Language]*languageTranslations -func (t flowTranslations) Languages() utils.LanguageList { +func (t localization) Languages() utils.LanguageList { languages := make(utils.LanguageList, 0, len(t)) for lang := range t { languages = append(languages, lang) @@ -33,10 +35,19 @@ func (t flowTranslations) Languages() utils.LanguageList { return languages } -func (t flowTranslations) GetLanguageTranslations(lang utils.Language) flows.Translations { +func (t localization) GetTranslations(lang utils.Language) flows.Translations { translations, found := t[lang] if found { return translations } return nil } + +// ReadLocalization reads entire localization flow segment +func ReadLocalization(data json.RawMessage) (flows.Localization, error) { + translations := &localization{} + if err := json.Unmarshal(data, translations); err != nil { + return nil, err + } + return translations, nil +} diff --git a/flows/interfaces.go b/flows/interfaces.go index 8a0d7f0fb..ba0f25a72 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -136,7 +136,7 @@ type Flow interface { Name() string Language() utils.Language ExpireAfterMinutes() int - Translations() FlowTranslations + Localization() Localization Validate(SessionAssets) error Nodes() []Node @@ -202,9 +202,9 @@ type Wait interface { ResumeByTimeOut(FlowRun) } -// FlowTranslations provide a way to get the Translations for a flow for a specific language -type FlowTranslations interface { - GetLanguageTranslations(utils.Language) Translations +// Localization provide a way to get the translations for a specific language +type Localization interface { + GetTranslations(utils.Language) Translations Languages() utils.LanguageList } diff --git a/flows/runs/run.go b/flows/runs/run.go index bf563dfe8..891f99ba8 100644 --- a/flows/runs/run.go +++ b/flows/runs/run.go @@ -277,7 +277,7 @@ func (r *flowRun) GetTranslatedTextArray(uuid utils.UUID, key string, native []s return native } - translations := r.Flow().Translations().GetLanguageTranslations(lang) + translations := r.Flow().Localization().GetTranslations(lang) if translations != nil { textArray := translations.GetTextArray(uuid, key) if textArray == nil { From 95bc7abae99f48a7aff45fe3cd2233a1d85a617a Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 15 Mar 2018 14:01:19 -0500 Subject: [PATCH 2/5] Move legacy definition stuff from definition package to legacy package --- cmd/flowserver/server.go | 12 +- flows/definition/flow.go | 127 +++++---- flows/definition/localization.go | 55 +++- flows/definition/node.go | 11 + flows/interfaces.go | 1 + .../legacy.go => legacy/definition.go | 257 +++++++----------- .../definition_test.go | 63 ++--- .../testdata/migrations/actions.json | 0 .../testdata/migrations/rulesets.json | 8 +- .../testdata/migrations/tests.json | 0 10 files changed, 267 insertions(+), 267 deletions(-) rename flows/definition/legacy.go => legacy/definition.go (76%) rename flows/definition/legacy_test.go => legacy/definition_test.go (90%) rename {flows/definition => legacy}/testdata/migrations/actions.json (100%) rename {flows/definition => legacy}/testdata/migrations/rulesets.json (98%) rename {flows/definition => legacy}/testdata/migrations/tests.json (100%) diff --git a/cmd/flowserver/server.go b/cmd/flowserver/server.go index 07ac8bf8f..6bac5124d 100644 --- a/cmd/flowserver/server.go +++ b/cmd/flowserver/server.go @@ -11,10 +11,10 @@ import ( "github.com/nyaruka/goflow/excellent" "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/flows/definition" "github.com/nyaruka/goflow/flows/engine" "github.com/nyaruka/goflow/flows/events" "github.com/nyaruka/goflow/flows/triggers" + "github.com/nyaruka/goflow/legacy" "github.com/nyaruka/goflow/utils" "github.com/go-chi/chi" @@ -270,11 +270,19 @@ func (s *FlowServer) handleMigrate(w http.ResponseWriter, r *http.Request) (inte return nil, fmt.Errorf("missing flows element") } - flows, err := definition.ReadLegacyFlows(migrate.Flows) + legacyFlows, err := legacy.ReadLegacyFlows(migrate.Flows) if err != nil { return nil, err } + flows := make([]flows.Flow, len(legacyFlows)) + for f := range legacyFlows { + flows[f], err = legacyFlows[f].Migrate() + if err != nil { + return nil, err + } + } + return flows, err } diff --git a/flows/definition/flow.go b/flows/definition/flow.go index bada0c4b6..0ce69359a 100644 --- a/flows/definition/flow.go +++ b/flows/definition/flow.go @@ -18,6 +18,46 @@ type flow struct { nodes []flows.Node nodeMap map[flows.NodeUUID]flows.Node + + // only read for legacy flows which are being migrated + ui map[string]interface{} +} + +type FlowObj = flow + +func NewFlow(uuid flows.FlowUUID, name string, language utils.Language, expireAfterMinutes int, localization flows.Localization, nodes []flows.Node, ui map[string]interface{}) (flows.Flow, error) { + f := &flow{ + uuid: uuid, + name: name, + language: language, + expireAfterMinutes: expireAfterMinutes, + localization: localization, + nodes: nodes, + ui: ui, + } + if err := f.buildNodeMap(); err != nil { + return nil, err + } + + // go back through nodes and perform basic structural validation + for _, node := range f.nodes { + + // check every exit has a valid destination + for _, exit := range node.Exits() { + if exit.DestinationNodeUUID() != "" && f.nodeMap[exit.DestinationNodeUUID()] == nil { + return nil, fmt.Errorf("destination %s of exit[uuid=%s] isn't a known node", exit.DestinationNodeUUID(), exit.UUID()) + } + } + + // and the router if there is one + if node.Router() != nil { + if err := node.Router().Validate(node.Exits()); err != nil { + return nil, fmt.Errorf("router is invalid on node[uuid=%s]: %v", node.UUID(), err) + } + } + } + + return f, nil } func (f *flow) UUID() flows.FlowUUID { return f.uuid } @@ -70,6 +110,19 @@ func (f *flow) Reference() *flows.FlowReference { return flows.NewFlowReference(f.uuid, f.name) } +func (f *flow) buildNodeMap() error { + f.nodeMap = make(map[flows.NodeUUID]flows.Node) + + for _, node := range f.nodes { + // make sure we haven't seen this node before + if f.nodeMap[node.UUID()] != nil { + return fmt.Errorf("duplicate node uuid: %s", node.UUID()) + } + f.nodeMap[node.UUID()] = node + } + return nil +} + var _ utils.VariableResolver = (*flow)(nil) //------------------------------------------------------------------------------------------ @@ -77,15 +130,13 @@ var _ utils.VariableResolver = (*flow)(nil) //------------------------------------------------------------------------------------------ type flowEnvelope struct { - UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` - Name string `json:"name" validate:"required"` - Language utils.Language `json:"language"` - ExpireAfterMinutes int `json:"expire_after_minutes"` - Localization localization `json:"localization"` - Nodes []*node `json:"nodes"` - - // only for writing out, optional - Metadata map[string]interface{} `json:"_ui,omitempty"` + UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` + Name string `json:"name" validate:"required"` + Language utils.Language `json:"language"` + ExpireAfterMinutes int `json:"expire_after_minutes"` + Localization localization `json:"localization"` + Nodes []*node `json:"nodes"` + UI map[string]interface{} `json:"_ui,omitempty"` } // ReadFlow reads a single flow definition from the passed in byte array @@ -94,57 +145,23 @@ func ReadFlow(data json.RawMessage) (flows.Flow, error) { if err := utils.UnmarshalAndValidate(data, &envelope, "flow"); err != nil { return nil, err } - - f := &flow{} - f.uuid = envelope.UUID - f.name = envelope.Name - f.language = envelope.Language - f.expireAfterMinutes = envelope.ExpireAfterMinutes - f.localization = envelope.Localization - - f.nodes = make([]flows.Node, len(envelope.Nodes)) - f.nodeMap = make(map[flows.NodeUUID]flows.Node) - - // for each node... - for n, node := range envelope.Nodes { - f.nodes[n] = node - - // make sure we haven't seen this node before - if f.nodeMap[node.UUID()] != nil { - return nil, fmt.Errorf("duplicate node uuid: %s", node.UUID()) - } - f.nodeMap[node.UUID()] = node + nodes := make([]flows.Node, len(envelope.Nodes)) + for n := range envelope.Nodes { + nodes[n] = envelope.Nodes[n] } - // go back through nodes and perform basic structural validation - for _, node := range f.nodes { - - // check every exit has a valid destination - for _, exit := range node.Exits() { - if exit.DestinationNodeUUID() != "" && f.nodeMap[exit.DestinationNodeUUID()] == nil { - return nil, fmt.Errorf("destination %s of exit[uuid=%s] isn't a known node", exit.DestinationNodeUUID(), exit.UUID()) - } - } - - // and the router if there is one - if node.Router() != nil { - if err := node.Router().Validate(node.Exits()); err != nil { - return nil, fmt.Errorf("router is invalid on node[uuid=%s]: %v", node.UUID(), err) - } - } - } - - return f, nil + return NewFlow(envelope.UUID, envelope.Name, envelope.Language, envelope.ExpireAfterMinutes, envelope.Localization, nodes, nil) } // MarshalJSON marshals this flow into JSON func (f *flow) MarshalJSON() ([]byte, error) { - - var fe = flowEnvelope{} - fe.UUID = f.uuid - fe.Name = f.name - fe.Language = f.language - fe.ExpireAfterMinutes = f.expireAfterMinutes + var fe = &flowEnvelope{ + UUID: f.uuid, + Name: f.name, + Language: f.language, + ExpireAfterMinutes: f.expireAfterMinutes, + UI: f.ui, + } if f.localization != nil { fe.Localization = *f.localization.(*localization) @@ -155,5 +172,5 @@ func (f *flow) MarshalJSON() ([]byte, error) { fe.Nodes[i] = f.nodes[i].(*node) } - return json.Marshal(&fe) + return json.Marshal(fe) } diff --git a/flows/definition/localization.go b/flows/definition/localization.go index 5bbf2d205..eb29ffa8b 100644 --- a/flows/definition/localization.go +++ b/flows/definition/localization.go @@ -7,16 +7,28 @@ import ( "github.com/nyaruka/goflow/utils" ) -// itemTranslations map a key for a node to a key - say "text" to "[je suis francais!]" +// the translations for a specific item, e.g. +// { +// "text": "Do you like cheese?" +// "quick_replies": ["Yes", "No"] +// } type itemTranslations map[string][]string -// languageTranslations map a node uuid to item_translations - say "node1-asdf" to { "text": "je suis francais!" } +// the translations for a specific language, e.g. +// { +// "f3368070-8db8-4549-872a-e69a9d060612": { +// "text": "Do you like cheese?" +// "quick_replies": ["Yes", "No"] +// }, +// "7a1aec43-f3e1-42f0-b967-0ee75e725e3a": { ... } +// } type languageTranslations map[utils.UUID]itemTranslations -func (t *languageTranslations) GetTextArray(uuid utils.UUID, key string) []string { - item, found := (*t)[uuid] +// GetTextArray returns the requested item translation +func (t languageTranslations) GetTextArray(uuid utils.UUID, property string) []string { + item, found := t[uuid] if found { - translation, found := item[key] + translation, found := item[property] if found { return translation } @@ -25,22 +37,37 @@ func (t *languageTranslations) GetTextArray(uuid utils.UUID, key string) []strin } // our top level container for all the translations for all languages -type localization map[utils.Language]*languageTranslations +type localization map[utils.Language]languageTranslations -func (t localization) Languages() utils.LanguageList { - languages := make(utils.LanguageList, 0, len(t)) - for lang := range t { +func NewLocalization() flows.Localization { + return make(localization) +} + +// Languages gets the list of languages included in this localization +func (l localization) Languages() utils.LanguageList { + languages := make(utils.LanguageList, 0, len(l)) + for lang := range l { languages = append(languages, lang) } return languages } -func (t localization) GetTranslations(lang utils.Language) flows.Translations { - translations, found := t[lang] - if found { - return translations +// AddItemTranslation adds a new item translation +func (l localization) AddItemTranslation(lang utils.Language, itemUUID utils.UUID, property string, translated []string) { + _, found := l[lang] + if !found { + l[lang] = make(languageTranslations) } - return nil + _, found = l[lang][itemUUID] + if !found { + l[lang][itemUUID] = make(itemTranslations) + } + l[lang][itemUUID][property] = translated +} + +// GetTranslations returns the translations for the given language +func (l localization) GetTranslations(lang utils.Language) flows.Translations { + return l[lang] } // ReadLocalization reads entire localization flow segment diff --git a/flows/definition/node.go b/flows/definition/node.go index 7869fea28..f71708516 100644 --- a/flows/definition/node.go +++ b/flows/definition/node.go @@ -34,6 +34,17 @@ type node struct { wait flows.Wait } +// NewNode creates a new flow node +func NewNode(uuid flows.NodeUUID, actions []flows.Action, router flows.Router, exits []flows.Exit, wait flows.Wait) flows.Node { + return &node{ + uuid: uuid, + actions: actions, + router: router, + exits: exits, + wait: wait, + } +} + func (n *node) UUID() flows.NodeUUID { return n.uuid } func (n *node) Router() flows.Router { return n.router } func (n *node) Actions() []flows.Action { return n.actions } diff --git a/flows/interfaces.go b/flows/interfaces.go index ba0f25a72..61aca049f 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -204,6 +204,7 @@ type Wait interface { // Localization provide a way to get the translations for a specific language type Localization interface { + AddItemTranslation(utils.Language, utils.UUID, string, []string) GetTranslations(utils.Language) Translations Languages() utils.LanguageList } diff --git a/flows/definition/legacy.go b/legacy/definition.go similarity index 76% rename from flows/definition/legacy.go rename to legacy/definition.go index cb24dd4da..411394e7e 100644 --- a/flows/definition/legacy.go +++ b/legacy/definition.go @@ -1,4 +1,4 @@ -package definition +package legacy import ( "encoding/json" @@ -8,9 +8,9 @@ import ( "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/actions" + "github.com/nyaruka/goflow/flows/definition" "github.com/nyaruka/goflow/flows/routers" "github.com/nyaruka/goflow/flows/waits" - "github.com/nyaruka/goflow/legacy" "github.com/nyaruka/goflow/utils" ) @@ -41,21 +41,16 @@ var legacyWebhookBody = `{ "channel": {} }` -// LegacyFlow imports an old-world flow so it can be exported anew +// LegacyFlow is a legacy style flow type LegacyFlow struct { - flow - envelope legacyFlowEnvelope + BaseLanguage utils.Language `json:"base_language"` + Metadata legacyMetadata `json:"metadata"` + RuleSets []legacyRuleSet `json:"rule_sets" validate:"dive"` + ActionSets []legacyActionSet `json:"action_sets" validate:"dive"` + Entry flows.NodeUUID `json:"entry" validate:"required,uuid4"` } -type legacyFlowEnvelope struct { - BaseLanguage utils.Language `json:"base_language"` - Metadata legacyMetadataEnvelope `json:"metadata"` - RuleSets []legacyRuleSet `json:"rule_sets" validate:"dive"` - ActionSets []legacyActionSet `json:"action_sets" validate:"dive"` - Entry flows.NodeUUID `json:"entry" validate:"required,uuid4"` -} - -type legacyMetadataEnvelope struct { +type legacyMetadata struct { UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` Name string `json:"name"` Expires int `json:"expires"` @@ -112,7 +107,7 @@ func (l *legacyLabelReference) UnmarshalJSON(data []byte) error { // if it starts with @ then it's an expression if strings.HasPrefix(nameExpression, "@") { - nameExpression, _ = legacy.MigrateTemplate(nameExpression, legacy.ExtraAsFunction) + nameExpression, _ = MigrateTemplate(nameExpression, ExtraAsFunction) } l.Name = nameExpression @@ -161,7 +156,7 @@ func (g *legacyGroupReference) UnmarshalJSON(data []byte) error { // if it starts with @ then it's an expression if strings.HasPrefix(nameExpression, "@") { - nameExpression, _ = legacy.MigrateTemplate(nameExpression, legacy.ExtraAsFunction) + nameExpression, _ = MigrateTemplate(nameExpression, ExtraAsFunction) } g.Name = nameExpression @@ -286,12 +281,12 @@ type wardTest struct { type localizations map[utils.Language]flows.Action -func addTranslationMap(baseLanguage utils.Language, translations *localization, mapped map[utils.Language]string, uuid utils.UUID, key string) string { +func addTranslationMap(baseLanguage utils.Language, localization flows.Localization, mapped map[utils.Language]string, uuid utils.UUID, property string) string { var inBaseLanguage string for language, item := range mapped { - expression, _ := legacy.MigrateTemplate(item, legacy.ExtraAsFunction) + expression, _ := MigrateTemplate(item, ExtraAsFunction) if language != baseLanguage { - addTranslation(translations, language, uuid, key, []string{expression}) + localization.AddItemTranslation(language, uuid, property, []string{expression}) } else { inBaseLanguage = expression } @@ -300,16 +295,16 @@ func addTranslationMap(baseLanguage utils.Language, translations *localization, return inBaseLanguage } -func addTranslationMultiMap(baseLanguage utils.Language, translations *localization, mapped map[utils.Language][]string, uuid utils.UUID, key string) []string { +func addTranslationMultiMap(baseLanguage utils.Language, localization flows.Localization, mapped map[utils.Language][]string, uuid utils.UUID, property string) []string { var inBaseLanguage []string for language, items := range mapped { expressions := make([]string, len(items)) for i := range items { - expression, _ := legacy.MigrateTemplate(items[i], legacy.ExtraAsFunction) + expression, _ := MigrateTemplate(items[i], ExtraAsFunction) expressions[i] = expression } if language != baseLanguage { - addTranslation(translations, language, uuid, key, expressions) + localization.AddItemTranslation(language, uuid, property, expressions) } else { inBaseLanguage = expressions } @@ -317,25 +312,7 @@ func addTranslationMultiMap(baseLanguage utils.Language, translations *localizat return inBaseLanguage } -func addTranslation(translations *localization, lang utils.Language, itemUUID utils.UUID, propKey string, translation []string) { - // ensure we have a translation set for this language - langTranslations, found := (*translations)[lang] - if !found { - langTranslations = &languageTranslations{} - (*translations)[lang] = langTranslations - } - - // ensure we have a translation set for this item - itemTrans, found := (*langTranslations)[itemUUID] - if !found { - itemTrans = itemTranslations{} - (*langTranslations)[itemUUID] = itemTrans - } - - itemTrans[propKey] = translation -} - -// Transforms a list of single item translations into a map of multi-item translations, e.g. +// TransformTranslations transforms a list of single item translations into a map of multi-item translations, e.g. // // [{"eng": "yes", "fra": "oui"}, {"eng": "no", "fra": "non"}] becomes {"eng": ["yes", "no"], "fra": ["oui", "non"]} // @@ -385,7 +362,7 @@ var testTypeMappings = map[string]string{ } // migrates the given legacy action to a new action -func migrateAction(baseLanguage utils.Language, a legacyAction, translations *localization) (flows.Action, error) { +func migrateAction(baseLanguage utils.Language, a legacyAction, localization flows.Localization) (flows.Action, error) { switch a.Type { case "add_label": labels := make([]*flows.LabelReference, len(a.Labels)) @@ -405,11 +382,11 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo return nil, err } - migratedSubject, _ := legacy.MigrateTemplate(a.Subject, legacy.ExtraAsFunction) - migratedBody, _ := legacy.MigrateTemplate(msg, legacy.ExtraAsFunction) + migratedSubject, _ := MigrateTemplate(a.Subject, ExtraAsFunction) + migratedBody, _ := MigrateTemplate(msg, ExtraAsFunction) migratedEmails := make([]string, len(a.Emails)) for e, email := range a.Emails { - migratedEmails[e], _ = legacy.MigrateTemplate(email, legacy.ExtraAsFunction) + migratedEmails[e], _ = MigrateTemplate(email, ExtraAsFunction) } return &actions.SendEmailAction{ @@ -450,7 +427,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo if variable.ID == "@new_contact" { createContact = true } else { - migratedVar, _ := legacy.MigrateTemplate(variable.ID, legacy.ExtraAsFunction) + migratedVar, _ := MigrateTemplate(variable.ID, ExtraAsFunction) variables = append(variables, migratedVar) } } @@ -491,9 +468,9 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo quickReplies = TransformTranslations(legacyQuickReplies) } - migratedText := addTranslationMap(baseLanguage, translations, msg, utils.UUID(a.UUID), "text") - migratedMedia := addTranslationMap(baseLanguage, translations, media, utils.UUID(a.UUID), "attachments") - migratedQuickReplies := addTranslationMultiMap(baseLanguage, translations, quickReplies, utils.UUID(a.UUID), "quick_replies") + migratedText := addTranslationMap(baseLanguage, localization, msg, utils.UUID(a.UUID), "text") + migratedMedia := addTranslationMap(baseLanguage, localization, media, utils.UUID(a.UUID), "attachments") + migratedQuickReplies := addTranslationMultiMap(baseLanguage, localization, quickReplies, utils.UUID(a.UUID), "quick_replies") attachments := []string{} if migratedMedia != "" { @@ -520,7 +497,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo } variables := make([]string, 0, len(a.Variables)) for _, variable := range a.Variables { - migratedVar, _ := legacy.MigrateTemplate(variable.ID, legacy.ExtraAsFunction) + migratedVar, _ := MigrateTemplate(variable.ID, ExtraAsFunction) variables = append(variables, migratedVar) } @@ -555,7 +532,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo BaseAction: actions.NewBaseAction(a.UUID), }, nil case "save": - migratedValue, _ := legacy.MigrateTemplate(a.Value, legacy.ExtraAsFunction) + migratedValue, _ := MigrateTemplate(a.Value, ExtraAsFunction) // flows now have different action for name changing if a.Field == "name" || a.Field == "first_name" { @@ -593,7 +570,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo BaseAction: actions.NewBaseAction(a.UUID), }, nil case "api": - migratedURL, _ := legacy.MigrateTemplate(a.Webhook, legacy.ExtraAsFunction) + migratedURL, _ := MigrateTemplate(a.Webhook, ExtraAsFunction) headers := make(map[string]string, len(a.WebhookHeaders)) for _, header := range a.WebhookHeaders { @@ -608,12 +585,12 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *lo Headers: headers, }, nil default: - return nil, fmt.Errorf("couldn't create action for %s", a.Type) + return nil, fmt.Errorf("unable to migrate legacy action type: %s", a.Type) } } // migrates the given legacy rule to a router case -func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r legacyRule, translations *localization) (routers.Case, error) { +func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r legacyRule, localization flows.Localization) (routers.Case, error) { category := r.Category[baseLanguage] newType, _ := testTypeMappings[r.Test.Type] @@ -633,7 +610,7 @@ func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r l case "eq", "gt", "gte", "lt", "lte": test := numericTest{} err = json.Unmarshal(r.Test.Data, &test) - migratedTest, err := legacy.MigrateTemplate(string(test.Test), legacy.ExtraAsFunction) + migratedTest, err := MigrateTemplate(string(test.Test), ExtraAsFunction) if err != nil { return routers.Case{}, err } @@ -642,11 +619,11 @@ func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r l case "between": test := betweenTest{} err = json.Unmarshal(r.Test.Data, &test) - migratedMin, err := legacy.MigrateTemplate(test.Min, legacy.ExtraAsFunction) + migratedMin, err := MigrateTemplate(test.Min, ExtraAsFunction) if err != nil { return routers.Case{}, err } - migratedMax, err := legacy.MigrateTemplate(test.Max, legacy.ExtraAsFunction) + migratedMax, err := MigrateTemplate(test.Max, ExtraAsFunction) if err != nil { return routers.Case{}, err } @@ -658,7 +635,7 @@ func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r l err = json.Unmarshal(r.Test.Data, &test) arguments = []string{test.Test[baseLanguage]} - addTranslationMap(baseLanguage, translations, test.Test, caseUUID, "arguments") + addTranslationMap(baseLanguage, localization, test.Test, caseUUID, "arguments") // tests against a single date value case "date_equal", "date_after", "date_before": @@ -727,7 +704,7 @@ type categoryName struct { order int } -func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *localization) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { +func parseRules(baseLanguage utils.Language, r legacyRuleSet, localization flows.Localization) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { // find our discrete categories categoryMap := make(map[string]categoryName) @@ -750,9 +727,9 @@ func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *loca exits := make([]flows.Exit, len(categoryMap)) exitMap := make(map[string]flows.Exit) for k, category := range categoryMap { - addTranslationMap(baseLanguage, translations, category.translations, utils.UUID(category.uuid), "name") + addTranslationMap(baseLanguage, localization, category.translations, utils.UUID(category.uuid), "name") - exits[category.order] = NewExit(category.uuid, category.destination, k) + exits[category.order] = definition.NewExit(category.uuid, category.destination, k) exitMap[k] = exits[category.order] } @@ -769,7 +746,7 @@ func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *loca continue } - c, err := migrateRule(baseLanguage, exitMap, r.Rules[i], translations) + c, err := migrateRule(baseLanguage, exitMap, r.Rules[i], localization) if err != nil { return nil, nil, "", err } @@ -786,7 +763,7 @@ func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *loca if r.Type == "webhook" { connectionErrorCategory := "Connection Error" connectionErrorExitUUID := flows.ExitUUID(utils.NewUUID()) - connectionErrorExit := NewExit(connectionErrorExitUUID, exits[1].(*exit).destination, connectionErrorCategory) + connectionErrorExit := definition.NewExit(connectionErrorExitUUID, exits[1].DestinationNodeUUID(), connectionErrorCategory) exits = append(exits, connectionErrorExit) cases = append(cases, routers.Case{ @@ -807,11 +784,12 @@ type fieldConfig struct { } // migrates the given legacy rulset to a node with a router -func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localization) (*node, error) { - node := &node{} - node.uuid = r.UUID +func migrateRuleSet(lang utils.Language, r legacyRuleSet, localization flows.Localization) (flows.Node, error) { + var newActions []flows.Action + var router flows.Router + var wait flows.Wait - exits, cases, defaultExit, err := parseRules(lang, r, translations) + exits, cases, defaultExit, err := parseRules(lang, r, localization) if err != nil { return nil, err } @@ -829,7 +807,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza flowUUID := flows.FlowUUID(config["flow"]["uuid"]) flowName := config["flow"]["name"] - node.actions = []flows.Action{ + newActions = []flows.Action{ &actions.StartFlowAction{ BaseAction: actions.NewBaseAction(flows.ActionUUID(utils.NewUUID())), Flow: flows.NewFlowReference(flowUUID, flowName), @@ -837,7 +815,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza } // subflow rulesets operate on the child flow status - node.router = routers.NewSwitchRouter(defaultExit, "@child.status", cases, resultName) + router = routers.NewSwitchRouter(defaultExit, "@child.status", cases, resultName) case "webhook": var config legacyWebhookConfig @@ -846,13 +824,13 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza return nil, err } - migratedURL, _ := legacy.MigrateTemplate(config.Webhook, legacy.ExtraAsFunction) + migratedURL, _ := MigrateTemplate(config.Webhook, ExtraAsFunction) migratedHeaders := make(map[string]string, len(config.Headers)) for _, header := range config.Headers { migratedHeaders[header.Name] = header.Value } - node.actions = []flows.Action{ + newActions = []flows.Action{ &actions.CallWebhookAction{ BaseAction: actions.NewBaseAction(flows.ActionUUID(utils.NewUUID())), URL: migratedURL, @@ -862,19 +840,19 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza } // subflow rulesets operate on the child flow status - node.router = routers.NewSwitchRouter(defaultExit, "@run.webhook.status", cases, resultName) + router = routers.NewSwitchRouter(defaultExit, "@run.webhook.status", cases, resultName) case "form_field": var config fieldConfig json.Unmarshal(r.Config, &config) - operand, _ := legacy.MigrateTemplate(r.Operand, legacy.ExtraAsFunction) + operand, _ := MigrateTemplate(r.Operand, ExtraAsFunction) operand = fmt.Sprintf("@(field(%s, %d, \"%s\"))", operand[1:], config.FieldIndex, config.FieldDelimiter) - node.router = routers.NewSwitchRouter(defaultExit, operand, cases, resultName) + router = routers.NewSwitchRouter(defaultExit, operand, cases, resultName) case "group": // in legacy flows these rulesets have their operand as @step.value but it's not used - node.router = routers.NewSwitchRouter(defaultExit, "@contact", cases, resultName) + router = routers.NewSwitchRouter(defaultExit, "@contact", cases, resultName) case "wait_message": // look for timeout test on the legacy ruleset @@ -891,7 +869,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza } } - node.wait = waits.NewMsgWait(timeout) + wait = waits.NewMsgWait(timeout) fallthrough case "flow_field": @@ -899,43 +877,35 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, translations *localiza case "contact_field": fallthrough case "expression": - operand, _ := legacy.MigrateTemplate(r.Operand, legacy.ExtraAsFunction) + operand, _ := MigrateTemplate(r.Operand, ExtraAsFunction) if operand == "" { operand = "@run.input" } - node.router = routers.NewSwitchRouter(defaultExit, operand, cases, resultName) + router = routers.NewSwitchRouter(defaultExit, operand, cases, resultName) case "random": - node.router = routers.NewRandomRouter(resultName) + router = routers.NewRandomRouter(resultName) default: return nil, fmt.Errorf("unrecognized ruleset type: %s", r.Type) } - node.exits = exits - - return node, nil + return definition.NewNode(r.UUID, newActions, router, exits, wait), nil } // migrates the given legacy actionset to a node with a set of migrated actions and a single exit -func migateActionSet(lang utils.Language, a legacyActionSet, translations *localization) (*node, error) { - node := &node{ - uuid: a.UUID, - actions: make([]flows.Action, len(a.Actions)), - exits: []flows.Exit{ - NewExit(a.ExitUUID, a.Destination, ""), - }, - } +func migateActionSet(lang utils.Language, a legacyActionSet, localization flows.Localization) (flows.Node, error) { + actions := make([]flows.Action, len(a.Actions)) // migrate each action for i := range a.Actions { - action, err := migrateAction(lang, a.Actions[i], translations) + action, err := migrateAction(lang, a.Actions[i], localization) if err != nil { - return nil, err + return nil, fmt.Errorf("error migrating action[type=%s]: %s", a.Actions[i].Type, err) } - node.actions[i] = action + actions[i] = action } - return node, nil + return definition.NewNode(a.UUID, actions, nil, []flows.Exit{definition.NewExit(a.ExitUUID, a.Destination, "")}, nil), nil } // ReadLegacyFlows reads in legacy formatted flows @@ -952,96 +922,75 @@ func ReadLegacyFlows(data []json.RawMessage) ([]*LegacyFlow, error) { return flows, nil } +// ReadLegacyFlow reads a single legacy formatted flow func ReadLegacyFlow(data json.RawMessage) (*LegacyFlow, error) { - var envelope legacyFlowEnvelope - var err error - - if err := utils.UnmarshalAndValidate(data, &envelope, ""); err != nil { + flow := &LegacyFlow{} + if err := utils.UnmarshalAndValidate(data, flow, ""); err != nil { return nil, err } + return flow, nil +} - f := &LegacyFlow{} - f.uuid = envelope.Metadata.UUID - f.name = envelope.Metadata.Name - f.language = envelope.BaseLanguage - f.expireAfterMinutes = envelope.Metadata.Expires - - translations := &localization{} +// Migrate migrates this legacy flow to the new format +func (f *LegacyFlow) Migrate() (flows.Flow, error) { + localization := definition.NewLocalization() + nodes := make([]flows.Node, len(f.ActionSets)+len(f.RuleSets)) - f.nodes = make([]flows.Node, len(envelope.ActionSets)+len(envelope.RuleSets)) - for i := range envelope.ActionSets { - node, err := migateActionSet(f.language, envelope.ActionSets[i], translations) + for i := range f.ActionSets { + node, err := migateActionSet(f.BaseLanguage, f.ActionSets[i], localization) if err != nil { - return nil, err + return nil, fmt.Errorf("error migrating action_set[uuid=%s]: %s", f.ActionSets[i].UUID, err) } - f.nodes[i] = node + nodes[i] = node } - for i := range envelope.RuleSets { - node, err := migrateRuleSet(f.language, envelope.RuleSets[i], translations) + for i := range f.RuleSets { + node, err := migrateRuleSet(f.BaseLanguage, f.RuleSets[i], localization) if err != nil { - return nil, err + return nil, fmt.Errorf("error migrating rule_set[uuid=%s]: %s", f.RuleSets[i].UUID, err) } - f.nodes[len(envelope.ActionSets)+i] = node + nodes[len(f.ActionSets)+i] = node } // make sure our entry node is first - for i := range f.nodes { - if f.nodes[i].UUID() == envelope.Entry { - firstNode := f.nodes[0] - f.nodes[0] = f.nodes[i] - f.nodes[i] = firstNode + for i := range nodes { + if nodes[i].UUID() == f.Entry { + firstNode := nodes[0] + nodes[0] = nodes[i] + nodes[i] = firstNode } } - f.localization = translations - f.envelope = envelope - - return f, err -} - -// MarshalJSON marshals this legacy flow into JSON -func (f *LegacyFlow) MarshalJSON() ([]byte, error) { - - var fe = flowEnvelope{} - fe.UUID = f.uuid - fe.Name = f.name - fe.Language = f.language - fe.ExpireAfterMinutes = f.expireAfterMinutes - - if f.localization != nil { - fe.Localization = *f.localization.(*localization) - } - - fe.Nodes = make([]*node, len(f.nodes)) - for i := range f.nodes { - fe.Nodes[i] = f.nodes[i].(*node) - } - - // add in our ui metadata - fe.Metadata = make(map[string]interface{}) - fe.Metadata["nodes"] = make(map[flows.NodeUUID]interface{}) - nodes := fe.Metadata["nodes"].(map[flows.NodeUUID]interface{}) + // convert our UI metadata + nodesUI := make(map[flows.NodeUUID]interface{}) - for i := range f.envelope.ActionSets { - actionset := f.envelope.ActionSets[i] + for i := range f.ActionSets { + actionset := f.ActionSets[i] nmd := make(map[string]interface{}) nmd["position"] = map[string]int{ "x": actionset.X, "y": actionset.Y, } - nodes[actionset.UUID] = nmd + nodesUI[actionset.UUID] = nmd } - for i := range f.envelope.RuleSets { - ruleset := f.envelope.RuleSets[i] + for i := range f.RuleSets { + ruleset := f.RuleSets[i] nmd := make(map[string]interface{}) nmd["position"] = map[string]int{ "x": ruleset.X, "y": ruleset.Y, } - nodes[ruleset.UUID] = nmd + nodesUI[ruleset.UUID] = nmd } - return json.Marshal(&fe) + return definition.NewFlow( + f.Metadata.UUID, + f.Metadata.Name, + f.BaseLanguage, + f.Metadata.Expires, + localization, + nodes, + map[string]interface{}{"nodes": nodesUI}, + ) } diff --git a/flows/definition/legacy_test.go b/legacy/definition_test.go similarity index 90% rename from flows/definition/legacy_test.go rename to legacy/definition_test.go index 2352059bf..b29fbd438 100644 --- a/flows/definition/legacy_test.go +++ b/legacy/definition_test.go @@ -1,21 +1,21 @@ -package definition_test +package legacy_test import ( "encoding/json" "fmt" "io/ioutil" - "testing" - "regexp" - "strings" + "testing" "github.com/buger/jsonparser" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/definition" "github.com/nyaruka/goflow/flows/routers" + "github.com/nyaruka/goflow/legacy" "github.com/nyaruka/goflow/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var legacyActionHolderDef = ` @@ -127,24 +127,20 @@ type RuleSetMigrationTest struct { func TestActionMigration(t *testing.T) { data, err := ioutil.ReadFile("testdata/migrations/actions.json") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var tests []ActionMigrationTest err = json.Unmarshal(data, &tests) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, test := range tests { legacyFlowsJSON := fmt.Sprintf(legacyActionHolderDef, string(test.LegacyAction)) legacyFlows, err := readLegacyTestFlows(legacyFlowsJSON) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + migratedFlow, err := legacyFlows[0].Migrate() + require.NoError(t, err) - migratedFlow := legacyFlows[0] migratedAction := migratedFlow.Nodes()[0].Actions()[0] migratedActionEnvelope, _ := utils.EnvelopeFromTyped(migratedAction) migratedActionRaw, _ := json.Marshal(migratedActionEnvelope) @@ -161,24 +157,20 @@ func TestActionMigration(t *testing.T) { func TestTestMigration(t *testing.T) { data, err := ioutil.ReadFile("testdata/migrations/tests.json") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var tests []TestMigrationTest err = json.Unmarshal(data, &tests) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, test := range tests { legacyFlowsJSON := fmt.Sprintf(legacyTestHolderDef, string(test.LegacyTest)) legacyFlows, err := readLegacyTestFlows(legacyFlowsJSON) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + migratedFlow, err := legacyFlows[0].Migrate() + require.NoError(t, err) - migratedFlow := legacyFlows[0] migratedRouter := migratedFlow.Nodes()[0].Router().(*routers.SwitchRouter) if len(migratedRouter.Cases) == 0 { @@ -200,24 +192,19 @@ func TestTestMigration(t *testing.T) { func TestRuleSetMigration(t *testing.T) { data, err := ioutil.ReadFile("testdata/migrations/rulesets.json") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var tests []RuleSetMigrationTest err = json.Unmarshal(data, &tests) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, test := range tests { legacyFlowsJSON := fmt.Sprintf(legacyRuleSetHolderDef, string(test.LegacyRuleSet)) legacyFlows, err := readLegacyTestFlows(legacyFlowsJSON) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - migratedFlow := legacyFlows[0] + migratedFlow, err := legacyFlows[0].Migrate() + require.NoError(t, err) // check we now have a new node in addition to the 3 actionsets used as destinations if len(migratedFlow.Nodes()) <= 3 { @@ -244,13 +231,13 @@ func TestRuleSetMigration(t *testing.T) { } } -func readLegacyTestFlows(flowsJSON string) ([]*definition.LegacyFlow, error) { +func readLegacyTestFlows(flowsJSON string) ([]*legacy.LegacyFlow, error) { var legacyFlows []json.RawMessage json.Unmarshal(json.RawMessage(flowsJSON), &legacyFlows) - return definition.ReadLegacyFlows(legacyFlows) + return legacy.ReadLegacyFlows(legacyFlows) } -func checkFlowLocalization(t *testing.T, flow *definition.LegacyFlow, expectedLocalizationRaw json.RawMessage, substitutionSource json.RawMessage) { +func checkFlowLocalization(t *testing.T, flow flows.Flow, expectedLocalizationRaw json.RawMessage, substitutionSource json.RawMessage) { actualLocalizationRaw, _ := json.Marshal(flow.Localization()) actualLocalizationJSON := formatJSON(actualLocalizationRaw) @@ -309,5 +296,5 @@ func TestTranslations(t *testing.T) { assert.Equal(t, map[utils.Language][]string{ "eng": {"Yes", "No", "Maybe", "Never"}, "fra": {"Oui", "Non", "", "Jamas"}, - }, definition.TransformTranslations(translations)) + }, legacy.TransformTranslations(translations)) } diff --git a/flows/definition/testdata/migrations/actions.json b/legacy/testdata/migrations/actions.json similarity index 100% rename from flows/definition/testdata/migrations/actions.json rename to legacy/testdata/migrations/actions.json diff --git a/flows/definition/testdata/migrations/rulesets.json b/legacy/testdata/migrations/rulesets.json similarity index 98% rename from flows/definition/testdata/migrations/rulesets.json rename to legacy/testdata/migrations/rulesets.json index 4a81a9263..df8ead100 100644 --- a/flows/definition/testdata/migrations/rulesets.json +++ b/legacy/testdata/migrations/rulesets.json @@ -218,7 +218,7 @@ { "category": {"eng": "< 10"}, "uuid": "1c75fd71-027b-40e8-a819-151a0f8140e6", - "destination": "7d40faea-723b-473d-8999-59fb7d3c3ca2", + "destination": "5b977652-91e3-48be-8e86-7c8094b4aa8f", "label": null, "destination_type": "A", "test": { @@ -229,7 +229,7 @@ { "category": {"eng": "> 10"}, "uuid": "40cc7c36-b7c8-4f05-ae82-25275607e5aa", - "destination": "c12f37e2-8e6c-4c81-ba6d-941bb3caf93f", + "destination": "833fc698-d590-42dc-93e1-39e701b7e8e4", "label": null, "destination_type": "A", "test": { @@ -276,12 +276,12 @@ "exits": [ { "uuid": "1c75fd71-027b-40e8-a819-151a0f8140e6", - "destination_node_uuid": "7d40faea-723b-473d-8999-59fb7d3c3ca2", + "destination_node_uuid": "5b977652-91e3-48be-8e86-7c8094b4aa8f", "name": "\u003c 10" }, { "uuid": "40cc7c36-b7c8-4f05-ae82-25275607e5aa", - "destination_node_uuid": "c12f37e2-8e6c-4c81-ba6d-941bb3caf93f", + "destination_node_uuid": "833fc698-d590-42dc-93e1-39e701b7e8e4", "name": "\u003e 10" } ], diff --git a/flows/definition/testdata/migrations/tests.json b/legacy/testdata/migrations/tests.json similarity index 100% rename from flows/definition/testdata/migrations/tests.json rename to legacy/testdata/migrations/tests.json From a1010ff6cb416d5b39dbb0eb567040716de9df36 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 15 Mar 2018 15:35:30 -0500 Subject: [PATCH 3/5] Simplify names in new legacy package --- legacy/definition.go | 103 +++++++++++++++++++------------------- legacy/definition_test.go | 2 +- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/legacy/definition.go b/legacy/definition.go index 411394e7e..d938ec461 100644 --- a/legacy/definition.go +++ b/legacy/definition.go @@ -41,22 +41,23 @@ var legacyWebhookBody = `{ "channel": {} }` -// LegacyFlow is a legacy style flow -type LegacyFlow struct { - BaseLanguage utils.Language `json:"base_language"` - Metadata legacyMetadata `json:"metadata"` - RuleSets []legacyRuleSet `json:"rule_sets" validate:"dive"` - ActionSets []legacyActionSet `json:"action_sets" validate:"dive"` - Entry flows.NodeUUID `json:"entry" validate:"required,uuid4"` +// Flow is a flow in the legacy format +type Flow struct { + BaseLanguage utils.Language `json:"base_language"` + Metadata Metadata `json:"metadata"` + RuleSets []RuleSet `json:"rule_sets" validate:"dive"` + ActionSets []ActionSet `json:"action_sets" validate:"dive"` + Entry flows.NodeUUID `json:"entry" validate:"required,uuid4"` } -type legacyMetadata struct { +// Metadata is the metadata section of a legacy flow +type Metadata struct { UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` Name string `json:"name"` Expires int `json:"expires"` } -type legacyRule struct { +type Rule struct { UUID flows.ExitUUID `json:"uuid" validate:"required,uuid4"` Destination flows.NodeUUID `json:"destination" validate:"omitempty,uuid4"` DestinationType string `json:"destination_type" validate:"eq=A|eq=R"` @@ -64,32 +65,32 @@ type legacyRule struct { Category map[utils.Language]string `json:"category"` } -type legacyRuleSet struct { +type RuleSet struct { Y int `json:"y"` X int `json:"x"` UUID flows.NodeUUID `json:"uuid" validate:"required,uuid4"` Type string `json:"ruleset_type"` Label string `json:"label"` Operand string `json:"operand"` - Rules []legacyRule `json:"rules"` + Rules []Rule `json:"rules"` Config json.RawMessage `json:"config"` } -type legacyActionSet struct { +type ActionSet struct { Y int `json:"y"` X int `json:"x"` Destination flows.NodeUUID `json:"destination" validate:"omitempty,uuid4"` ExitUUID flows.ExitUUID `json:"exit_uuid" validate:"required,uuid4"` UUID flows.NodeUUID `json:"uuid" validate:"required,uuid4"` - Actions []legacyAction `json:"actions"` + Actions []Action `json:"actions"` } -type legacyLabelReference struct { +type LabelReference struct { UUID flows.LabelUUID Name string } -func (l *legacyLabelReference) Migrate() *flows.LabelReference { +func (l *LabelReference) Migrate() *flows.LabelReference { if len(l.UUID) > 0 { return flows.NewLabelReference(l.UUID, l.Name) } @@ -97,7 +98,7 @@ func (l *legacyLabelReference) Migrate() *flows.LabelReference { } // UnmarshalJSON unmarshals a legacy label reference from the given JSON -func (l *legacyLabelReference) UnmarshalJSON(data []byte) error { +func (l *LabelReference) UnmarshalJSON(data []byte) error { // label reference may be a string if data[0] == '"' { var nameExpression string @@ -125,20 +126,20 @@ func (l *legacyLabelReference) UnmarshalJSON(data []byte) error { return nil } -type legacyContactReference struct { +type ContactReference struct { UUID flows.ContactUUID `json:"uuid"` } -func (c *legacyContactReference) Migrate() *flows.ContactReference { +func (c *ContactReference) Migrate() *flows.ContactReference { return flows.NewContactReference(c.UUID, "") } -type legacyGroupReference struct { +type GroupReference struct { UUID flows.GroupUUID Name string } -func (g *legacyGroupReference) Migrate() *flows.GroupReference { +func (g *GroupReference) Migrate() *flows.GroupReference { if len(g.UUID) > 0 { return flows.NewGroupReference(g.UUID, g.Name) } @@ -146,7 +147,7 @@ func (g *legacyGroupReference) Migrate() *flows.GroupReference { } // UnmarshalJSON unmarshals a legacy group reference from the given JSON -func (g *legacyGroupReference) UnmarshalJSON(data []byte) error { +func (g *GroupReference) UnmarshalJSON(data []byte) error { // group reference may be a string if data[0] == '"' { var nameExpression string @@ -174,31 +175,31 @@ func (g *legacyGroupReference) UnmarshalJSON(data []byte) error { return nil } -type legacyVariable struct { +type VariableReference struct { ID string `json:"id"` } -type legacyFlowReference struct { +type FlowReference struct { UUID flows.FlowUUID `json:"uuid"` Name string `json:"name"` } -func (f *legacyFlowReference) Migrate() *flows.FlowReference { +func (f *FlowReference) Migrate() *flows.FlowReference { return flows.NewFlowReference(f.UUID, f.Name) } -type legacyWebhookConfig struct { - Webhook string `json:"webhook"` - Action string `json:"webhook_action"` - Headers []legacyWebhookHeader `json:"webhook_headers"` +type WebhookConfig struct { + Webhook string `json:"webhook"` + Action string `json:"webhook_action"` + Headers []WebhookHeader `json:"webhook_headers"` } -type legacyWebhookHeader struct { +type WebhookHeader struct { Name string `json:"name"` Value string `json:"value"` } -type legacyAction struct { +type Action struct { Type string `json:"type"` UUID flows.ActionUUID `json:"uuid"` Name string `json:"name"` @@ -210,9 +211,9 @@ type legacyAction struct { SendAll bool `json:"send_all"` // variable contact actions - Contacts []legacyContactReference `json:"contacts"` - Groups []legacyGroupReference `json:"groups"` - Variables []legacyVariable `json:"variables"` + Contacts []ContactReference `json:"contacts"` + Groups []GroupReference `json:"groups"` + Variables []VariableReference `json:"variables"` // save actions Field string `json:"field"` @@ -223,15 +224,15 @@ type legacyAction struct { Language utils.Language `json:"lang"` // webhook - Action string `json:"action"` - Webhook string `json:"webhook"` - WebhookHeaders []legacyWebhookHeader `json:"webhook_headers"` + Action string `json:"action"` + Webhook string `json:"webhook"` + WebhookHeaders []WebhookHeader `json:"webhook_headers"` // add lable action - Labels []legacyLabelReference `json:"labels"` + Labels []LabelReference `json:"labels"` // Start/Trigger flow - Flow legacyFlowReference `json:"flow"` + Flow FlowReference `json:"flow"` // channel Channel flows.ChannelUUID `json:"channel"` @@ -271,7 +272,7 @@ type timeoutTest struct { } type groupTest struct { - Test legacyGroupReference `json:"test"` + Test GroupReference `json:"test"` } type wardTest struct { @@ -279,8 +280,6 @@ type wardTest struct { District string `json:"district"` } -type localizations map[utils.Language]flows.Action - func addTranslationMap(baseLanguage utils.Language, localization flows.Localization, mapped map[utils.Language]string, uuid utils.UUID, property string) string { var inBaseLanguage string for language, item := range mapped { @@ -362,7 +361,7 @@ var testTypeMappings = map[string]string{ } // migrates the given legacy action to a new action -func migrateAction(baseLanguage utils.Language, a legacyAction, localization flows.Localization) (flows.Action, error) { +func migrateAction(baseLanguage utils.Language, a Action, localization flows.Localization) (flows.Action, error) { switch a.Type { case "add_label": labels := make([]*flows.LabelReference, len(a.Labels)) @@ -590,7 +589,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, localization flo } // migrates the given legacy rule to a router case -func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r legacyRule, localization flows.Localization) (routers.Case, error) { +func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r Rule, localization flows.Localization) (routers.Case, error) { category := r.Category[baseLanguage] newType, _ := testTypeMappings[r.Test.Type] @@ -704,7 +703,7 @@ type categoryName struct { order int } -func parseRules(baseLanguage utils.Language, r legacyRuleSet, localization flows.Localization) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { +func parseRules(baseLanguage utils.Language, r RuleSet, localization flows.Localization) ([]flows.Exit, []routers.Case, flows.ExitUUID, error) { // find our discrete categories categoryMap := make(map[string]categoryName) @@ -784,7 +783,7 @@ type fieldConfig struct { } // migrates the given legacy rulset to a node with a router -func migrateRuleSet(lang utils.Language, r legacyRuleSet, localization flows.Localization) (flows.Node, error) { +func migrateRuleSet(lang utils.Language, r RuleSet, localization flows.Localization) (flows.Node, error) { var newActions []flows.Action var router flows.Router var wait flows.Wait @@ -818,7 +817,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, localization flows.Loc router = routers.NewSwitchRouter(defaultExit, "@child.status", cases, resultName) case "webhook": - var config legacyWebhookConfig + var config WebhookConfig err := json.Unmarshal(r.Config, &config) if err != nil { return nil, err @@ -893,7 +892,7 @@ func migrateRuleSet(lang utils.Language, r legacyRuleSet, localization flows.Loc } // migrates the given legacy actionset to a node with a set of migrated actions and a single exit -func migateActionSet(lang utils.Language, a legacyActionSet, localization flows.Localization) (flows.Node, error) { +func migateActionSet(lang utils.Language, a ActionSet, localization flows.Localization) (flows.Node, error) { actions := make([]flows.Action, len(a.Actions)) // migrate each action @@ -909,9 +908,9 @@ func migateActionSet(lang utils.Language, a legacyActionSet, localization flows. } // ReadLegacyFlows reads in legacy formatted flows -func ReadLegacyFlows(data []json.RawMessage) ([]*LegacyFlow, error) { +func ReadLegacyFlows(data []json.RawMessage) ([]*Flow, error) { var err error - flows := make([]*LegacyFlow, len(data)) + flows := make([]*Flow, len(data)) for f := range data { flows[f], err = ReadLegacyFlow(data[f]) if err != nil { @@ -923,8 +922,8 @@ func ReadLegacyFlows(data []json.RawMessage) ([]*LegacyFlow, error) { } // ReadLegacyFlow reads a single legacy formatted flow -func ReadLegacyFlow(data json.RawMessage) (*LegacyFlow, error) { - flow := &LegacyFlow{} +func ReadLegacyFlow(data json.RawMessage) (*Flow, error) { + flow := &Flow{} if err := utils.UnmarshalAndValidate(data, flow, ""); err != nil { return nil, err } @@ -932,7 +931,7 @@ func ReadLegacyFlow(data json.RawMessage) (*LegacyFlow, error) { } // Migrate migrates this legacy flow to the new format -func (f *LegacyFlow) Migrate() (flows.Flow, error) { +func (f *Flow) Migrate() (flows.Flow, error) { localization := definition.NewLocalization() nodes := make([]flows.Node, len(f.ActionSets)+len(f.RuleSets)) diff --git a/legacy/definition_test.go b/legacy/definition_test.go index b29fbd438..a2764b114 100644 --- a/legacy/definition_test.go +++ b/legacy/definition_test.go @@ -231,7 +231,7 @@ func TestRuleSetMigration(t *testing.T) { } } -func readLegacyTestFlows(flowsJSON string) ([]*legacy.LegacyFlow, error) { +func readLegacyTestFlows(flowsJSON string) ([]*legacy.Flow, error) { var legacyFlows []json.RawMessage json.Unmarshal(json.RawMessage(flowsJSON), &legacyFlows) return legacy.ReadLegacyFlows(legacyFlows) From a51d25dfb17290e0b32683bc835721d1ee442495 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 15 Mar 2018 15:45:15 -0500 Subject: [PATCH 4/5] Fix migrating flows with localization --- flows/definition/flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flows/definition/flow.go b/flows/definition/flow.go index 0ce69359a..6a74b5e59 100644 --- a/flows/definition/flow.go +++ b/flows/definition/flow.go @@ -164,7 +164,7 @@ func (f *flow) MarshalJSON() ([]byte, error) { } if f.localization != nil { - fe.Localization = *f.localization.(*localization) + fe.Localization = f.localization.(localization) } fe.Nodes = make([]*node, len(f.nodes)) From c416b42d836ef2d0c9158f6b87631cee90651b0b Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 15 Mar 2018 15:58:15 -0500 Subject: [PATCH 5/5] Don't even unmarshall _ui section in flows --- flows/definition/flow.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/flows/definition/flow.go b/flows/definition/flow.go index 6a74b5e59..4b5d2f72a 100644 --- a/flows/definition/flow.go +++ b/flows/definition/flow.go @@ -130,13 +130,17 @@ var _ utils.VariableResolver = (*flow)(nil) //------------------------------------------------------------------------------------------ type flowEnvelope struct { - UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` - Name string `json:"name" validate:"required"` - Language utils.Language `json:"language"` - ExpireAfterMinutes int `json:"expire_after_minutes"` - Localization localization `json:"localization"` - Nodes []*node `json:"nodes"` - UI map[string]interface{} `json:"_ui,omitempty"` + UUID flows.FlowUUID `json:"uuid" validate:"required,uuid4"` + Name string `json:"name" validate:"required"` + Language utils.Language `json:"language"` + ExpireAfterMinutes int `json:"expire_after_minutes"` + Localization localization `json:"localization"` + Nodes []*node `json:"nodes"` +} + +type flowEnvelopeWithUI struct { + flowEnvelope + UI map[string]interface{} `json:"_ui,omitempty"` } // ReadFlow reads a single flow definition from the passed in byte array @@ -155,12 +159,14 @@ func ReadFlow(data json.RawMessage) (flows.Flow, error) { // MarshalJSON marshals this flow into JSON func (f *flow) MarshalJSON() ([]byte, error) { - var fe = &flowEnvelope{ - UUID: f.uuid, - Name: f.name, - Language: f.language, - ExpireAfterMinutes: f.expireAfterMinutes, - UI: f.ui, + var fe = &flowEnvelopeWithUI{ + flowEnvelope: flowEnvelope{ + UUID: f.uuid, + Name: f.name, + Language: f.language, + ExpireAfterMinutes: f.expireAfterMinutes, + }, + UI: f.ui, } if f.localization != nil {