Skip to content

Commit

Permalink
Merge pull request #187 from nyaruka/legacy_move
Browse files Browse the repository at this point in the history
Move legacy stuff into its own package part 2/2
  • Loading branch information
rowanseymour authored Mar 16, 2018
2 parents 5246eda + c416b42 commit 96c3ccc
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 322 deletions.
12 changes: 10 additions & 2 deletions cmd/flowserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -269,11 +269,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
}

Expand Down
2 changes: 1 addition & 1 deletion flows/actions/send_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
141 changes: 82 additions & 59 deletions flows/definition/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,58 @@ type flow struct {
language utils.Language
expireAfterMinutes int

translations flows.FlowTranslations
localization flows.Localization

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 }
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
Expand Down Expand Up @@ -70,22 +110,37 @@ 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)

//------------------------------------------------------------------------------------------
// JSON Encoding / Decoding
//------------------------------------------------------------------------------------------

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"`

// 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"`
}

type flowEnvelopeWithUI struct {
flowEnvelope
UI map[string]interface{} `json:"_ui,omitempty"`
}

// ReadFlow reads a single flow definition from the passed in byte array
Expand All @@ -94,66 +149,34 @@ 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.translations = 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
}

// 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)
}
}
nodes := make([]flows.Node, len(envelope.Nodes))
for n := range envelope.Nodes {
nodes[n] = envelope.Nodes[n]
}

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 = &flowEnvelopeWithUI{
flowEnvelope: flowEnvelope{
UUID: f.uuid,
Name: f.name,
Language: f.language,
ExpireAfterMinutes: f.expireAfterMinutes,
},
UI: f.ui,
}

var fe = flowEnvelope{}
fe.UUID = f.uuid
fe.Name = f.name
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))
for i := range f.nodes {
fe.Nodes[i] = f.nodes[i].(*node)
}

return json.Marshal(&fe)
return json.Marshal(fe)
}
68 changes: 53 additions & 15 deletions flows/definition/localization.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,80 @@
package definition

import (
"encoding/json"

"github.com/nyaruka/goflow/flows"
"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
}
}
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 NewLocalization() flows.Localization {
return make(localization)
}

func (t flowTranslations) Languages() utils.LanguageList {
languages := make(utils.LanguageList, 0, len(t))
for lang := range t {
// 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 flowTranslations) GetLanguageTranslations(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
func ReadLocalization(data json.RawMessage) (flows.Localization, error) {
translations := &localization{}
if err := json.Unmarshal(data, translations); err != nil {
return nil, err
}
return translations, nil
}
11 changes: 11 additions & 0 deletions flows/definition/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
9 changes: 5 additions & 4 deletions flows/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ type Flow interface {
Name() string
Language() utils.Language
ExpireAfterMinutes() int
Translations() FlowTranslations
Localization() Localization

Validate(SessionAssets) error
Nodes() []Node
Expand Down Expand Up @@ -202,9 +202,10 @@ 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 {
AddItemTranslation(utils.Language, utils.UUID, string, []string)
GetTranslations(utils.Language) Translations
Languages() utils.LanguageList
}

Expand Down
2 changes: 1 addition & 1 deletion flows/runs/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 96c3ccc

Please sign in to comment.