...

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

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

     1  //go:build launchdarkly_easyjson
     2  
     3  package ldcontext
     4  
     5  import (
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
     7  	"github.com/launchdarkly/go-sdk-common/v3/lderrors"
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
     9  
    10  	"github.com/launchdarkly/go-jsonstream/v3/jwriter"
    11  
    12  	"github.com/mailru/easyjson/jlexer"
    13  	ej_jwriter "github.com/mailru/easyjson/jwriter"
    14  )
    15  
    16  // This conditionally-compiled file provides custom marshal and unmarshal functions for the Context
    17  // type in EasyJSON.
    18  //
    19  // EasyJSON's code generator does recognize the same MarshalJSON and UnmarshalJSON methods used by
    20  // encoding/json, and will call them if present. But this mechanism is inefficient: when marshaling
    21  // it requires the allocation of intermediate byte slices, and when unmarshaling it causes the
    22  // JSON object to be parsed twice. It is preferable to have our marshal/unmarshal methods write to
    23  // and read from the EasyJSON Writer/Lexer directly.
    24  //
    25  // Unmarshaling is the most performance-critical code path, because the client-side endpoints of
    26  // the LD back-end use this implementation to get the context parameters for every request. So,
    27  // rather than using an adapter to delegate jsonstream operations to EasyJSON, as we do for many
    28  // other types-- which is preferred if performance is a bit less critical, because then we only
    29  // have to write the logic once-- the Context unmarshaler is fully reimplemented here with direct
    30  // calls to EasyJSON lexer methods. This allows us to take full advantage of EasyJSON optimizations
    31  // that are available in our service code but may not be available in customer application code,
    32  // such as the use of the unsafe package for direct []byte-to-string conversion.
    33  //
    34  // This means that if we make changes to the schema or the unmarshaling logic, we will need to
    35  // update both context_unmarshaling.go and context_easyjson.go. Our unit tests run the same test
    36  // data against both implementations to verify that they are in sync.
    37  //
    38  // For more information, see: https://github.com/launchdarkly/go-jsonstream/v3
    39  
    40  // Arbitrary preallocation size that's likely to be longer than we will need for private/redacted
    41  // attribute lists, to minimize reallocations during unmarshaling.
    42  const initialAttrListAllocSize = 10
    43  
    44  // MarshalEasyJSON is the marshaler method for Context when using the EasyJSON API. Because
    45  // marshaling of contexts is not a requirement in high-traffic LaunchDarkly services, the
    46  // current implementation delegates to the default non-EasyJSON marshaler.
    47  //
    48  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
    49  func (c Context) MarshalEasyJSON(writer *ej_jwriter.Writer) {
    50  	if err := c.Err(); err != nil {
    51  		writer.Error = err
    52  		return
    53  	}
    54  	wrappedWriter := jwriter.NewWriterFromEasyJSONWriter(writer)
    55  	ContextSerialization.MarshalToJSONWriter(&wrappedWriter, &c)
    56  }
    57  
    58  // MarshalEasyJSON is the marshaler method for EventOutputContext when using the EasyJSON API.
    59  // Because marshaling of contexts is not a requirement in high-traffic LaunchDarkly services,
    60  // the current implementation delegates to the default non-EasyJSON marshaler.
    61  //
    62  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
    63  func (c EventOutputContext) MarshalEasyJSON(writer *ej_jwriter.Writer) {
    64  	if err := c.Err(); err != nil {
    65  		writer.Error = err
    66  		return
    67  	}
    68  	wrappedWriter := jwriter.NewWriterFromEasyJSONWriter(writer)
    69  	ContextSerialization.MarshalToJSONWriterEventOutput(&wrappedWriter, &c)
    70  }
    71  
    72  // UnmarshalEasyJSON is the unmarshaler method for Context when using the EasyJSON API. Because
    73  // unmarshaling of contexts is a requirement in high-traffic LaunchDarkly services, this
    74  // implementation is optimized for speed and memory usage and does not share code with the default
    75  // unmarshaler.
    76  //
    77  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
    78  func (c *Context) UnmarshalEasyJSON(in *jlexer.Lexer) {
    79  	ContextSerialization.UnmarshalFromEasyJSONLexer(in, c)
    80  }
    81  
    82  // UnmarshalEasyJSON is the unmarshaler method for Context when using the EasyJSON API. Because
    83  // unmarshaling of contexts is a requirement in high-traffic LaunchDarkly services, this
    84  // implementation is optimized for speed and memory usage and does not share code with the default
    85  // unmarshaler.
    86  //
    87  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
    88  func (c *EventOutputContext) UnmarshalEasyJSON(in *jlexer.Lexer) {
    89  	ContextSerialization.UnmarshalFromEasyJSONLexerEventOutput(in, c)
    90  }
    91  
    92  // Note: other ContextSerialization methods are defined in context_serialization.go.
    93  
    94  // UnmarshalFromEasyJSONLexer unmarshals a Context with the EasyJSON API. Because unmarshaling
    95  // of contexts is a requirement in high-traffic LaunchDarkly services, this implementation is
    96  // optimized for speed and memory usage and does not share code with the default unmarshaler.
    97  //
    98  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
    99  func (s ContextSerializationMethods) UnmarshalFromEasyJSONLexer(in *jlexer.Lexer, c *Context) {
   100  	unmarshalFromEasyJSONLexer(in, c, false)
   101  }
   102  
   103  // UnmarshalFromEasyJSONLexerEventOutput unmarshals an EventContext with the EasyJSON API.
   104  // Because unmarshaling of contexts in event data is a requirement in high-traffic LaunchDarkly
   105  // services, this implementation is optimized for speed and memory usage and does not share code
   106  // with the default unmarshaler.
   107  //
   108  // This method is only available when compiling with the build tag "launchdarkly_easyjson".
   109  func (s ContextSerializationMethods) UnmarshalFromEasyJSONLexerEventOutput(in *jlexer.Lexer, c *EventOutputContext) {
   110  	unmarshalFromEasyJSONLexer(in, &c.Context, true)
   111  }
   112  
   113  func unmarshalFromEasyJSONLexer(in *jlexer.Lexer, c *Context, usingEventFormat bool) {
   114  	if in.IsNull() {
   115  		in.Delim('{') // to trigger an "expected an object, got null" error
   116  		return
   117  	}
   118  
   119  	// Do a first pass where we just check for the "kind" property, because that determines what
   120  	// schema we use to parse everything else.
   121  	kind, hasKind, err := parseKindOnlyEasyJSON(in)
   122  	if err != nil {
   123  		in.AddError(err)
   124  		return
   125  	}
   126  
   127  	switch {
   128  	case !hasKind:
   129  		unmarshalOldUserSchemaEasyJSON(c, in, usingEventFormat)
   130  	case kind == MultiKind:
   131  		unmarshalMultiKindEasyJSON(c, in, usingEventFormat)
   132  	default:
   133  		unmarshalSingleKindEasyJSON(c, in, "", usingEventFormat)
   134  	}
   135  }
   136  
   137  func unmarshalSingleKindEasyJSON(c *Context, in *jlexer.Lexer, knownKind Kind, usingEventFormat bool) {
   138  	c.defined = true
   139  	if knownKind != "" {
   140  		c.kind = Kind(knownKind)
   141  	}
   142  	hasKey := false
   143  	var attributes ldvalue.ValueMapBuilder
   144  	in.Delim('{')
   145  	for !in.IsDelim('}') {
   146  		// Because the field name will often be a literal that we won't be retaining, we don't want the overhead
   147  		// of allocating a string for it every time. So we call UnsafeBytes(), which still reads a JSON string
   148  		// like String(), but returns the data as a subslice of the existing byte slice if possible-- allocating
   149  		// a new byte slice only in the unlikely case that there were escape sequences. Go's switch statement is
   150  		// optimized so that doing "switch string(key)" does *not* allocate a string, but just uses the bytes.
   151  		key := in.UnsafeBytes()
   152  		in.WantColon()
   153  		switch string(key) {
   154  		case ldattr.KindAttr:
   155  			c.kind = Kind(in.String())
   156  		case ldattr.KeyAttr:
   157  			c.key = in.String()
   158  			hasKey = true
   159  		case ldattr.NameAttr:
   160  			c.name = readOptStringEasyJSON(in)
   161  		case ldattr.AnonymousAttr:
   162  			c.anonymous = in.Bool()
   163  		case jsonPropMeta:
   164  			if in.IsNull() {
   165  				in.Skip()
   166  				break
   167  			}
   168  			in.Delim('{')
   169  			for !in.IsDelim('}') {
   170  				key := in.UnsafeBytes() // see comment above
   171  				in.WantColon()
   172  				switch {
   173  				case string(key) == jsonPropPrivate && !usingEventFormat:
   174  					readPrivateAttributesEasyJSON(in, c, false)
   175  				case string(key) == jsonPropRedacted && usingEventFormat:
   176  					readPrivateAttributesEasyJSON(in, c, false)
   177  				default:
   178  					// Unrecognized property names within _meta are ignored. Calling SkipRecursive makes the Lexer
   179  					// consume and discard the property value so we can advance to the next object property.
   180  					in.SkipRecursive()
   181  				}
   182  				in.WantComma()
   183  			}
   184  			in.Delim('}')
   185  		default:
   186  			if in.IsNull() {
   187  				in.Skip()
   188  			} else {
   189  				var v ldvalue.Value
   190  				v.UnmarshalEasyJSON(in)
   191  				attributes.Set(internAttributeNameIfPossible(key), v)
   192  			}
   193  		}
   194  		in.WantComma()
   195  	}
   196  	in.Delim('}')
   197  	if in.Error() != nil {
   198  		return
   199  	}
   200  	if !hasKey {
   201  		in.AddError(lderrors.ErrContextKeyMissing{})
   202  		return
   203  	}
   204  	c.kind, c.err = validateSingleKind(c.kind)
   205  	if c.err != nil {
   206  		in.AddError(c.err)
   207  		return
   208  	}
   209  	if c.key == "" {
   210  		c.err = lderrors.ErrContextKeyEmpty{}
   211  		in.AddError(c.err)
   212  	} else {
   213  		c.fullyQualifiedKey = makeFullyQualifiedKeySingleKind(c.kind, c.key, true)
   214  		c.attributes = attributes.Build()
   215  	}
   216  }
   217  
   218  func unmarshalMultiKindEasyJSON(c *Context, in *jlexer.Lexer, usingEventFormat bool) {
   219  	var b MultiBuilder
   220  	in.Delim('{')
   221  	for !in.IsDelim('}') {
   222  		name := in.String()
   223  		in.WantColon()
   224  		if name == ldattr.KindAttr {
   225  			in.SkipRecursive()
   226  		} else {
   227  			var subContext Context
   228  			unmarshalSingleKindEasyJSON(&subContext, in, Kind(name), usingEventFormat)
   229  			b.Add(subContext)
   230  		}
   231  		in.WantComma()
   232  	}
   233  	in.Delim('}')
   234  	if in.Error() == nil {
   235  		*c = b.Build()
   236  		if err := c.Err(); err != nil {
   237  			in.AddError(err)
   238  		}
   239  	}
   240  }
   241  
   242  func unmarshalOldUserSchemaEasyJSON(c *Context, in *jlexer.Lexer, usingEventFormat bool) {
   243  	c.defined = true
   244  	c.kind = DefaultKind
   245  	hasKey := false
   246  	var attributes ldvalue.ValueMapBuilder
   247  	in.Delim('{')
   248  	for !in.IsDelim('}') {
   249  		// See comment about UnsafeBytes in unmarshalSingleKindEasyJSON.
   250  		key := in.UnsafeBytes()
   251  		in.WantColon()
   252  		switch string(key) {
   253  		case ldattr.KeyAttr:
   254  			c.key = in.String()
   255  			hasKey = true
   256  		case ldattr.NameAttr:
   257  			c.name = readOptStringEasyJSON(in)
   258  		case jsonPropOldUserSecondary:
   259  			c.secondary = readOptStringEasyJSON(in)
   260  		case ldattr.AnonymousAttr:
   261  			if in.IsNull() {
   262  				in.Skip()
   263  				c.anonymous = false
   264  			} else {
   265  				c.anonymous = in.Bool()
   266  			}
   267  		case jsonPropOldUserCustom:
   268  			if in.IsNull() {
   269  				in.Skip()
   270  				attributes = ldvalue.ValueMapBuilder{}
   271  				break
   272  			}
   273  			in.Delim('{')
   274  			for !in.IsDelim('}') {
   275  				name := in.String()
   276  				in.WantColon()
   277  				if in.IsNull() {
   278  					in.Skip()
   279  				} else {
   280  					var value ldvalue.Value
   281  					value.UnmarshalEasyJSON(in)
   282  					if isOldUserCustomAttributeNameAllowed(name) {
   283  						attributes.Set(name, value)
   284  					}
   285  				}
   286  				in.WantComma()
   287  			}
   288  			in.Delim('}')
   289  		case jsonPropOldUserPrivate:
   290  			if usingEventFormat {
   291  				in.SkipRecursive()
   292  				break
   293  			}
   294  			readPrivateAttributesEasyJSON(in, c, true)
   295  			// The "true" here means to interpret the strings as literal attribute names, since the
   296  			// attribute reference path syntax was not used in the old user schema.
   297  		case jsonPropOldUserRedacted:
   298  			if !usingEventFormat {
   299  				in.SkipRecursive()
   300  				break
   301  			}
   302  			readPrivateAttributesEasyJSON(in, c, true)
   303  		case "firstName", "lastName", "email", "country", "avatar", "ip":
   304  			if in.IsNull() {
   305  				in.Skip()
   306  			} else {
   307  				value := ldvalue.String(in.String())
   308  				attributes.Set(internAttributeNameIfPossible(key), value)
   309  			}
   310  		default:
   311  			// In the old user schema, unrecognized top-level property names are ignored. Calling SkipRecursive
   312  			// makes the Lexer consume and discard the property value so we can advance to the next object property.
   313  			in.SkipRecursive()
   314  		}
   315  		in.WantComma()
   316  	}
   317  	in.Delim('}')
   318  	if in.Error() != nil {
   319  		return
   320  	}
   321  	if !hasKey {
   322  		in.AddError(lderrors.ErrContextKeyMissing{})
   323  		return
   324  	}
   325  	c.fullyQualifiedKey = c.key
   326  	c.attributes = attributes.Build()
   327  }
   328  
   329  func parseKindOnlyEasyJSON(originalLexer *jlexer.Lexer) (Kind, bool, error) {
   330  	// Make an exact copy of the original lexer so that changes in its state will not
   331  	// affect the original lexer; both point to the same []byte array, but each has its
   332  	// own "current position" and "next token" fields.
   333  	in := *originalLexer
   334  	in.Delim('{')
   335  	for !in.IsDelim('}') {
   336  		key := in.UnsafeFieldName(false)
   337  		in.WantColon()
   338  		if key == ldattr.KindAttr {
   339  			kind := in.String()
   340  			if in.Error() == nil && kind == "" {
   341  				return "", false, lderrors.ErrContextKindEmpty{}
   342  			}
   343  			return Kind(kind), true, in.Error()
   344  		}
   345  		in.SkipRecursive()
   346  		in.WantComma()
   347  	}
   348  	in.Delim('}')
   349  	return "", false, in.Error()
   350  }
   351  
   352  func readOptStringEasyJSON(in *jlexer.Lexer) ldvalue.OptionalString {
   353  	if in.IsNull() {
   354  		in.Skip()
   355  		return ldvalue.OptionalString{}
   356  	} else {
   357  		return ldvalue.NewOptionalString(in.String())
   358  	}
   359  }
   360  
   361  func readPrivateAttributesEasyJSON(in *jlexer.Lexer, c *Context, asLiterals bool) {
   362  	c.privateAttrs = nil
   363  	if in.IsNull() {
   364  		in.SkipRecursive()
   365  		return
   366  	}
   367  	in.Delim('[')
   368  	for !in.IsDelim(']') {
   369  		if c.privateAttrs == nil {
   370  			c.privateAttrs = make([]ldattr.Ref, 0, initialAttrListAllocSize)
   371  		}
   372  		c.privateAttrs = append(c.privateAttrs, refOrLiteralRef(in.String(), asLiterals))
   373  		in.WantComma()
   374  	}
   375  	in.Delim(']')
   376  }
   377  

View as plain text