Skip to content

Commit

Permalink
Merge pull request #5 from launchdarkly/eb/ch58941/hide-user-fields
Browse files Browse the repository at this point in the history
(v2) hide all User fields, improve internal structure, add getters
  • Loading branch information
eli-darkly authored Jan 28, 2020
2 parents e63561b + b0924fd commit 67e75cc
Show file tree
Hide file tree
Showing 6 changed files with 596 additions and 325 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ clean:

test:
@# Note, we need to specify all these packages individually for go test in order to remain 1.8-compatible
go test -race -v ./ldvalue
go test -race -v ./ldvalue ./lduser

$(LINTER):
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s $(GOLANGCI_LINT_VERSION)
Expand Down
276 changes: 125 additions & 151 deletions lduser/user.go
Original file line number Diff line number Diff line change
@@ -1,113 +1,105 @@
package lduser

import (
"encoding/json"

"gopkg.in/launchdarkly/go-sdk-common.v2/ldvalue"
)

// UserAttribute is a string type representing the name of a user attribute.
//
// Constants like KeyAttribute describe all of the built-in attributes; you may also cast any string to
// UserAttribute when referencing a custom attribute name.
type UserAttribute string

const (
// KeyAttribute is the standard attribute name corresponding to User.GetKey().
KeyAttribute UserAttribute = "key"
// SecondaryKeyAttribute is the standard attribute name corresponding to User.GetSecondaryKey().
SecondaryKeyAttribute UserAttribute = "secondary"
// IPAttribute is the standard attribute name corresponding to User.GetIP().
IPAttribute UserAttribute = "ip"
// CountryAttribute is the standard attribute name corresponding to User.GetCountry().
CountryAttribute UserAttribute = "country"
// EmailAttribute is the standard attribute name corresponding to User.GetEmail().
EmailAttribute UserAttribute = "email"
// FirstNameAttribute is the standard attribute name corresponding to User.GetFirstName().
FirstNameAttribute UserAttribute = "firstName"
// LastNameAttribute is the standard attribute name corresponding to User.GetLastName().
LastNameAttribute UserAttribute = "lastName"
// AvatarAttribute is the standard attribute name corresponding to User.GetAvatar().
AvatarAttribute UserAttribute = "avatar"
// NameAttribute is the standard attribute name corresponding to User.GetName().
NameAttribute UserAttribute = "name"
// AnonymousAttribute is the standard attribute name corresponding to User.GetAnonymous().
AnonymousAttribute UserAttribute = "anonymous"
)

// 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
// 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).
// User fields are immutable and can be accessed only via getter methods. To construct a User, use either
// a simple constructor (NewUser, NewAnonymousUser) or the builder pattern with NewUserBuilder.
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" bson:"key"`
// 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 ldvalue.OptionalString `json:"secondary" bson:"secondary"`
// 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 ldvalue.OptionalString `json:"ip" bson:"ip"` //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 ldvalue.OptionalString `json:"country" bson:"country"`
// 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 ldvalue.OptionalString `json:"email" bson:"email"`
// 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 ldvalue.OptionalString `json:"firstName" bson:"firstName"`
// 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 ldvalue.OptionalString `json:"lastName" bson:"lastName"`
// 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 ldvalue.OptionalString `json:"avatar" bson:"avatar"`
// 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 ldvalue.OptionalString `json:"name" bson:"name"`
// 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 ldvalue.Value `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]ldvalue.Value `json:"custom,omitempty" bson:"custom,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:"-"`
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 ldvalue.Value
custom map[UserAttribute]ldvalue.Value
privateAttributes map[UserAttribute]struct{}
}

// GetAttribute returns one of the user's attributes.
//
// The attribute parameter specifies which attribute to get. To get a custom attribute rather than one
// of the built-in ones identified by the UserAttribute constants, simply cast any string to the
// UserAttribute type.
//
// If no value has been set for this attribute, GetAttribute returns ldvalue.Null().
func (u User) GetAttribute(attribute UserAttribute) ldvalue.Value {
switch attribute {
case KeyAttribute:
return ldvalue.String(u.key)
case SecondaryKeyAttribute:
return u.secondary.AsValue()
case IPAttribute:
return u.ip.AsValue()
case CountryAttribute:
return u.country.AsValue()
case EmailAttribute:
return u.email.AsValue()
case FirstNameAttribute:
return u.firstName.AsValue()
case LastNameAttribute:
return u.lastName.AsValue()
case AvatarAttribute:
return u.avatar.AsValue()
case NameAttribute:
return u.name.AsValue()
case AnonymousAttribute:
return u.anonymous
default:
value, _ := u.GetCustom(attribute)
return value
}
}

// GetKey gets the unique key of the user.
func (u User) GetKey() string {
return u.Key
return u.key
}

// GetSecondaryKey returns the secondary key of the user, if any.
Expand All @@ -117,55 +109,55 @@ func (u User) GetKey() string {
// 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 u.Secondary
return u.secondary
}

// GetIP returns the IP address attribute of the user, if any.
func (u User) GetIP() ldvalue.OptionalString {
return u.Ip
return u.ip
}

// GetCountry returns the country attribute of the user, if any.
func (u User) GetCountry() ldvalue.OptionalString {
return u.Country
return u.country
}

// GetEmail returns the email address attribute of the user, if any.
func (u User) GetEmail() ldvalue.OptionalString {
return u.Email
return u.email
}

// GetFirstName returns the first name attribute of the user, if any.
func (u User) GetFirstName() ldvalue.OptionalString {
return u.FirstName
return u.firstName
}

// GetLastName returns the last name attribute of the user, if any.
func (u User) GetLastName() ldvalue.OptionalString {
return u.LastName
return u.lastName
}

// GetAvatar returns the avatar URL attribute of the user, if any.
func (u User) GetAvatar() ldvalue.OptionalString {
return u.Avatar
return u.avatar
}

// GetName returns the full name attribute of the user, if any.
func (u User) GetName() ldvalue.OptionalString {
return u.Name
return 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.BoolValue()
return u.anonymous.BoolValue()
}

// 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.Anonymous.BoolValue(), !u.Anonymous.IsNull()
return u.anonymous.BoolValue(), !u.anonymous.IsNull()
}

// GetCustom returns a custom attribute of the user by name. The boolean second return value indicates
Expand All @@ -175,21 +167,28 @@ func (u User) GetAnonymousOptional() (bool, bool) {
// 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) {
value, found := u.Custom[attrName]
func (u User) GetCustom(attribute UserAttribute) (ldvalue.Value, bool) {
value, found := u.custom[attribute]
return value, found
}

// GetCustomKeys returns the keys of all custom attributes that have been set on this user.
func (u User) GetCustomKeys() []string {
if len(u.Custom) == 0 {
return nil
}
keys := make([]string, 0, len(u.Custom))
for key := range u.Custom {
keys = append(keys, key)
// ForCustom iterates through all custom attributes that have been set on this user.
//
// The specified function receives each custom attribute name and the corresponding value. This avoids
// the overhead of returning the attributes as a map, since to preserve immutability the map would
// have to be copied.
func (u User) ForCustom(fn func(string, ldvalue.Value)) {
for key, value := range u.custom {
fn(string(key), value)
}
return keys
}

// IsPrivateAttribute tests whether the given attribute is private for this user.
//
// The attribute name can either be a built-in attribute like NameAttribute or a custom one.
func (u User) IsPrivateAttribute(attribute UserAttribute) bool {
_, ok := u.privateAttributes[attribute]
return ok
}

// Equal tests whether two users have equal attributes.
Expand All @@ -198,57 +197,32 @@ func (u User) GetCustomKeys() []string {
// 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.Key != other.Key ||
u.Secondary != other.Secondary ||
u.Ip != other.Ip ||
u.Country != other.Country ||
u.Email != other.Email ||
u.FirstName != other.FirstName ||
u.LastName != other.LastName ||
u.Avatar != other.Avatar ||
u.Name != other.Name ||
!u.Anonymous.Equal(other.Anonymous) {
if u.key != other.key ||
u.secondary != other.secondary ||
u.ip != other.ip ||
u.country != other.country ||
u.email != other.email ||
u.firstName != other.firstName ||
u.lastName != other.lastName ||
u.avatar != other.avatar ||
u.name != other.name ||
!u.anonymous.Equal(other.anonymous) {
return false
}
if len(u.Custom) != len(other.Custom) {
if len(u.custom) != len(other.custom) {
return false
}
for k, v := range u.Custom {
v1, ok := other.Custom[k]
for k, v := range u.custom {
v1, ok := other.custom[k]
if !ok || !v.Equal(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 ""
}

func stringSlicesEqual(a []string, b []string) bool {
if len(a) != len(b) {
if len(u.privateAttributes) != len(other.privateAttributes) {
return false
}
for _, n0 := range a {
ok := false
for _, n1 := range b {
if n1 == n0 {
ok = true
break
}
}
if !ok {
for k := range u.privateAttributes {
if _, ok := other.privateAttributes[k]; !ok {
return false
}
}
Expand Down
Loading

0 comments on commit 67e75cc

Please sign in to comment.