Skip to content

Commit

Permalink
Merge pull request #1174 from nyaruka/simplify_envs
Browse files Browse the repository at this point in the history
Simplify environments
  • Loading branch information
rowanseymour authored Aug 15, 2023
2 parents 2fe2f47 + 0e94df8 commit 7b86d92
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 267 deletions.
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

0 comments on commit 7b86d92

Please sign in to comment.