Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify environments #1174

Merged
merged 3 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions flows/actions/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}

//------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion flows/actions/call_classifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion flows/actions/send_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}

Expand Down
2 changes: 2 additions & 0 deletions flows/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions flows/engine/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.NewEnvironment(s) }

func (s *session) Contact() *flows.Contact { return s.contact }
func (s *session) SetContact(contact *flows.Contact) { s.contact = contact }
Expand Down
53 changes: 49 additions & 4 deletions flows/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,72 @@ package flows
import (
"regexp"
"strings"
"time"

"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/envs"
"golang.org/x/exp/slices"
)

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 {
Expand Down
93 changes: 86 additions & 7 deletions flows/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ 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"
)
Expand Down Expand Up @@ -64,22 +66,99 @@ var assetsJSON = `{
]
}`

func TestEnvironment(t *testing.T) {
func TestEnvironmentLocationResolving(t *testing.T) {
env := envs.NewBuilder().WithDefaultCountry("RW").Build()
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)

kigali := fenv.LocationResolver().LookupLocation("Rwanda > Kigali City")
senv := session.MergedEnvironment()
assert.Equal(t, envs.Country("RW"), senv.DefaultCountry())
require.NotNil(t, senv.LocationResolver())

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())
}

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 TestEnvironmentMerging(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)

// 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())
}
2 changes: 1 addition & 1 deletion flows/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion flows/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ type Session interface {

Environment() envs.Environment
SetEnvironment(envs.Environment)
MergedEnvironment() envs.Environment

Contact() *Contact
SetContact(*Contact)
Expand Down Expand Up @@ -396,7 +397,6 @@ type Run interface {
RunSummary
FlowReference() *assets.FlowReference

Environment() envs.Environment
Session() Session
SaveResult(*Result)
SetStatus(RunStatus)
Expand Down
Loading
Loading