From 086fa953f583c501ae6c63ee01bdb7c47b01c64c Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 15 Aug 2023 13:18:45 -0500 Subject: [PATCH 1/3] Simplify how we create a merged environment with properties from the contact --- flows/engine/engine.go | 2 + flows/engine/session.go | 1 + flows/environment.go | 54 +++++++++++++ flows/environment_test.go | 128 +++++++++++++++++++++++++++++- flows/interfaces.go | 1 + flows/runs/environment.go | 61 --------------- flows/runs/environment_test.go | 137 --------------------------------- flows/runs/run.go | 11 +-- flows/triggers/base.go | 4 +- 9 files changed, 189 insertions(+), 210 deletions(-) delete mode 100644 flows/runs/environment.go delete mode 100644 flows/runs/environment_test.go diff --git a/flows/engine/engine.go b/flows/engine/engine.go index 08192a459..c92035f7a 100644 --- a/flows/engine/engine.go +++ b/flows/engine/engine.go @@ -5,6 +5,7 @@ import ( "github.com/nyaruka/gocommon/uuids" "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" ) @@ -20,6 +21,7 @@ type engine struct { func (e *engine) NewSession(sa flows.SessionAssets, trigger flows.Trigger) (flows.Session, flows.Sprint, error) { s := &session{ uuid: flows.SessionUUID(uuids.New()), + env: envs.NewBuilder().Build(), engine: e, assets: sa, trigger: trigger, diff --git a/flows/engine/session.go b/flows/engine/session.go index 988cb729a..341560bf7 100644 --- a/flows/engine/session.go +++ b/flows/engine/session.go @@ -61,6 +61,7 @@ func (s *session) SetType(type_ flows.FlowType) { s.type_ = type_ } func (s *session) Environment() envs.Environment { return s.env } func (s *session) SetEnvironment(env envs.Environment) { s.env = env } +func (s *session) MergedEnvironment() envs.Environment { return flows.NewMergedEnvironment(s) } func (s *session) Contact() *flows.Contact { return s.contact } func (s *session) SetContact(contact *flows.Contact) { s.contact = contact } diff --git a/flows/environment.go b/flows/environment.go index b431c3931..8b3eedf5e 100644 --- a/flows/environment.go +++ b/flows/environment.go @@ -3,9 +3,11 @@ package flows import ( "regexp" "strings" + "time" "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/envs" + "golang.org/x/exp/slices" ) type environment struct { @@ -80,3 +82,55 @@ func (r *assetLocationResolver) FindLocationsFuzzy(text string, level envs.Locat func (r *assetLocationResolver) LookupLocation(path envs.LocationPath) *envs.Location { return r.locations.FindByPath(path) } + +// an extended environment which takes some values from a contact if there is one and if the have those values. +type mergedEnvironment struct { + envs.Environment + + session Session +} + +// NewMergedEnvironment creates a new merged environment from a session's base environment and its contact +func NewMergedEnvironment(s Session) envs.Environment { + return &mergedEnvironment{ + NewEnvironment(s.Environment(), s.Assets().Locations()), + s, + } +} + +func (e *mergedEnvironment) Timezone() *time.Location { + contact := e.session.Contact() + + // if we have a contact and they have a timezone that overrides the base enviroment's timezone + if contact != nil && contact.Timezone() != nil { + return contact.Timezone() + } + return e.Environment.Timezone() +} + +func (e *mergedEnvironment) DefaultLanguage() envs.Language { + contact := e.session.Contact() + + // if we have a contact and they have a language and it's an allowed language that overrides the base environment's languuage + if contact != nil && contact.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), contact.Language()) { + return contact.Language() + } + return e.Environment.DefaultLanguage() +} + +func (e *mergedEnvironment) DefaultCountry() envs.Country { + contact := e.session.Contact() + + // if we have a contact and they have a preferred channel with a country that overrides the base environment's country + if contact != nil { + cc := contact.Country() + if cc != envs.NilCountry { + return cc + } + } + return e.Environment.DefaultCountry() +} + +func (e *mergedEnvironment) DefaultLocale() envs.Locale { + return envs.NewLocale(e.DefaultLanguage(), e.DefaultCountry()) +} diff --git a/flows/environment_test.go b/flows/environment_test.go index f136d7c1b..97853587c 100644 --- a/flows/environment_test.go +++ b/flows/environment_test.go @@ -2,17 +2,20 @@ package flows_test import ( "testing" + "time" + "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/assets/static" "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/engine" + "github.com/nyaruka/goflow/flows/triggers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var assetsJSON = `{ +var assets1JSON = `{ "flows": [ { "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", @@ -66,7 +69,7 @@ var assetsJSON = `{ func TestEnvironment(t *testing.T) { env := envs.NewBuilder().WithDefaultCountry("RW").Build() - source, err := static.NewSource([]byte(assetsJSON)) + source, err := static.NewSource([]byte(assets1JSON)) require.NoError(t, err) sa, err := engine.NewSessionAssets(env, source, nil) @@ -83,3 +86,124 @@ func TestEnvironment(t *testing.T) { assert.Equal(t, 1, len(matches)) assert.Equal(t, "Gisozi", matches[0].Name()) } + +var assets2JSON = `{ + "flows": [ + { + "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", + "name": "Test", + "spec_version": "13.1.0", + "language": "eng", + "type": "messaging", + "nodes": [] + } + ], + "channels": [ + { + "uuid": "57f1078f-88aa-46f4-a59a-948a5739c03d", + "name": "Android Channel", + "address": "+17036975131", + "schemes": ["tel"], + "roles": ["send", "receive"], + "country": "US" + } + ], + "locations": [ + { + "name": "Rwanda", + "aliases": ["Ruanda"], + "children": [ + { + "name": "Kigali City", + "aliases": ["Kigali", "Kigari"], + "children": [ + { + "name": "Gasabo", + "children": [ + { + "name": "Gisozi" + }, + { + "name": "Ndera" + } + ] + }, + { + "name": "Nyarugenge", + "children": [] + } + ] + } + ] + } + ] +}` + +const contactJSON = `{ + "uuid": "ba96bf7f-bc2a-4873-a7c7-254d1927c4e3", + "id": 1234567, + "name": "Ben Haggerty", + "created_on": "2018-01-01T12:00:00.000000000-00:00", + "fields": {}, + "language": "fra", + "timezone": "America/Guayaquil", + "urns": [ + "tel:+12065551212" + ] +}` + +func TestMergedEnvironment(t *testing.T) { + tzRW, _ := time.LoadLocation("Africa/Kigali") + tzEC, _ := time.LoadLocation("America/Guayaquil") + tzUK, _ := time.LoadLocation("Europe/London") + + env := envs.NewBuilder(). + WithAllowedLanguages([]envs.Language{"eng", "fra", "kin"}). + WithDefaultCountry("RW"). + WithTimezone(tzRW). + Build() + source, err := static.NewSource([]byte(assets2JSON)) + require.NoError(t, err) + + sa, err := engine.NewSessionAssets(env, source, nil) + require.NoError(t, err) + + contact, err := flows.ReadContact(sa, []byte(contactJSON), assets.IgnoreMissing) + require.NoError(t, err) + + trigger := triggers.NewBuilder(env, assets.NewFlowReference("76f0a02f-3b75-4b86-9064-e9195e1b3a02", "Test"), contact).Manual().Build() + eng := engine.NewBuilder().Build() + + session, _, err := eng.NewSession(sa, trigger) + require.NoError(t, err) + + // main environment on the session has the values we started with + serializedEnv := session.Environment() + assert.Equal(t, envs.Language("eng"), serializedEnv.DefaultLanguage()) + assert.Equal(t, []envs.Language{"eng", "fra", "kin"}, serializedEnv.AllowedLanguages()) + assert.Equal(t, envs.Country("RW"), serializedEnv.DefaultCountry()) + assert.Equal(t, "en-RW", serializedEnv.DefaultLocale().ToBCP47()) + assert.Equal(t, tzRW, serializedEnv.Timezone()) + + // merged environment on the session has values from the contact + mergedEnv := session.MergedEnvironment() + assert.Equal(t, envs.Language("fra"), mergedEnv.DefaultLanguage()) + assert.Equal(t, []envs.Language{"eng", "fra", "kin"}, mergedEnv.AllowedLanguages()) + assert.Equal(t, envs.Country("US"), mergedEnv.DefaultCountry()) + assert.Equal(t, "fr-US", mergedEnv.DefaultLocale().ToBCP47()) + assert.Equal(t, tzEC, mergedEnv.Timezone()) + assert.NotNil(t, mergedEnv.LocationResolver()) + + // can make changes to contact + session.Contact().SetLanguage(envs.Language("kin")) + session.Contact().SetTimezone(tzUK) + + // and environment reflects those changes + assert.Equal(t, envs.Language("kin"), mergedEnv.DefaultLanguage()) + assert.Equal(t, tzUK, mergedEnv.Timezone()) + + // if contact language is not an allowed language it won't be used + session.Contact().SetLanguage(envs.Language("spa")) + assert.Equal(t, envs.Language("eng"), mergedEnv.DefaultLanguage()) + assert.Equal(t, "en-US", mergedEnv.DefaultLocale().ToBCP47()) +} diff --git a/flows/interfaces.go b/flows/interfaces.go index 2ab05f52a..342834406 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -354,6 +354,7 @@ type Session interface { Environment() envs.Environment SetEnvironment(envs.Environment) + MergedEnvironment() envs.Environment Contact() *Contact SetContact(*Contact) diff --git a/flows/runs/environment.go b/flows/runs/environment.go deleted file mode 100644 index 53572e2ad..000000000 --- a/flows/runs/environment.go +++ /dev/null @@ -1,61 +0,0 @@ -package runs - -import ( - "time" - - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "golang.org/x/exp/slices" -) - -// an extended environment which takes some values from a contact if there is one and if the have those values. -type runEnvironment struct { - envs.Environment - - run *flowRun -} - -// creates a run environment based on the given run -func newRunEnvironment(base envs.Environment, run *flowRun) envs.Environment { - return &runEnvironment{ - flows.NewEnvironment(base, run.Session().Assets().Locations()), - run, - } -} - -func (e *runEnvironment) Timezone() *time.Location { - contact := e.run.Contact() - - // if we have a contact and they have a timezone that overrides the base enviroment's timezone - if contact != nil && contact.Timezone() != nil { - return contact.Timezone() - } - return e.Environment.Timezone() -} - -func (e *runEnvironment) DefaultLanguage() envs.Language { - contact := e.run.Contact() - - // if we have a contact and they have a language and it's an allowed language that overrides the base environment's languuage - if contact != nil && contact.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), contact.Language()) { - return contact.Language() - } - return e.Environment.DefaultLanguage() -} - -func (e *runEnvironment) DefaultCountry() envs.Country { - contact := e.run.Contact() - - // if we have a contact and they have a preferred channel with a country that overrides the base environment's country - if contact != nil { - cc := contact.Country() - if cc != envs.NilCountry { - return cc - } - } - return e.Environment.DefaultCountry() -} - -func (e *runEnvironment) DefaultLocale() envs.Locale { - return envs.NewLocale(e.DefaultLanguage(), e.DefaultCountry()) -} diff --git a/flows/runs/environment_test.go b/flows/runs/environment_test.go deleted file mode 100644 index fcd77eda0..000000000 --- a/flows/runs/environment_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package runs_test - -import ( - "testing" - "time" - - "github.com/nyaruka/goflow/assets" - "github.com/nyaruka/goflow/assets/static" - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/flows/engine" - "github.com/nyaruka/goflow/flows/triggers" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var assetsJSON = `{ - "flows": [ - { - "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", - "name": "Test", - "spec_version": "13.1.0", - "language": "eng", - "type": "messaging", - "nodes": [] - } - ], - "channels": [ - { - "uuid": "57f1078f-88aa-46f4-a59a-948a5739c03d", - "name": "Android Channel", - "address": "+17036975131", - "schemes": ["tel"], - "roles": ["send", "receive"], - "country": "US" - } - ], - "locations": [ - { - "name": "Rwanda", - "aliases": ["Ruanda"], - "children": [ - { - "name": "Kigali City", - "aliases": ["Kigali", "Kigari"], - "children": [ - { - "name": "Gasabo", - "children": [ - { - "name": "Gisozi" - }, - { - "name": "Ndera" - } - ] - }, - { - "name": "Nyarugenge", - "children": [] - } - ] - } - ] - } - ] -}` - -const contactJSON = `{ - "uuid": "ba96bf7f-bc2a-4873-a7c7-254d1927c4e3", - "id": 1234567, - "name": "Ben Haggerty", - "created_on": "2018-01-01T12:00:00.000000000-00:00", - "fields": {}, - "language": "fra", - "timezone": "America/Guayaquil", - "urns": [ - "tel:+12065551212" - ] -}` - -func TestRunEnvironment(t *testing.T) { - tzRW, _ := time.LoadLocation("Africa/Kigali") - tzEC, _ := time.LoadLocation("America/Guayaquil") - tzUK, _ := time.LoadLocation("Europe/London") - - env := envs.NewBuilder(). - WithAllowedLanguages([]envs.Language{"eng", "fra", "kin"}). - WithDefaultCountry("RW"). - WithTimezone(tzRW). - Build() - source, err := static.NewSource([]byte(assetsJSON)) - require.NoError(t, err) - - sa, err := engine.NewSessionAssets(env, source, nil) - require.NoError(t, err) - - contact, err := flows.ReadContact(sa, []byte(contactJSON), assets.IgnoreMissing) - require.NoError(t, err) - - trigger := triggers.NewBuilder(env, assets.NewFlowReference("76f0a02f-3b75-4b86-9064-e9195e1b3a02", "Test"), contact).Manual().Build() - eng := engine.NewBuilder().Build() - - session, _, err := eng.NewSession(sa, trigger) - require.NoError(t, err) - - // environment on the session has the values we started with - sessionEnv := session.Environment() - assert.Equal(t, envs.Language("eng"), sessionEnv.DefaultLanguage()) - assert.Equal(t, []envs.Language{"eng", "fra", "kin"}, sessionEnv.AllowedLanguages()) - assert.Equal(t, envs.Country("RW"), sessionEnv.DefaultCountry()) - assert.Equal(t, "en-RW", sessionEnv.DefaultLocale().ToBCP47()) - assert.Equal(t, tzRW, sessionEnv.Timezone()) - - // environment on the run has values from the contact - run := session.Runs()[0] - runEnv := run.Environment() - assert.Equal(t, envs.Language("fra"), runEnv.DefaultLanguage()) - assert.Equal(t, []envs.Language{"eng", "fra", "kin"}, runEnv.AllowedLanguages()) - assert.Equal(t, envs.Country("US"), runEnv.DefaultCountry()) - assert.Equal(t, "fr-US", runEnv.DefaultLocale().ToBCP47()) - assert.Equal(t, tzEC, runEnv.Timezone()) - assert.NotNil(t, runEnv.LocationResolver()) - - // can make changes to contact - run.Contact().SetLanguage(envs.Language("kin")) - run.Contact().SetTimezone(tzUK) - - // and environment reflects those changes - assert.Equal(t, envs.Language("kin"), runEnv.DefaultLanguage()) - assert.Equal(t, tzUK, runEnv.Timezone()) - - // if contact language is not an allowed language it won't be used - run.Contact().SetLanguage(envs.Language("spa")) - assert.Equal(t, envs.Language("eng"), runEnv.DefaultLanguage()) - assert.Equal(t, "en-US", runEnv.DefaultLocale().ToBCP47()) -} diff --git a/flows/runs/run.go b/flows/runs/run.go index 15a27be8a..d46723496 100644 --- a/flows/runs/run.go +++ b/flows/runs/run.go @@ -20,9 +20,8 @@ import ( ) type flowRun struct { - uuid flows.RunUUID - session flows.Session - environment envs.Environment + uuid flows.RunUUID + session flows.Session flow flows.Flow flowRef *assets.FlowReference @@ -57,7 +56,6 @@ func NewRun(session flows.Session, flow flows.Flow, parent flows.Run) flows.Run modifiedOn: now, } - r.environment = newRunEnvironment(session.Environment(), r) r.webhook = types.XObjectEmpty r.legacyExtra = newLegacyExtra(r) @@ -66,7 +64,7 @@ func NewRun(session flows.Session, flow flows.Flow, parent flows.Run) flows.Run func (r *flowRun) UUID() flows.RunUUID { return r.uuid } func (r *flowRun) Session() flows.Session { return r.session } -func (r *flowRun) Environment() envs.Environment { return r.environment } +func (r *flowRun) Environment() envs.Environment { return r.session.MergedEnvironment() } func (r *flowRun) Flow() flows.Flow { return r.flow } func (r *flowRun) FlowReference() *assets.FlowReference { return r.flowRef } @@ -462,8 +460,7 @@ func ReadRun(session flows.Session, data json.RawMessage, missing assets.Missing } } - // create a run specific environment and context - r.environment = newRunEnvironment(session.Environment(), r) + // create context r.webhook = lastWebhookSavedAsExtra(r) r.legacyExtra = newLegacyExtra(r) diff --git a/flows/triggers/base.go b/flows/triggers/base.go index 27976393e..593b91ef9 100644 --- a/flows/triggers/base.go +++ b/flows/triggers/base.go @@ -86,13 +86,11 @@ func (t *baseTrigger) Initialize(session flows.Session, logEvent flows.EventCall if t.environment != nil { session.SetEnvironment(t.environment) - } else { - session.SetEnvironment(envs.NewBuilder().Build()) } - if t.contact != nil { session.SetContact(t.contact.Clone()) } + return nil } From e3758b15f4127b57737477c5643a59f557767098 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 15 Aug 2023 13:48:21 -0500 Subject: [PATCH 2/3] Merge both derived environments --- flows/engine/session.go | 2 +- flows/environment.go | 103 ++++++++++++++---------------- flows/environment_test.go | 81 ++++++----------------- flows/routers/cases/tests_test.go | 82 ++++++++++++++++-------- 4 files changed, 121 insertions(+), 147 deletions(-) diff --git a/flows/engine/session.go b/flows/engine/session.go index 341560bf7..82ff832cc 100644 --- a/flows/engine/session.go +++ b/flows/engine/session.go @@ -61,7 +61,7 @@ func (s *session) SetType(type_ flows.FlowType) { s.type_ = type_ } func (s *session) Environment() envs.Environment { return s.env } func (s *session) SetEnvironment(env envs.Environment) { s.env = env } -func (s *session) MergedEnvironment() envs.Environment { return flows.NewMergedEnvironment(s) } +func (s *session) MergedEnvironment() envs.Environment { return flows.NewEnvironment(s) } func (s *session) Contact() *flows.Contact { return s.contact } func (s *session) SetContact(contact *flows.Contact) { s.contact = contact } diff --git a/flows/environment.go b/flows/environment.go index 8b3eedf5e..66e7c0ead 100644 --- a/flows/environment.go +++ b/flows/environment.go @@ -13,19 +13,62 @@ import ( type environment struct { envs.Environment + session Session locationResolver envs.LocationResolver } -// NewEnvironment creates a new environment -func NewEnvironment(base envs.Environment, la *LocationAssets) envs.Environment { +// NewEnvironment creates a new environment from a session's base environment that merges some properties with +// those from the contact, and adds support for location resolving using the session's locations assets. +func NewEnvironment(s Session) envs.Environment { var locationResolver envs.LocationResolver - hierarchies := la.Hierarchies() + hierarchies := s.Assets().Locations().Hierarchies() if len(hierarchies) > 0 { locationResolver = &assetLocationResolver{hierarchies[0]} } - return &environment{base, locationResolver} + return &environment{ + Environment: s.Environment(), + session: s, + locationResolver: locationResolver, + } +} + +func (e *environment) Timezone() *time.Location { + contact := e.session.Contact() + + // if we have a contact and they have a timezone that overrides the base enviroment's timezone + if contact != nil && contact.Timezone() != nil { + return contact.Timezone() + } + return e.Environment.Timezone() +} + +func (e *environment) DefaultLanguage() envs.Language { + contact := e.session.Contact() + + // if we have a contact and they have a language and it's an allowed language that overrides the base environment's languuage + if contact != nil && contact.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), contact.Language()) { + return contact.Language() + } + return e.Environment.DefaultLanguage() +} + +func (e *environment) DefaultCountry() envs.Country { + contact := e.session.Contact() + + // if we have a contact and they have a preferred channel with a country that overrides the base environment's country + if contact != nil { + cc := contact.Country() + if cc != envs.NilCountry { + return cc + } + } + return e.Environment.DefaultCountry() +} + +func (e *environment) DefaultLocale() envs.Locale { + return envs.NewLocale(e.DefaultLanguage(), e.DefaultCountry()) } func (e *environment) LocationResolver() envs.LocationResolver { @@ -82,55 +125,3 @@ func (r *assetLocationResolver) FindLocationsFuzzy(text string, level envs.Locat func (r *assetLocationResolver) LookupLocation(path envs.LocationPath) *envs.Location { return r.locations.FindByPath(path) } - -// an extended environment which takes some values from a contact if there is one and if the have those values. -type mergedEnvironment struct { - envs.Environment - - session Session -} - -// NewMergedEnvironment creates a new merged environment from a session's base environment and its contact -func NewMergedEnvironment(s Session) envs.Environment { - return &mergedEnvironment{ - NewEnvironment(s.Environment(), s.Assets().Locations()), - s, - } -} - -func (e *mergedEnvironment) Timezone() *time.Location { - contact := e.session.Contact() - - // if we have a contact and they have a timezone that overrides the base enviroment's timezone - if contact != nil && contact.Timezone() != nil { - return contact.Timezone() - } - return e.Environment.Timezone() -} - -func (e *mergedEnvironment) DefaultLanguage() envs.Language { - contact := e.session.Contact() - - // if we have a contact and they have a language and it's an allowed language that overrides the base environment's languuage - if contact != nil && contact.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), contact.Language()) { - return contact.Language() - } - return e.Environment.DefaultLanguage() -} - -func (e *mergedEnvironment) DefaultCountry() envs.Country { - contact := e.session.Contact() - - // if we have a contact and they have a preferred channel with a country that overrides the base environment's country - if contact != nil { - cc := contact.Country() - if cc != envs.NilCountry { - return cc - } - } - return e.Environment.DefaultCountry() -} - -func (e *mergedEnvironment) DefaultLocale() envs.Locale { - return envs.NewLocale(e.DefaultLanguage(), e.DefaultCountry()) -} diff --git a/flows/environment_test.go b/flows/environment_test.go index 97853587c..b62c6ae2d 100644 --- a/flows/environment_test.go +++ b/flows/environment_test.go @@ -10,12 +10,11 @@ import ( "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/engine" "github.com/nyaruka/goflow/flows/triggers" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var assets1JSON = `{ +var assetsJSON = `{ "flows": [ { "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", @@ -67,78 +66,34 @@ var assets1JSON = `{ ] }` -func TestEnvironment(t *testing.T) { +func TestEnvironmentLocationResolving(t *testing.T) { env := envs.NewBuilder().WithDefaultCountry("RW").Build() - source, err := static.NewSource([]byte(assets1JSON)) + source, err := static.NewSource([]byte(assetsJSON)) require.NoError(t, err) sa, err := engine.NewSessionAssets(env, source, nil) require.NoError(t, err) - fenv := flows.NewEnvironment(env, sa.Locations()) - assert.Equal(t, envs.Country("RW"), fenv.DefaultCountry()) - require.NotNil(t, fenv.LocationResolver()) + contact := flows.NewEmptyContact(sa, "", envs.NilLanguage, nil) + + trigger := triggers.NewBuilder(env, assets.NewFlowReference("76f0a02f-3b75-4b86-9064-e9195e1b3a02", "Test"), contact).Manual().Build() + eng := engine.NewBuilder().Build() + + session, _, err := eng.NewSession(sa, trigger) + require.NoError(t, err) + + senv := session.MergedEnvironment() + assert.Equal(t, envs.Country("RW"), senv.DefaultCountry()) + require.NotNil(t, senv.LocationResolver()) - kigali := fenv.LocationResolver().LookupLocation("Rwanda > Kigali City") + kigali := senv.LocationResolver().LookupLocation("Rwanda > Kigali City") assert.Equal(t, "Kigali City", kigali.Name()) - matches := fenv.LocationResolver().FindLocationsFuzzy("gisozi town", flows.LocationLevelWard, nil) + matches := senv.LocationResolver().FindLocationsFuzzy("gisozi town", flows.LocationLevelWard, nil) assert.Equal(t, 1, len(matches)) assert.Equal(t, "Gisozi", matches[0].Name()) } -var assets2JSON = `{ - "flows": [ - { - "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", - "name": "Test", - "spec_version": "13.1.0", - "language": "eng", - "type": "messaging", - "nodes": [] - } - ], - "channels": [ - { - "uuid": "57f1078f-88aa-46f4-a59a-948a5739c03d", - "name": "Android Channel", - "address": "+17036975131", - "schemes": ["tel"], - "roles": ["send", "receive"], - "country": "US" - } - ], - "locations": [ - { - "name": "Rwanda", - "aliases": ["Ruanda"], - "children": [ - { - "name": "Kigali City", - "aliases": ["Kigali", "Kigari"], - "children": [ - { - "name": "Gasabo", - "children": [ - { - "name": "Gisozi" - }, - { - "name": "Ndera" - } - ] - }, - { - "name": "Nyarugenge", - "children": [] - } - ] - } - ] - } - ] -}` - const contactJSON = `{ "uuid": "ba96bf7f-bc2a-4873-a7c7-254d1927c4e3", "id": 1234567, @@ -152,7 +107,7 @@ const contactJSON = `{ ] }` -func TestMergedEnvironment(t *testing.T) { +func TestEnvironmentMerging(t *testing.T) { tzRW, _ := time.LoadLocation("Africa/Kigali") tzEC, _ := time.LoadLocation("America/Guayaquil") tzUK, _ := time.LoadLocation("Europe/London") @@ -162,7 +117,7 @@ func TestMergedEnvironment(t *testing.T) { WithDefaultCountry("RW"). WithTimezone(tzRW). Build() - source, err := static.NewSource([]byte(assets2JSON)) + source, err := static.NewSource([]byte(assetsJSON)) require.NoError(t, err) sa, err := engine.NewSessionAssets(env, source, nil) diff --git a/flows/routers/cases/tests_test.go b/flows/routers/cases/tests_test.go index 37d4dd4c8..6585cf0b4 100644 --- a/flows/routers/cases/tests_test.go +++ b/flows/routers/cases/tests_test.go @@ -7,11 +7,14 @@ import ( "github.com/nyaruka/gocommon/dates" "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/assets/static" "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/excellent" "github.com/nyaruka/goflow/excellent/types" "github.com/nyaruka/goflow/flows" + "github.com/nyaruka/goflow/flows/engine" "github.com/nyaruka/goflow/flows/routers/cases" + "github.com/nyaruka/goflow/flows/triggers" "github.com/nyaruka/goflow/test" "github.com/stretchr/testify/assert" @@ -31,45 +34,59 @@ var ERROR = types.NewXErrorf("any error") var kgl, _ = time.LoadLocation("Africa/Kigali") -var locationHierarchyJSON = `{ - "name": "Rwanda", - "aliases": ["Ruanda"], - "children": [ +var assetsJSON = `{ + "flows": [ + { + "uuid": "76f0a02f-3b75-4b86-9064-e9195e1b3a02", + "name": "Test", + "spec_version": "13.1.0", + "language": "eng", + "type": "messaging", + "nodes": [] + } + ], + "locations": [ { - "name": "Kigali City", - "aliases": ["Kigali", "Kigari"], + "name": "Rwanda", + "aliases": ["Ruanda"], "children": [ { - "name": "Gasabo", + "name": "Kigali City", + "aliases": ["Kigali", "Kigari"], "children": [ { - "name": "Gisozi" + "name": "Gasabo", + "children": [ + { + "name": "Gisozi" + }, + { + "name": "Ndera" + } + ] }, { - "name": "Ndera" + "name": "Nyarugenge", + "children": [] } ] }, { - "name": "Nyarugenge", + "name": "Québec", + "aliases": ["Q.C", "Le Québec", "Quebec", "Que,", "Que", "Qc", "Québec"], + "children": [] + }, + { + "name": "Île-de-France", + "aliases": [], + "children": [] + }, + { + "name": "Paktika", + "aliases": ["Janikhel", "Terwa", "Yahyakhel", "Yusufkhel", "\u067e\u06a9\u062a\u06cc\u06a9\u0627", "\u062a\u0631\u0648\u0648", "\u06cc\u062d\u06cc\u06cc \u062e\u06cc\u0644", "\u06cc\u0648\u0633\u0641 \u062e\u06cc\u0644"], "children": [] } ] - }, - { - "name": "Québec", - "aliases": ["Q.C", "Le Québec", "Quebec", "Que,", "Que", "Qc", "Québec"], - "children": [] - }, - { - "name": "Île-de-France", - "aliases": [], - "children": [] - }, - { - "name": "Paktika", - "aliases": ["Janikhel", "Terwa", "Yahyakhel", "Yusufkhel", "\u067e\u06a9\u062a\u06cc\u06a9\u0627", "\u062a\u0631\u0648\u0648", "\u06cc\u062d\u06cc\u06cc \u062e\u06cc\u0644", "\u06cc\u0648\u0633\u0641 \u062e\u06cc\u0644"], - "children": [] } ] }` @@ -490,10 +507,21 @@ func TestTests(t *testing.T) { WithDefaultCountry(envs.Country("RW")). Build() - locations, err := envs.ReadLocationHierarchy([]byte(locationHierarchyJSON)) + source, err := static.NewSource([]byte(assetsJSON)) + require.NoError(t, err) + + sa, err := engine.NewSessionAssets(env, source, nil) + require.NoError(t, err) + + contact := flows.NewEmptyContact(sa, "", envs.NilLanguage, nil) + + trigger := triggers.NewBuilder(env, assets.NewFlowReference("76f0a02f-3b75-4b86-9064-e9195e1b3a02", "Test"), contact).Manual().Build() + eng := engine.NewBuilder().Build() + + session, _, err := eng.NewSession(sa, trigger) require.NoError(t, err) - env = flows.NewEnvironment(env, flows.NewLocationAssets([]assets.LocationHierarchy{locations})) + env = session.MergedEnvironment() for _, tc := range testTests { testID := fmt.Sprintf("%s(%#v)", tc.name, tc.args) From 0e94df84e64bc2a356ceee8a4faa2074bbce98cf Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 15 Aug 2023 14:13:13 -0500 Subject: [PATCH 3/3] Remove environment altogether from runs --- flows/actions/base.go | 8 ++++---- flows/actions/call_classifier.go | 2 +- flows/actions/send_msg.go | 2 +- flows/field_test.go | 2 +- flows/interfaces.go | 1 - flows/routers/switch.go | 6 +++--- flows/routers/waits/dial.go | 2 +- flows/runs/run.go | 19 +++++++++---------- 8 files changed, 20 insertions(+), 22 deletions(-) diff --git a/flows/actions/base.go b/flows/actions/base.go index 1b799f41f..38f37548b 100644 --- a/flows/actions/base.go +++ b/flows/actions/base.go @@ -178,7 +178,7 @@ func (a *baseAction) updateWebhook(run flows.Run, call *flows.WebhookCall) { // helper to apply a contact modifier func (a *baseAction) applyModifier(run flows.Run, mod flows.Modifier, logModifier flows.ModifierCallback, logEvent flows.EventCallback) bool { logModifier(mod) - return modifiers.Apply(run.Environment(), run.Session().Engine().Services(), run.Session().Assets(), run.Contact(), mod, logEvent) + return modifiers.Apply(run.Session().MergedEnvironment(), run.Session().Engine().Services(), run.Session().Assets(), run.Contact(), mod, logEvent) } // helper to log a failure @@ -266,11 +266,11 @@ func (a *otherContactsAction) resolveRecipients(run flows.Run, logEvent flows.Ev // next up try it as a URN urn := urns.URN(evaluatedLegacyVar) if urn.Validate() == nil { - urn = urn.Normalize(string(run.Environment().DefaultCountry())) + urn = urn.Normalize(string(run.Session().MergedEnvironment().DefaultCountry())) urnList = append(urnList, urn) } else { // if that fails, try to parse as phone number - parsedTel := utils.ParsePhoneNumber(evaluatedLegacyVar, string(run.Environment().DefaultCountry())) + parsedTel := utils.ParsePhoneNumber(evaluatedLegacyVar, string(run.Session().MergedEnvironment().DefaultCountry())) if parsedTel != "" { urn, _ := urns.NewURNFromParts(urns.TelScheme, parsedTel, "", "") urnList = append(urnList, urn) @@ -396,7 +396,7 @@ func resolveUser(run flows.Run, ref *assets.UserReference, logEvent flows.EventC } func currentLocale(run flows.Run, lang envs.Language) envs.Locale { - return envs.NewLocale(lang, run.Environment().DefaultCountry()) + return envs.NewLocale(lang, run.Session().MergedEnvironment().DefaultCountry()) } //------------------------------------------------------------------------------------------ diff --git a/flows/actions/call_classifier.go b/flows/actions/call_classifier.go index df698ec39..b6c0163a5 100644 --- a/flows/actions/call_classifier.go +++ b/flows/actions/call_classifier.go @@ -92,7 +92,7 @@ func (a *CallClassifierAction) classify(run flows.Run, step flows.Step, input st httpLogger := &flows.HTTPLogger{} - classification, err := svc.Classify(run.Environment(), input, httpLogger.Log) + classification, err := svc.Classify(run.Session().MergedEnvironment(), input, httpLogger.Log) if len(httpLogger.Logs) > 0 { logEvent(events.NewClassifierCalled(classifier.Reference(), httpLogger.Logs)) diff --git a/flows/actions/send_msg.go b/flows/actions/send_msg.go index 5ddf4faeb..b70617435 100644 --- a/flows/actions/send_msg.go +++ b/flows/actions/send_msg.go @@ -103,7 +103,7 @@ func (a *SendMsgAction) Execute(run flows.Run, step flows.Step, logModifier flow if a.Templating != nil { // looks for a translation in the contact locale or environment default locales := []envs.Locale{ - run.Environment().DefaultLocale(), + run.Session().MergedEnvironment().DefaultLocale(), run.Session().Environment().DefaultLocale(), } diff --git a/flows/field_test.go b/flows/field_test.go index 9b317408b..07c592a1c 100644 --- a/flows/field_test.go +++ b/flows/field_test.go @@ -81,7 +81,7 @@ func TestFieldValueParse(t *testing.T) { } for _, tc := range tcs { - actual := session.Contact().Fields().Parse(session.Runs()[0].Environment(), fields, tc.field, tc.value) + actual := session.Contact().Fields().Parse(session.MergedEnvironment(), fields, tc.field, tc.value) assert.Equal(t, tc.expected, actual, "parse mismatch for field %s and value '%s'", tc.field.Key(), tc.value) } diff --git a/flows/interfaces.go b/flows/interfaces.go index 342834406..bbc7c9915 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -397,7 +397,6 @@ type Run interface { RunSummary FlowReference() *assets.FlowReference - Environment() envs.Environment Session() Session SaveResult(*Result) SetStatus(RunStatus) diff --git a/flows/routers/switch.go b/flows/routers/switch.go index ac7c863bd..cf36a4529 100644 --- a/flows/routers/switch.go +++ b/flows/routers/switch.go @@ -118,7 +118,7 @@ func (r *SwitchRouter) Validate(flow flows.Flow, exits []flows.Exit) error { // Route determines which exit to take from a node func (r *SwitchRouter) Route(run flows.Run, step flows.Step, logEvent flows.EventCallback) (flows.ExitUUID, string, error) { - env := run.Environment() + env := run.Session().MergedEnvironment() // first evaluate our operand operand, err := run.EvaluateTemplateValue(r.operand) @@ -184,7 +184,7 @@ func (r *SwitchRouter) matchCase(run flows.Run, step flows.Step, operand types.X } // call our function - result := xtest.Call(run.Environment(), args) + result := xtest.Call(run.Session().MergedEnvironment(), args) // tests have to return either errors or test results switch typed := result.(type) { @@ -205,7 +205,7 @@ func (r *SwitchRouter) matchCase(run flows.Run, step flows.Step, operand types.X run.LogError(step, errors.Errorf("test %s returned non-object extra", strings.ToUpper(test))) } - resultAsStr, xerr := types.ToXText(run.Environment(), match) + resultAsStr, xerr := types.ToXText(run.Session().MergedEnvironment(), match) if xerr != nil { return "", "", nil, xerr } diff --git a/flows/routers/waits/dial.go b/flows/routers/waits/dial.go index 07d64813d..28ed7211f 100644 --- a/flows/routers/waits/dial.go +++ b/flows/routers/waits/dial.go @@ -66,7 +66,7 @@ func (w *DialWait) Begin(run flows.Run, log flows.EventCallback) bool { log(events.NewError(err)) } - urn, err := urns.NewTelURNForCountry(phone, string(run.Environment().DefaultCountry())) + urn, err := urns.NewTelURNForCountry(phone, string(run.Session().MergedEnvironment().DefaultCountry())) if err != nil { log(events.NewError(err)) return false diff --git a/flows/runs/run.go b/flows/runs/run.go index d46723496..3d0bb9336 100644 --- a/flows/runs/run.go +++ b/flows/runs/run.go @@ -62,9 +62,8 @@ func NewRun(session flows.Session, flow flows.Flow, parent flows.Run) flows.Run return r } -func (r *flowRun) UUID() flows.RunUUID { return r.uuid } -func (r *flowRun) Session() flows.Session { return r.session } -func (r *flowRun) Environment() envs.Environment { return r.session.MergedEnvironment() } +func (r *flowRun) UUID() flows.RunUUID { return r.uuid } +func (r *flowRun) Session() flows.Session { return r.session } func (r *flowRun) Flow() flows.Flow { return r.flow } func (r *flowRun) FlowReference() *assets.FlowReference { return r.flowRef } @@ -74,7 +73,7 @@ func (r *flowRun) Events() []flows.Event { return r.events } func (r *flowRun) Results() flows.Results { return r.results } func (r *flowRun) SaveResult(result *flows.Result) { // truncate value if necessary - result.Value = stringsx.Truncate(result.Value, r.Environment().MaxValueLength()) + result.Value = stringsx.Truncate(result.Value, r.session.MergedEnvironment().MaxValueLength()) r.results.Save(result) r.modifiedOn = dates.Now() @@ -307,16 +306,16 @@ func (r *flowRun) nodeContext(env envs.Environment) map[string]types.XValue { // EvaluateTemplate evaluates the given template in the context of this run func (r *flowRun) EvaluateTemplateValue(template string) (types.XValue, error) { - ctx := types.NewXObject(r.RootContext(r.Environment())) + ctx := types.NewXObject(r.RootContext(r.session.MergedEnvironment())) - return excellent.EvaluateTemplateValue(r.Environment(), ctx, template) + return excellent.EvaluateTemplateValue(r.session.MergedEnvironment(), ctx, template) } // EvaluateTemplateText evaluates the given template as text in the context of this run func (r *flowRun) EvaluateTemplateText(template string, escaping excellent.Escaping, truncate bool) (string, error) { - ctx := types.NewXObject(r.RootContext(r.Environment())) + ctx := types.NewXObject(r.RootContext(r.session.MergedEnvironment())) - value, err := excellent.EvaluateTemplate(r.Environment(), ctx, template, escaping) + value, err := excellent.EvaluateTemplate(r.session.MergedEnvironment(), ctx, template, escaping) if truncate { value = stringsx.TruncateEllipsis(value, r.Session().Engine().MaxTemplateChars()) } @@ -333,13 +332,13 @@ func (r *flowRun) getLanguages() []envs.Language { languages := make([]envs.Language, 0, 3) // if contact has an allowed language, it takes priority - contactLanguage := r.Environment().DefaultLanguage() + contactLanguage := r.session.MergedEnvironment().DefaultLanguage() if contactLanguage != envs.NilLanguage { languages = append(languages, contactLanguage) } // next we include the default language if it's different to the contact language - defaultLanguage := r.Session().Environment().DefaultLanguage() + defaultLanguage := r.session.Environment().DefaultLanguage() if defaultLanguage != envs.NilLanguage && defaultLanguage != contactLanguage { languages = append(languages, defaultLanguage) }