From 576fd88050432e1b4c4aa5d1c4d0be9889587124 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 27 Jan 2020 17:23:44 -0800 Subject: [PATCH] (v2 - #1) copy User and related types from go-server-sdk (#3) * copy User and related types from go-server-sdk * lint * lint * rm references to obsolete function --- lduser/user.go | 291 +++++++++++++++++++++++++++++++++ lduser/user_builder.go | 356 +++++++++++++++++++++++++++++++++++++++++ lduser/user_test.go | 331 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 978 insertions(+) create mode 100644 lduser/user.go create mode 100644 lduser/user_builder.go create mode 100644 lduser/user_test.go diff --git a/lduser/user.go b/lduser/user.go new file mode 100644 index 0000000..467f9d8 --- /dev/null +++ b/lduser/user.go @@ -0,0 +1,291 @@ +package lduser + +import ( + "encoding/json" + "time" + + "gopkg.in/launchdarkly/go-sdk-common.v2/ldvalue" +) + +// A User contains specific attributes of a user browsing your site. The only mandatory property is the Key, +// which must uniquely identify each user. For authenticated users, this may be a username or e-mail address. +// For anonymous users, this could be an IP address or session ID. +// +// Besides the mandatory Key, User supports two kinds of optional attributes: interpreted attributes (e.g. +// Ip and Country) and custom attributes. LaunchDarkly can parse interpreted attributes and attach meaning +// to them. For example, from an IP address, LaunchDarkly can do a geo IP lookup and determine the user's +// country. +// +// Custom attributes are not parsed by LaunchDarkly. They can be used in custom rules-- for example, a custom +// attribute such as "customer_ranking" can be used to launch a feature to the top 10% of users on a site. +// +// User fields will be made private in the future, accessible only via getter methods, to prevent unsafe +// modification of users after they are created. The preferred method of constructing a User is to use either +// a simple constructor (NewUser, NewAnonymousUser) or the builder pattern with NewUserBuilder. If you do set +// the User fields directly, it is important not to change any map/slice elements, and not change a string +// that is pointed to by an existing pointer, after the User has been passed to any SDK methods; otherwise, +// flag evaluations and analytics events may refer to the wrong user properties (or, in the case of a map, +// you may even cause a concurrent modification panic). +type User struct { + // Key is the unique key of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Key *string `json:"key,omitempty" bson:"key,omitempty"` + // SecondaryKey is the secondary key of the user. + // + // This affects feature flag targeting + // (https://docs.launchdarkly.com/docs/targeting-users#section-targeting-rules-based-on-user-attributes) + // as follows: if you have chosen to bucket users by a specific attribute, the secondary key (if set) + // is used to further distinguish between users who are otherwise identical according to that attribute. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Secondary *string `json:"secondary,omitempty" bson:"secondary,omitempty"` + // Ip is the IP address attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Ip *string `json:"ip,omitempty" bson:"ip,omitempty"` //nolint (nonstandard capitalization) + // Country is the country attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Country *string `json:"country,omitempty" bson:"country,omitempty"` + // Email is the email address attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Email *string `json:"email,omitempty" bson:"email,omitempty"` + // FirstName is the first name attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + FirstName *string `json:"firstName,omitempty" bson:"firstName,omitempty"` + // LastName is the last name attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + LastName *string `json:"lastName,omitempty" bson:"lastName,omitempty"` + // Avatar is the avatar URL attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Avatar *string `json:"avatar,omitempty" bson:"avatar,omitempty"` + // Name is the name attribute of the user. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Name *string `json:"name,omitempty" bson:"name,omitempty"` + // Anonymous indicates whether the user is anonymous. + // + // If a user is anonymous, the user key will not appear on your LaunchDarkly dashboard. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Anonymous *bool `json:"anonymous,omitempty" bson:"anonymous,omitempty"` + // Custom is the user's map of custom attribute names and values. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Custom *map[string]interface{} `json:"custom,omitempty" bson:"custom,omitempty"` + // Derived is used internally by the SDK. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + Derived map[string]*DerivedAttribute `json:"derived,omitempty" bson:"derived,omitempty"` + + // PrivateAttributes contains a list of attribute names that were included in the user, + // but were marked as private. As such, these attributes are not included in the fields above. + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + PrivateAttributes []string `json:"privateAttrs,omitempty" bson:"privateAttrs,omitempty"` + + // This contains list of attributes to keep private, whether they appear at the top-level or Custom + // The attribute "key" is always sent regardless of whether it is in this list, and "custom" cannot be used to + // eliminate all custom attributes + // + // Deprecated: Direct access to User fields is now deprecated in favor of UserBuilder. In a future version, + // User fields will be private and only accessible via getter methods. + PrivateAttributeNames []string `json:"-" bson:"-"` +} + +// GetKey gets the unique key of the user. +func (u User) GetKey() string { + // Key is only nullable for historical reasons - all users should have a key + if u.Key == nil { + return "" + } + return *u.Key +} + +// GetSecondaryKey returns the secondary key of the user, if any. +// +// This affects feature flag targeting +// (https://docs.launchdarkly.com/docs/targeting-users#section-targeting-rules-based-on-user-attributes) +// as follows: if you have chosen to bucket users by a specific attribute, the secondary key (if set) +// is used to further distinguish between users who are otherwise identical according to that attribute. +func (u User) GetSecondaryKey() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Secondary) +} + +// GetIP returns the IP address attribute of the user, if any. +func (u User) GetIP() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Ip) +} + +// GetCountry returns the country attribute of the user, if any. +func (u User) GetCountry() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Country) +} + +// GetEmail returns the email address attribute of the user, if any. +func (u User) GetEmail() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Email) +} + +// GetFirstName returns the first name attribute of the user, if any. +func (u User) GetFirstName() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.FirstName) +} + +// GetLastName returns the last name attribute of the user, if any. +func (u User) GetLastName() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.LastName) +} + +// GetAvatar returns the avatar URL attribute of the user, if any. +func (u User) GetAvatar() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Avatar) +} + +// GetName returns the full name attribute of the user, if any. +func (u User) GetName() ldvalue.OptionalString { + return ldvalue.NewOptionalStringFromPointer(u.Name) +} + +// GetAnonymous returns the anonymous attribute of the user. +// +// If a user is anonymous, the user key will not appear on your LaunchDarkly dashboard. +func (u User) GetAnonymous() bool { + return u.Anonymous != nil && *u.Anonymous +} + +// GetAnonymousOptional returns the anonymous attribute of the user, with a second value indicating +// whether that attribute was defined for the user or not. +func (u User) GetAnonymousOptional() (bool, bool) { + return u.GetAnonymous(), u.Anonymous != nil +} + +// GetCustom returns a custom attribute of the user by name. The boolean second return value indicates +// whether any value was set for this attribute or not. +// +// The value is returned using the ldvalue.Value type, which can contain any type supported by JSON: +// boolean, number, string, array (slice), or object (map). Use Value methods to access the value as +// the desired type, rather than casting it. If the attribute did not exist, the value will be +// ldvalue.Null() and the second return value will be false. +func (u User) GetCustom(attrName string) (ldvalue.Value, bool) { + if u.Custom == nil { + return ldvalue.Null(), false + } + value, found := (*u.Custom)[attrName] + // Note: since the value is currently represented internally as interface{}, we are using a + // method that wraps the same interface{} in a Value, to avoid the overhead of a deep copy. + // This is designated as Unsafe because it is possible (using another Unsafe method) to access + // the interface{} value directly and, if it contains a slice or map, modify it. In a future + // version when the User fields are no longer exposed and backward compatibility is no longer + // necessary, a custom attribute will be stored as a completely immutable Value. + return ldvalue.UnsafeUseArbitraryValue(value), found //nolint (using deprecated method) +} + +// GetCustomKeys returns the keys of all custom attributes that have been set on this user. +func (u User) GetCustomKeys() []string { + if u.Custom == nil || len(*u.Custom) == 0 { + return nil + } + keys := make([]string, 0, len(*u.Custom)) + for key := range *u.Custom { + keys = append(keys, key) + } + return keys +} + +// Equal tests whether two users have equal attributes. +// +// Regular struct equality comparison is not allowed for User because it can contain slices and +// maps. This method is faster than using reflect.DeepEqual(), and also correctly ignores +// insignificant differences in the internal representation of the attributes. +func (u User) Equal(other User) bool { + if u.GetKey() != other.GetKey() || + u.GetSecondaryKey() != other.GetSecondaryKey() || + u.GetIP() != other.GetIP() || + u.GetCountry() != other.GetCountry() || + u.GetEmail() != other.GetEmail() || + u.GetFirstName() != other.GetFirstName() || + u.GetLastName() != other.GetLastName() || + u.GetAvatar() != other.GetAvatar() || + u.GetName() != other.GetName() || + u.GetAnonymous() != other.GetAnonymous() { + return false + } + if (u.Anonymous == nil) != (other.Anonymous == nil) || + u.Anonymous != nil && *u.Anonymous != *other.Anonymous { + return false + } + if (u.Custom == nil) != (other.Custom == nil) || + u.Custom != nil && len(*u.Custom) != len(*other.Custom) { + return false + } + if u.Custom != nil { + for k, v := range *u.Custom { + v1, ok := (*other.Custom)[k] + if !ok || v != v1 { + return false + } + } + } + if !stringSlicesEqual(u.PrivateAttributeNames, other.PrivateAttributeNames) { + return false + } + if !stringSlicesEqual(u.PrivateAttributes, other.PrivateAttributes) { + return false + } + return true +} + +// String returns a simple string representation of a user. +func (u User) String() string { + if bytes, err := json.Marshal(u); err == nil { + return string(bytes) + } + return "" +} + +// DerivedAttribute is an entry in a Derived attribute map and is for internal use by LaunchDarkly only.' +// Derived attributes sent to LaunchDarkly are ignored. +// +// Deprecated: this type is for internal use and will be removed in a future version. +type DerivedAttribute struct { + Value interface{} `json:"value" bson:"value"` + LastDerived time.Time `json:"lastDerived" bson:"lastDerived"` +} + +func stringSlicesEqual(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for _, n0 := range a { + ok := false + for _, n1 := range b { + if n1 == n0 { + ok = true + break + } + } + if !ok { + return false + } + } + return true +} diff --git a/lduser/user_builder.go b/lduser/user_builder.go new file mode 100644 index 0000000..3b5fb8a --- /dev/null +++ b/lduser/user_builder.go @@ -0,0 +1,356 @@ +package lduser + +import "gopkg.in/launchdarkly/go-sdk-common.v2/ldvalue" + +// NewUser creates a new user identified by the given key. +func NewUser(key string) User { + return User{Key: &key} +} + +// NewAnonymousUser creates a new anonymous user identified by the given key. +func NewAnonymousUser(key string) User { + anonymous := true + return User{Key: &key, Anonymous: &anonymous} +} + +// UserBuilder is a mutable object that uses the Builder pattern to specify properties for a User. +// This is the preferred method for constructing a User; direct access to User fields will be +// removed in a future version. +// +// Obtain an instance of UserBuilder by calling NewUserBuilder, then call setter methods such as +// Name to specify any additional user properties, then call Build() to construct the User. All of +// the UserBuilder setters return a reference the same builder, so they can be chained together: +// +// user := NewUserBuilder("user-key").Name("Bob").Email("test@example.com").Build() +// +// Setters for user attributes that can be designated private return the type +// UserBuilderCanMakeAttributePrivate, so you can chain the AsPrivateAttribute method: +// +// user := NewUserBuilder("user-key").Name("Bob").AsPrivateAttribute().Build() // Name is now private +// +// A UserBuilder should not be accessed by multiple goroutines at once. +type UserBuilder interface { + // Key changes the unique key for the user being built. + Key(value string) UserBuilder + + // Secondary sets the secondary key attribute for the user being built. + // + // This affects feature flag targeting + // (https://docs.launchdarkly.com/docs/targeting-users#section-targeting-rules-based-on-user-attributes) + // as follows: if you have chosen to bucket users by a specific attribute, the secondary key (if set) + // is used to further distinguish between users who are otherwise identical according to that attribute. + Secondary(value string) UserBuilderCanMakeAttributePrivate + + // IP sets the IP address attribute for the user being built. + IP(value string) UserBuilderCanMakeAttributePrivate + + // Country sets the country attribute for the user being built. + Country(value string) UserBuilderCanMakeAttributePrivate + + // Email sets the email attribute for the user being built. + Email(value string) UserBuilderCanMakeAttributePrivate + + // FirstName sets the first name attribute for the user being built. + FirstName(value string) UserBuilderCanMakeAttributePrivate + + // LastName sets the last name attribute for the user being built. + LastName(value string) UserBuilderCanMakeAttributePrivate + + // Avatar sets the avatar URL attribute for the user being built. + Avatar(value string) UserBuilderCanMakeAttributePrivate + + // Name sets the full name attribute for the user being built. + Name(value string) UserBuilderCanMakeAttributePrivate + + // Anonymous sets the anonymous attribute for the user being built. + // + // If a user is anonymous, the user key will not appear on your LaunchDarkly dashboard. + Anonymous(value bool) UserBuilder + + // Custom sets a custom attribute for the user being built. + // + // user := NewUserBuilder("user-key"). + // Custom("custom-attr-name", ldvalue.String("some-string-value")).AsPrivateAttribute(). + // Build() + Custom(name string, value ldvalue.Value) UserBuilderCanMakeAttributePrivate + + // Build creates a User from the current UserBuilder properties. + // + // The User is independent of the UserBuilder once you have called Build(); modifying the UserBuilder + // will not affect an already-created User. + Build() User +} + +// UserBuilderCanMakeAttributePrivate is an extension of UserBuilder that allows attributes to be +// made private via the AsPrivateAttribute() method. All UserBuilderCanMakeAttributePrivate setter +// methods are the same as UserBuilder, and apply to the original builder. +// +// UserBuilder setter methods for attributes that can be made private always return this interface. +// See AsPrivateAttribute for details. +type UserBuilderCanMakeAttributePrivate interface { + UserBuilder + + // AsPrivateAttribute marks the last attribute that was set on this builder as being a private + // attribute: that is, its value will not be sent to LaunchDarkly. + // + // This action only affects analytics events that are generated by this particular user object. To + // mark some (or all) user attributes as private for all users, use the Config properties + // PrivateAttributeName and AllAttributesPrivate. + // + // Most attributes can be made private, but Key and Anonymous cannot. This is enforced by the + // compiler, since the builder methods for attributes that can be made private are the only ones + // that return UserBuilderCanMakeAttributePrivate; therefore, you cannot write an expression like + // NewUserBuilder("user-key").AsPrivateAttribute(). + // + // In this example, FirstName and LastName are marked as private, but Country is not: + // + // user := NewUserBuilder("user-key"). + // FirstName("Pierre").AsPrivateAttribute(). + // LastName("Menard").AsPrivateAttribute(). + // Country("ES"). + // Build() + AsPrivateAttribute() UserBuilder + + // AsNonPrivateAttribute marks the last attribute that was set on this builder as not being a + // private attribute: that is, its value will be sent to LaunchDarkly and can appear on the dashboard. + // + // This is the opposite of AsPrivateAttribute(), and has no effect unless you have previously called + // AsPrivateAttribute() for the same attribute on the same user builder. For more details, see + // AsPrivateAttribute(). + AsNonPrivateAttribute() UserBuilder +} + +type userBuilderImpl struct { + key string + secondary ldvalue.OptionalString + ip ldvalue.OptionalString + country ldvalue.OptionalString + email ldvalue.OptionalString + firstName ldvalue.OptionalString + lastName ldvalue.OptionalString + avatar ldvalue.OptionalString + name ldvalue.OptionalString + anonymous bool + hasAnonymous bool + custom map[string]interface{} + privateAttrs map[string]bool +} + +type userBuilderCanMakeAttributePrivate struct { + builder *userBuilderImpl + attrName string +} + +// NewUserBuilder constructs a new UserBuilder, specifying the user key. +// +// For authenticated users, the key may be a username or e-mail address. For anonymous users, +// this could be an IP address or session ID. +func NewUserBuilder(key string) UserBuilder { + return &userBuilderImpl{key: key} +} + +// NewUserBuilderFromUser constructs a new UserBuilder, copying all attributes from an existing user. You may +// then call setter methods on the new UserBuilder to modify those attributes. +func NewUserBuilderFromUser(fromUser User) UserBuilder { + builder := &userBuilderImpl{ + secondary: fromUser.GetSecondaryKey(), + ip: fromUser.GetIP(), + country: fromUser.GetCountry(), + email: fromUser.GetEmail(), + firstName: fromUser.GetFirstName(), + lastName: fromUser.GetLastName(), + avatar: fromUser.GetAvatar(), + name: fromUser.GetName(), + } + if fromUser.Key != nil { + builder.key = *fromUser.Key + } + if fromUser.Anonymous != nil { + builder.anonymous = *fromUser.Anonymous + builder.hasAnonymous = true + } + if fromUser.Custom != nil { + builder.custom = make(map[string]interface{}, len(*fromUser.Custom)) + for k, v := range *fromUser.Custom { + builder.custom[k] = v + } + } + if len(fromUser.PrivateAttributeNames) > 0 { + builder.privateAttrs = make(map[string]bool, len(fromUser.PrivateAttributeNames)) + for _, name := range fromUser.PrivateAttributeNames { + builder.privateAttrs[name] = true + } + } + return builder +} + +func (b *userBuilderImpl) canMakeAttributePrivate(attrName string) UserBuilderCanMakeAttributePrivate { + return &userBuilderCanMakeAttributePrivate{builder: b, attrName: attrName} +} + +func (b *userBuilderImpl) Key(value string) UserBuilder { + b.key = value + return b +} + +func (b *userBuilderImpl) Secondary(value string) UserBuilderCanMakeAttributePrivate { + b.secondary = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("secondary") +} + +func (b *userBuilderImpl) IP(value string) UserBuilderCanMakeAttributePrivate { + b.ip = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("ip") +} + +func (b *userBuilderImpl) Country(value string) UserBuilderCanMakeAttributePrivate { + b.country = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("country") +} + +func (b *userBuilderImpl) Email(value string) UserBuilderCanMakeAttributePrivate { + b.email = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("email") +} + +func (b *userBuilderImpl) FirstName(value string) UserBuilderCanMakeAttributePrivate { + b.firstName = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("firstName") +} + +func (b *userBuilderImpl) LastName(value string) UserBuilderCanMakeAttributePrivate { + b.lastName = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("lastName") +} + +func (b *userBuilderImpl) Avatar(value string) UserBuilderCanMakeAttributePrivate { + b.avatar = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("avatar") +} + +func (b *userBuilderImpl) Name(value string) UserBuilderCanMakeAttributePrivate { + b.name = ldvalue.NewOptionalString(value) + return b.canMakeAttributePrivate("name") +} + +func (b *userBuilderImpl) Anonymous(value bool) UserBuilder { + b.anonymous = value + b.hasAnonymous = true + return b +} + +func (b *userBuilderImpl) Custom(name string, value ldvalue.Value) UserBuilderCanMakeAttributePrivate { + if b.custom == nil { + b.custom = make(map[string]interface{}) + } + // Note: since User.Custom is currently exported, and existing application code may expect to + // see only basic Go types in that map rather than ldvalue.Value instances, we are using a + // method that converts ldvalue.Value to a raw bool, string, map, etc. If it is a slice or a + // map, then it is mutable, which is undesirable; this is why direct access to User.Custom is + // deprecated. In a future version when backward compatibility is no longer an issue, a custom + // attribute will be stored as a completely immutable Value. + b.custom[name] = value.UnsafeArbitraryValue() //nolint (using deprecated method) + return b.canMakeAttributePrivate(name) +} + +func (b *userBuilderImpl) Build() User { + key := b.key + u := User{ + Key: &key, + Secondary: b.secondary.AsPointer(), + Ip: b.ip.AsPointer(), + Country: b.country.AsPointer(), + Email: b.email.AsPointer(), + FirstName: b.firstName.AsPointer(), + LastName: b.lastName.AsPointer(), + Avatar: b.avatar.AsPointer(), + Name: b.name.AsPointer(), + } + if b.hasAnonymous { + value := b.anonymous + u.Anonymous = &value + } + if len(b.custom) > 0 { + c := make(map[string]interface{}, len(b.custom)) + for k, v := range b.custom { + c[k] = v + } + u.Custom = &c + } + if len(b.privateAttrs) > 0 { + a := make([]string, 0, len(b.privateAttrs)) + for key, value := range b.privateAttrs { + if value { + a = append(a, key) + } + } + u.PrivateAttributeNames = a + } + return u +} + +func (b *userBuilderCanMakeAttributePrivate) AsPrivateAttribute() UserBuilder { + if b.builder.privateAttrs == nil { + b.builder.privateAttrs = make(map[string]bool) + } + b.builder.privateAttrs[b.attrName] = true + return b.builder +} + +func (b *userBuilderCanMakeAttributePrivate) AsNonPrivateAttribute() UserBuilder { + if b.builder.privateAttrs != nil { + delete(b.builder.privateAttrs, b.attrName) + } + return b.builder +} + +func (b *userBuilderCanMakeAttributePrivate) Key(value string) UserBuilder { + return b.builder.Key(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Secondary(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.Secondary(value) +} + +func (b *userBuilderCanMakeAttributePrivate) IP(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.IP(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Country(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.Country(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Email(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.Email(value) +} + +func (b *userBuilderCanMakeAttributePrivate) FirstName(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.FirstName(value) +} + +func (b *userBuilderCanMakeAttributePrivate) LastName(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.LastName(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Avatar(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.Avatar(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Name(value string) UserBuilderCanMakeAttributePrivate { + return b.builder.Name(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Anonymous(value bool) UserBuilder { + return b.builder.Anonymous(value) +} + +func (b *userBuilderCanMakeAttributePrivate) Custom( + name string, + value ldvalue.Value, +) UserBuilderCanMakeAttributePrivate { + return b.builder.Custom(name, value) +} + +func (b *userBuilderCanMakeAttributePrivate) Build() User { + return b.builder.Build() +} diff --git a/lduser/user_test.go b/lduser/user_test.go new file mode 100644 index 0000000..e68d569 --- /dev/null +++ b/lduser/user_test.go @@ -0,0 +1,331 @@ +package lduser + +import ( + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/launchdarkly/go-sdk-common.v2/ldvalue" +) + +type userStringPropertyDesc struct { + name string + getter func(User) ldvalue.OptionalString + setter func(UserBuilder, string) UserBuilderCanMakeAttributePrivate + deprecatedGetter func(User) *string +} + +var userSecondaryKeyProperty = userStringPropertyDesc{ + "secondary", + User.GetSecondaryKey, + UserBuilder.Secondary, + func(u User) *string { return u.Secondary }, +} +var userIPProperty = userStringPropertyDesc{ + "ip", + User.GetIP, + UserBuilder.IP, + func(u User) *string { return u.Ip }, +} +var userCountryProperty = userStringPropertyDesc{ + "country", + User.GetCountry, + UserBuilder.Country, + func(u User) *string { return u.Country }, +} +var userFirstNameProperty = userStringPropertyDesc{ + "firstName", + User.GetFirstName, + UserBuilder.FirstName, + func(u User) *string { return u.FirstName }, +} +var userLastNameProperty = userStringPropertyDesc{ + "lastName", + User.GetLastName, + UserBuilder.LastName, + func(u User) *string { return u.LastName }, +} +var userAvatarProperty = userStringPropertyDesc{ + "avatar", + User.GetAvatar, + UserBuilder.Avatar, + func(u User) *string { return u.Avatar }, +} +var userNameProperty = userStringPropertyDesc{ + "name", + User.GetName, + UserBuilder.Name, + func(u User) *string { return u.Name }, +} + +var allUserStringProperties = []userStringPropertyDesc{ + userSecondaryKeyProperty, + userIPProperty, + userCountryProperty, + userFirstNameProperty, + userLastNameProperty, + userAvatarProperty, + userNameProperty, +} + +func (p userStringPropertyDesc) assertNotSet(t *testing.T, user User) { + assert.Equal(t, ldvalue.OptionalString{}, p.getter(user), "should not have had a value for %s", p.name) + assert.Nil(t, p.deprecatedGetter(user), "should not have had a value for %s", p.name) +} + +func assertStringPropertiesNotSet(t *testing.T, user User) { + for _, p := range allUserStringProperties { + p.assertNotSet(t, user) + } +} + +func TestNewUser(t *testing.T) { + user := NewUser("some-key") + + assert.Equal(t, "some-key", user.GetKey()) + + for _, p := range allUserStringProperties { + p.assertNotSet(t, user) + } + assert.Nil(t, user.Anonymous) + assert.Nil(t, user.Custom) + assert.Nil(t, user.PrivateAttributeNames) +} + +func TestNewAnonymousUser(t *testing.T) { + user := NewAnonymousUser("some-key") + + assert.Equal(t, "some-key", user.GetKey()) + + for _, p := range allUserStringProperties { + p.assertNotSet(t, user) + } + assert.True(t, *user.Anonymous) + assert.Nil(t, user.Custom) + assert.Nil(t, user.PrivateAttributeNames) +} + +func TestUserBuilderSetsOnlyKeyByDefault(t *testing.T) { + user := NewUserBuilder("some-key").Build() + + assert.Equal(t, "some-key", user.GetKey()) + + for _, p := range allUserStringProperties { + p.assertNotSet(t, user) + } + assert.Nil(t, user.Anonymous) + assert.Nil(t, user.Custom) + assert.Nil(t, user.PrivateAttributeNames) +} + +func TestUserBuilderCanSetStringAttributes(t *testing.T) { + for _, p := range allUserStringProperties { + t.Run(p.name, func(t *testing.T) { + builder := NewUserBuilder("some-key") + p.setter(builder, "value") + user := builder.Build() + + assert.Equal(t, "some-key", user.GetKey()) + + for _, p1 := range allUserStringProperties { + if p1.name == p.name { + assert.Equal(t, ldvalue.NewOptionalString("value"), p.getter(user), p.name) + assert.NotNil(t, p.deprecatedGetter(user), p.name) + assert.Equal(t, "value", *p.deprecatedGetter(user), p.name) + } else { + p1.assertNotSet(t, user) + } + } + + assert.Nil(t, user.Anonymous) + assert.Nil(t, user.Custom) + assert.Nil(t, user.PrivateAttributeNames) + }) + } +} + +func TestUserBuilderCanSetAnonymous(t *testing.T) { + user0 := NewUserBuilder("some-key").Build() + assert.False(t, user0.GetAnonymous()) + value, ok := user0.GetAnonymousOptional() + assert.False(t, ok) + assert.False(t, value) + assert.Nil(t, user0.Anonymous) + + user1 := NewUserBuilder("some-key").Anonymous(true).Build() + assert.True(t, user1.GetAnonymous()) + value, ok = user1.GetAnonymousOptional() + assert.True(t, ok) + assert.True(t, value) + assert.NotNil(t, user1.Anonymous) + assert.True(t, *user1.Anonymous) + + user2 := NewUserBuilder("some-key").Anonymous(false).Build() + assert.False(t, user2.GetAnonymous()) + value, ok = user2.GetAnonymousOptional() + assert.True(t, ok) + assert.False(t, value) + assert.NotNil(t, user2.Anonymous) + assert.False(t, *user2.Anonymous) +} + +func TestUserBuilderCanSetPrivateStringAttributes(t *testing.T) { + for _, p := range allUserStringProperties { + t.Run(p.name, func(t *testing.T) { + builder := NewUserBuilder("some-key") + p.setter(builder, "value").AsPrivateAttribute() + user := builder.Build() + + assert.Equal(t, "some-key", user.GetKey()) + + for _, p1 := range allUserStringProperties { + if p1.name == p.name { + assert.Equal(t, ldvalue.NewOptionalString("value"), p.getter(user)) + assert.NotNil(t, p.deprecatedGetter(user), p.name) + assert.Equal(t, "value", *p.deprecatedGetter(user), p.name) + } else { + p1.assertNotSet(t, user) + } + } + + assert.Nil(t, user.Anonymous) + assert.Nil(t, user.Custom) + assert.Equal(t, []string{p.name}, user.PrivateAttributeNames) + }) + } +} + +func TestUserBuilderCanMakeAttributeNonPrivate(t *testing.T) { + builder := NewUserBuilder("some-key") + builder.Country("us").AsNonPrivateAttribute() + builder.Email("e").AsPrivateAttribute() + builder.Name("n").AsPrivateAttribute() + builder.Email("f").AsNonPrivateAttribute() + user := builder.Build() + assert.Equal(t, "f", *user.Email) + assert.Equal(t, []string{"name"}, user.PrivateAttributeNames) +} + +func TestUserBuilderCanSetCustomAttributes(t *testing.T) { + user := NewUserBuilder("some-key").Custom("first", ldvalue.Int(1)).Custom("second", ldvalue.String("two")).Build() + + value, ok := user.GetCustom("first") + assert.True(t, ok) + assert.Equal(t, 1, value.IntValue()) + + value, ok = user.GetCustom("second") + assert.True(t, ok) + assert.Equal(t, "two", value.StringValue()) + + value, ok = user.GetCustom("no") + assert.False(t, ok) + assert.Equal(t, ldvalue.Null(), value) + + keys := user.GetCustomKeys() + sort.Strings(keys) + assert.Equal(t, []string{"first", "second"}, keys) + + assert.Nil(t, user.PrivateAttributeNames) +} + +func TestUserWithNoCustomAttributes(t *testing.T) { + user := NewUser("some-key") + + assert.Nil(t, user.Custom) + + value, ok := user.GetCustom("attr") + assert.False(t, ok) + assert.Equal(t, ldvalue.Null(), value) + + assert.Nil(t, user.GetCustomKeys()) +} + +func TestUserBuilderCanSetPrivateCustomAttributes(t *testing.T) { + user := NewUserBuilder("some-key").Custom("first", ldvalue.Int(1)).AsPrivateAttribute(). + Custom("second", ldvalue.String("two")).Build() + + value, ok := user.GetCustom("first") + assert.True(t, ok) + assert.Equal(t, 1, value.IntValue()) + + value, ok = user.GetCustom("second") + assert.True(t, ok) + assert.Equal(t, "two", value.StringValue()) + + value, ok = user.GetCustom("no") + assert.False(t, ok) + assert.Equal(t, ldvalue.Null(), value) + + keys := user.GetCustomKeys() + sort.Strings(keys) + assert.Equal(t, []string{"first", "second"}, keys) + + assert.NotNil(t, user.PrivateAttributeNames) + assert.Equal(t, []string{"first"}, user.PrivateAttributeNames) +} + +func TestUserBuilderCanCopyFromExistingUserWithOnlyKey(t *testing.T) { + user0 := NewUser("some-key") + user1 := NewUserBuilderFromUser(user0).Build() + + assert.Equal(t, "some-key", user1.GetKey()) + + for _, p := range allUserStringProperties { + p.assertNotSet(t, user1) + } + assert.Nil(t, user1.Anonymous) + assert.Nil(t, user1.Custom) + assert.Nil(t, user1.PrivateAttributeNames) +} + +func TestUserBuilderCanCopyFromExistingUserWithAllAttributes(t *testing.T) { + user0 := newUserBuilderWithAllPropertiesSet("some-key").Build() + user1 := NewUserBuilderFromUser(user0).Build() + assert.Equal(t, user0, user1) +} + +func TestUserEqualsComparesAllAttributes(t *testing.T) { + shouldNotEqual := func(a User, b User) { + assert.False(t, b.Equal(a), "%s should not equal %s", b, a) + } + + user0 := NewUser("some-key") + assert.True(t, user0.Equal(user0), "%s should equal itself", user0) + + user1 := newUserBuilderWithAllPropertiesSet("some-key").Build() + assert.True(t, user1.Equal(user1), "%s should equal itself", user1) + user2 := NewUserBuilderFromUser(user1).Build() + assert.True(t, user2.Equal(user1), "%s should equal %s", user2, user1) + + for i, p := range allUserStringProperties { + builder3 := NewUserBuilderFromUser(user1) + p.setter(builder3, "different-value") + user3 := builder3.Build() + shouldNotEqual(user1, user3) + + builder4 := NewUserBuilderFromUser(user1) + p.setter(builder4, fmt.Sprintf("value%d", i)).AsPrivateAttribute() + user4 := builder4.Build() + shouldNotEqual(user1, user4) + } + + shouldNotEqual(user1, NewUserBuilderFromUser(user1).Key("other-key").Build()) + + shouldNotEqual(user0, NewUserBuilderFromUser(user0).Anonymous(true).Build()) + shouldNotEqual(NewUserBuilderFromUser(user0).Anonymous(true).Build(), NewUserBuilderFromUser(user0).Anonymous(false).Build()) + + shouldNotEqual(user1, NewUserBuilderFromUser(user1).Custom("thing1", ldvalue.String("value9")).Build()) + shouldNotEqual(user1, NewUserBuilderFromUser(user1).Custom("thing1", ldvalue.String("value1")).AsPrivateAttribute().Build()) +} + +func newUserBuilderWithAllPropertiesSet(key string) UserBuilder { + builder := NewUserBuilder(key) + for i, p := range allUserStringProperties { + p.setter(builder, fmt.Sprintf("value%d", i)) + } + builder.Anonymous(true) + builder.Custom("thing1", ldvalue.String("value1")) + builder.Custom("thing2", ldvalue.String("value2")).AsPrivateAttribute() + return builder +}