...

Source file src/github.com/launchdarkly/go-sdk-common/v3/ldcontext/context_unmarshaling.go

Documentation: github.com/launchdarkly/go-sdk-common/v3/ldcontext

     1  package ldcontext
     2  
     3  import (
     4  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
     5  	"github.com/launchdarkly/go-sdk-common/v3/lderrors"
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
     7  
     8  	"github.com/launchdarkly/go-jsonstream/v3/jreader"
     9  )
    10  
    11  // See internAttributeNameIfPossible().
    12  var internCommonAttributeNamesMap = makeInternCommonAttributeNamesMap() //nolint:gochecknoglobals
    13  
    14  func makeInternCommonAttributeNamesMap() map[string]string {
    15  	ret := make(map[string]string)
    16  	for _, a := range []string{"email", "firstName", "lastName", "country", "ip", "avatar"} {
    17  		ret[a] = a
    18  	}
    19  	return ret
    20  }
    21  
    22  // UnmarshalJSON provides JSON deserialization for Context when using [encoding/json.UnmarshalJSON].
    23  //
    24  // LaunchDarkly's JSON schema for contexts is standardized across SDKs. For unmarshaling, there are
    25  // three supported formats:
    26  //
    27  //  1. A single context, identified by a top-level "kind" property that is not "multi".
    28  //  2. A multi-context, identified by a top-level "kind" property of "multi".
    29  //  3. A user context in the format used by older LaunchDarkly SDKs. This has no top-level "kind";
    30  //     its kind is assumed to be [DefaultKind]. It follows a different layout in which some predefined
    31  //     attribute names are top-level properties, while others are within a "custom" property. Also,
    32  //     unlike new Contexts, old-style users were allowed to have an empty string "" as a key.
    33  //
    34  // Trying to unmarshal any non-struct value, including a JSON null, into a [Context] will return a
    35  // json.UnmarshalTypeError. If you want to unmarshal optional context data that might be null, pass
    36  // a **Context rather than a *Context to json.Unmarshal.
    37  func (c *Context) UnmarshalJSON(data []byte) error {
    38  	r := jreader.NewReader(data)
    39  	return ContextSerialization.UnmarshalFromJSONReader(&r, c)
    40  }
    41  
    42  func unmarshalFromJSONReader(r *jreader.Reader, c *Context, usingEventFormat bool) {
    43  	// Do a first pass where we just check for the "kind" property, because that determines what
    44  	// schema we use to parse everything else.
    45  	kind, hasKind, err := parseKindOnly(r)
    46  	if err != nil {
    47  		r.AddError(err)
    48  		return
    49  	}
    50  	switch {
    51  	case !hasKind:
    52  		err = unmarshalOldUserSchema(c, r, usingEventFormat)
    53  	case kind == MultiKind:
    54  		err = unmarshalMultiKind(c, r, usingEventFormat)
    55  	default:
    56  		err = unmarshalSingleKind(c, r, "", usingEventFormat)
    57  	}
    58  	if err != nil {
    59  		r.AddError(err)
    60  	}
    61  }
    62  
    63  func parseKindOnly(originalReader *jreader.Reader) (Kind, bool, error) {
    64  	// Make an exact copy of the original Reader so that changes in its state will not
    65  	// affect the original Reader; both point to the same []byte array, but each has its
    66  	// own "current position" and "next token" fields.
    67  	r := *originalReader
    68  	for obj := r.Object(); obj.Next(); {
    69  		if string(obj.Name()) == ldattr.KindAttr {
    70  			kind := r.String()
    71  			if r.Error() == nil && kind == "" {
    72  				return "", false, lderrors.ErrContextKindEmpty{}
    73  			}
    74  			return Kind(kind), true, r.Error()
    75  			// We can immediately return here and not bother parsing the rest of the JSON object; we'll be
    76  			// creating another Reader that'll start over with the same byte slice for the second pass.
    77  		}
    78  		// If we see any property other than "kind" in this loop, just skip it. Calling SkipValue makes
    79  		// the Reader consume and discard the property value so we can advance to the next object property.
    80  		// Unfortunately, since JSON property ordering is indeterminate, we have no way to know how many
    81  		// properties we might see before we see "kind"-- if we see it at all.
    82  		_ = r.SkipValue()
    83  	}
    84  	return "", false, r.Error()
    85  }
    86  
    87  func readOptString(r *jreader.Reader) ldvalue.OptionalString {
    88  	if s, nonNull := r.StringOrNull(); nonNull {
    89  		return ldvalue.NewOptionalString(s)
    90  	}
    91  	return ldvalue.OptionalString{}
    92  }
    93  
    94  func unmarshalSingleKind(c *Context, r *jreader.Reader, knownKind Kind, usingEventFormat bool) error {
    95  	var b Builder
    96  	if knownKind != "" {
    97  		b.Kind(knownKind)
    98  	}
    99  	hasKey := false
   100  	for obj := r.Object(); obj.Next(); {
   101  		switch string(obj.Name()) {
   102  		case ldattr.KindAttr:
   103  			b.Kind(Kind(r.String()))
   104  		case ldattr.KeyAttr:
   105  			// Null isn't allowed for the key, but rather than just calling r.String() so that the parser would
   106  			// signal an error if it saw anything other than a string, we're calling r.StringOrNull() here so
   107  			// we can detect the null case and report it as a more specific error. This is used by LaunchDarkly
   108  			// service code for better reporting on any invalid data we may receive.
   109  			if s, nonNull := r.StringOrNull(); nonNull {
   110  				b.Key(s)
   111  				hasKey = true
   112  			} else {
   113  				return lderrors.ErrContextKeyNull{}
   114  			}
   115  		case ldattr.NameAttr:
   116  			b.OptName(readOptString(r))
   117  		case ldattr.AnonymousAttr:
   118  			b.Anonymous(r.Bool())
   119  		case jsonPropMeta:
   120  			for metaObj := r.ObjectOrNull(); metaObj.Next(); {
   121  				switch string(metaObj.Name()) {
   122  				case jsonPropPrivate:
   123  					if usingEventFormat {
   124  						_ = r.SkipValue()
   125  						continue
   126  					}
   127  					readPrivateAttributes(r, &b, false)
   128  				case jsonPropRedacted:
   129  					if !usingEventFormat {
   130  						_ = r.SkipValue()
   131  						continue
   132  					}
   133  					readPrivateAttributes(r, &b, false)
   134  				default:
   135  					// Unrecognized property names within _meta are ignored. Calling SkipValue makes the Reader
   136  					// consume and discard the property value so we can advance to the next object property.
   137  					_ = r.SkipValue()
   138  				}
   139  			}
   140  		default:
   141  			var v ldvalue.Value
   142  			v.ReadFromJSONReader(r)
   143  			b.SetValue(internAttributeNameIfPossible(obj.Name()), v)
   144  		}
   145  	}
   146  	if r.Error() != nil {
   147  		return r.Error()
   148  	}
   149  	if !hasKey {
   150  		return lderrors.ErrContextKeyMissing{}
   151  	}
   152  	*c = b.Build()
   153  	return c.Err()
   154  }
   155  
   156  func unmarshalMultiKind(c *Context, r *jreader.Reader, usingEventFormat bool) error {
   157  	var b MultiBuilder
   158  	for obj := r.Object(); obj.Next(); {
   159  		name := string(obj.Name())
   160  		if name == ldattr.KindAttr {
   161  			_ = r.SkipValue()
   162  			continue
   163  		}
   164  		var subContext Context
   165  		if err := unmarshalSingleKind(&subContext, r, Kind(name), usingEventFormat); err != nil {
   166  			return err
   167  		}
   168  		b.Add(subContext)
   169  	}
   170  	*c = b.Build()
   171  	return c.Err()
   172  }
   173  
   174  func unmarshalOldUserSchema(c *Context, r *jreader.Reader, usingEventFormat bool) error {
   175  	var b Builder
   176  	b.setAllowEmptyKey(true)
   177  	var secondary ldvalue.OptionalString
   178  	hasKey := false
   179  	for obj := r.Object(); obj.Next(); {
   180  		switch string(obj.Name()) {
   181  		case ldattr.KeyAttr:
   182  			b.Key(r.String())
   183  			hasKey = true
   184  		case ldattr.NameAttr:
   185  			b.OptName(readOptString(r))
   186  		case jsonPropOldUserSecondary:
   187  			secondary = readOptString(r)
   188  		case ldattr.AnonymousAttr:
   189  			value, _ := r.BoolOrNull()
   190  			b.Anonymous(value)
   191  		case jsonPropOldUserCustom:
   192  			for customObj := r.ObjectOrNull(); customObj.Next(); {
   193  				name := string(customObj.Name())
   194  				var value ldvalue.Value
   195  				value.ReadFromJSONReader(r)
   196  				if isOldUserCustomAttributeNameAllowed(name) {
   197  					b.SetValue(name, value)
   198  				}
   199  			}
   200  		case jsonPropOldUserPrivate:
   201  			if usingEventFormat {
   202  				_ = r.SkipValue()
   203  				continue
   204  			}
   205  			readPrivateAttributes(r, &b, true)
   206  			// The "true" here means to interpret the strings as literal attribute names, since the
   207  			// attribute reference path syntax was not used in the old user schema.
   208  		case jsonPropOldUserRedacted:
   209  			if !usingEventFormat {
   210  				_ = r.SkipValue()
   211  				continue
   212  			}
   213  			readPrivateAttributes(r, &b, true)
   214  		case "firstName", "lastName", "email", "country", "avatar", "ip":
   215  			if s := readOptString(r); s.IsDefined() {
   216  				b.SetString(internAttributeNameIfPossible(obj.Name()), s.StringValue())
   217  			}
   218  		default:
   219  			// In the old user schema, unrecognized top-level property names are ignored. Calling SkipValue
   220  			// makes the Reader consume and discard the property value so we can advance to the next object property.
   221  			_ = r.SkipValue()
   222  		}
   223  	}
   224  	if r.Error() != nil {
   225  		return r.Error()
   226  	}
   227  	if !hasKey {
   228  		return lderrors.ErrContextKeyMissing{}
   229  	}
   230  	*c = b.Build()
   231  	if secondary.IsDefined() {
   232  		c.secondary = secondary // there is deliberately no way to do this via the builder API
   233  	}
   234  	return c.Err()
   235  }
   236  
   237  func isOldUserCustomAttributeNameAllowed(name string) bool {
   238  	// If we see any of these names within the "custom": {} object in old-style user JSON, logically
   239  	// we can't use it because it would collide with a top-level property.
   240  	switch name {
   241  	case ldattr.KindAttr, ldattr.KeyAttr, ldattr.NameAttr, ldattr.AnonymousAttr, jsonPropMeta:
   242  		return false
   243  	default:
   244  		return true
   245  	}
   246  }
   247  
   248  // internAttributeNameIfPossible takes a byte slice representing a property name, and returns an existing
   249  // string if we already have a string literal equal to that name; otherwise it converts the bytes to a string.
   250  //
   251  // The reason for this logic is that LaunchDarkly-enabled applications will generally send the same attribute
   252  // names over and over again, and we can guess what many of them will be. The old user model had standard
   253  // top-level properties with predefined names like "email", which now are mostly considered custom attributes
   254  // that are stored as map entries instead of struct fields. In a high-traffic environment where many contexts
   255  // are being deserialized, i.e. the LD client-side service endpoints, if we are servicing 1000 requests that
   256  // each have users with "firstName" and "lastName" attributes, it's desirable to reuse those strings rather
   257  // than allocating a new string each time; the overall memory usage may be negligible but the allocation and
   258  // GC overhead still adds up.
   259  //
   260  // Recent versions of Go have an optimization for looking up string(x) as a string key in a map if x is a
   261  // byte slice, so that it does *not* have to allocate a string instance just to do this.
   262  func internAttributeNameIfPossible(nameBytes []byte) string {
   263  	if internedName, ok := internCommonAttributeNamesMap[string(nameBytes)]; ok {
   264  		return internedName
   265  	}
   266  	return string(nameBytes)
   267  }
   268  
   269  func unmarshalWithKindAndKeyOnly(r *jreader.Reader, c *Context) {
   270  	kind, hasKind, err := parseKindOnly(r)
   271  	if err != nil {
   272  		r.AddError(err)
   273  		return
   274  	}
   275  	switch {
   276  	case !hasKind:
   277  		err = unmarshalWithKindAndKeyOnlyOldUser(r, c)
   278  	case kind == MultiKind:
   279  		var mb MultiBuilder
   280  		for obj := r.Object(); obj.Next(); {
   281  			switch string(obj.Name()) {
   282  			case ldattr.KindAttr:
   283  				_ = r.SkipValue()
   284  			default:
   285  				kind := Kind(obj.Name())
   286  				var mc Context
   287  				if err = unmarshalWithKindAndKeyOnlySingleKind(r, &mc, kind); err != nil {
   288  					break
   289  				}
   290  				mb.Add(mc)
   291  			}
   292  		}
   293  		*c = mb.Build()
   294  		err = c.Err()
   295  	default:
   296  		err = unmarshalWithKindAndKeyOnlySingleKind(r, c, "")
   297  	}
   298  	if err != nil {
   299  		r.AddError(err)
   300  	}
   301  }
   302  
   303  func unmarshalWithKindAndKeyOnlySingleKind(r *jreader.Reader, c *Context, kind Kind) error {
   304  	var key string
   305  	hasKey := false
   306  	for obj := r.Object(); obj.Next(); {
   307  		switch string(obj.Name()) {
   308  		case ldattr.KindAttr:
   309  			kind = Kind(r.String())
   310  		case ldattr.KeyAttr:
   311  			key = r.String()
   312  			hasKey = true
   313  		default:
   314  			_ = r.SkipValue()
   315  		}
   316  	}
   317  	if !hasKey {
   318  		r.AddError(lderrors.ErrContextKeyMissing{})
   319  		return lderrors.ErrContextKeyMissing{}
   320  	}
   321  	*c = NewWithKind(kind, key)
   322  	return c.Err()
   323  }
   324  
   325  func unmarshalWithKindAndKeyOnlyOldUser(r *jreader.Reader, c *Context) error {
   326  	for obj := r.Object(); obj.Next(); {
   327  		switch string(obj.Name()) {
   328  		case ldattr.KeyAttr:
   329  			key := r.String()
   330  			var b Builder
   331  			*c = b.setAllowEmptyKey(true).Key(key).Build()
   332  			return c.Err()
   333  		default:
   334  			_ = r.SkipValue()
   335  		}
   336  	}
   337  	r.AddError(lderrors.ErrContextKeyMissing{})
   338  	return lderrors.ErrContextKeyMissing{}
   339  }
   340  
   341  func readPrivateAttributes(r *jreader.Reader, b *Builder, asLiterals bool) {
   342  	for privateArr := r.ArrayOrNull(); privateArr.Next(); {
   343  		b.PrivateRef(refOrLiteralRef(r.String(), asLiterals))
   344  	}
   345  }
   346  
   347  func refOrLiteralRef(s string, asLiteral bool) ldattr.Ref {
   348  	if asLiteral {
   349  		return ldattr.NewLiteralRef(s)
   350  	}
   351  	return ldattr.NewRef(s)
   352  }
   353  

View as plain text