...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel/model_marshal.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel

     1  package ldmodel
     2  
     3  import (
     4  	"github.com/launchdarkly/go-jsonstream/v3/jwriter"
     5  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
     7  )
     8  
     9  // For backward compatibility, we are only allowed to drop out properties that have default values if
    10  // Go SDK 4.x would also have done so (since some SDKs are not tolerant of missing properties in
    11  // general). This is true of all properties that have OptionalInt-like semantics (having either a
    12  // numeric value or "undefined"), and properties that could be either a JSON object or null (like
    13  // VariationOrRollout.Rollout), and the BucketBy property which has optional-string-like behavior.
    14  // Array properties should not be dropped even if nil.
    15  //
    16  // Properties that did not exist prior to Go SDK v5 are always safe to drop if they have default
    17  // values, since older SDKs will never look for them. These are:
    18  // - FeatureFlag.ClientSideAvailability
    19  // - Segment.Unbounded
    20  
    21  func marshalFeatureFlag(flag FeatureFlag) ([]byte, error) {
    22  	w := jwriter.NewWriter()
    23  	marshalFeatureFlagToWriter(flag, &w)
    24  	return w.Bytes(), w.Error()
    25  }
    26  
    27  func marshalFeatureFlagToWriter(flag FeatureFlag, w *jwriter.Writer) {
    28  	obj := w.Object()
    29  
    30  	obj.Name("key").String(flag.Key)
    31  
    32  	obj.Name("on").Bool(flag.On)
    33  
    34  	prereqsArr := obj.Name("prerequisites").Array()
    35  	for _, p := range flag.Prerequisites {
    36  		prereqObj := prereqsArr.Object()
    37  		prereqObj.Name("key").String(p.Key)
    38  		prereqObj.Name("variation").Int(p.Variation)
    39  		prereqObj.End()
    40  	}
    41  	prereqsArr.End()
    42  
    43  	writeTargets(&obj, flag.Targets, "targets")
    44  	writeTargets(&obj, flag.ContextTargets, "contextTargets")
    45  
    46  	rulesArr := obj.Name("rules").Array()
    47  	for _, r := range flag.Rules {
    48  		ruleObj := rulesArr.Object()
    49  		writeVariationOrRolloutProperties(&ruleObj, r.VariationOrRollout)
    50  		ruleObj.Maybe("id", r.ID != "").String(r.ID)
    51  		writeClauses(w, &ruleObj, r.Clauses)
    52  		ruleObj.Name("trackEvents").Bool(r.TrackEvents)
    53  		ruleObj.End()
    54  	}
    55  	rulesArr.End()
    56  
    57  	fallthroughObj := obj.Name("fallthrough").Object()
    58  	writeVariationOrRolloutProperties(&fallthroughObj, flag.Fallthrough)
    59  	fallthroughObj.End()
    60  
    61  	flag.OffVariation.WriteToJSONWriter(obj.Name("offVariation"))
    62  
    63  	variationsArr := obj.Name("variations").Array()
    64  	for _, v := range flag.Variations {
    65  		v.WriteToJSONWriter(w)
    66  	}
    67  	variationsArr.End()
    68  
    69  	// In the older JSON schema, ClientSideAvailability.UsingEnvironmentID was in "clientSide", and
    70  	// ClientSideAvailability.UsingMobileKey was assumed to be true. In the newer schema, those are
    71  	// both in a "clientSideAvailability" object.
    72  	//
    73  	// If ClientSideAvailability.Explicit is true, then this flag used the newer schema and should be
    74  	// reserialized the same way. If it is false, we will reserialize with the old schema, which
    75  	// does not include UsingMobileKey; note that in that case UsingMobileKey is assumed to be true.
    76  	//
    77  	// For backward compatibility with older SDKs that might be reading a flag that was serialized by
    78  	// this SDK, we always include the older "clientSide" property if it would be true.
    79  	if flag.ClientSideAvailability.Explicit {
    80  		csaObj := obj.Name("clientSideAvailability").Object()
    81  		csaObj.Name("usingMobileKey").Bool(flag.ClientSideAvailability.UsingMobileKey)
    82  		csaObj.Name("usingEnvironmentId").Bool(flag.ClientSideAvailability.UsingEnvironmentID)
    83  		csaObj.End()
    84  	}
    85  	obj.Name("clientSide").Bool(flag.ClientSideAvailability.UsingEnvironmentID)
    86  
    87  	obj.Name("salt").String(flag.Salt)
    88  
    89  	obj.Name("trackEvents").Bool(flag.TrackEvents)
    90  	obj.Name("trackEventsFallthrough").Bool(flag.TrackEventsFallthrough)
    91  
    92  	obj.Name("debugEventsUntilDate").Float64OrNull(flag.DebugEventsUntilDate != 0, float64(flag.DebugEventsUntilDate))
    93  
    94  	obj.Name("version").Int(flag.Version)
    95  
    96  	obj.Name("deleted").Bool(flag.Deleted)
    97  
    98  	obj.End()
    99  }
   100  
   101  func writeTargets(obj *jwriter.ObjectState, targets []Target, name string) {
   102  	targetsArr := obj.Name(name).Array()
   103  	for _, t := range targets {
   104  		targetObj := targetsArr.Object()
   105  		if t.ContextKind != "" {
   106  			targetObj.Name("contextKind").String(string(t.ContextKind))
   107  		}
   108  		targetObj.Name("variation").Int(t.Variation)
   109  		writeStringArray(&targetObj, "values", t.Values)
   110  		targetObj.End()
   111  	}
   112  	targetsArr.End()
   113  }
   114  
   115  func marshalSegment(segment Segment) ([]byte, error) {
   116  	w := jwriter.NewWriter()
   117  	marshalSegmentToWriter(segment, &w)
   118  	return w.Bytes(), w.Error()
   119  }
   120  
   121  func marshalSegmentToWriter(segment Segment, w *jwriter.Writer) {
   122  	obj := w.Object()
   123  
   124  	obj.Name("key").String(segment.Key)
   125  	writeStringArray(&obj, "included", segment.Included)
   126  	writeStringArray(&obj, "excluded", segment.Excluded)
   127  	writeSegmentTargets(&obj, segment.IncludedContexts, "includedContexts")
   128  	writeSegmentTargets(&obj, segment.ExcludedContexts, "excludedContexts")
   129  	obj.Name("salt").String(segment.Salt)
   130  
   131  	rulesArr := obj.Name("rules").Array()
   132  	for _, r := range segment.Rules {
   133  		ruleObj := rulesArr.Object()
   134  		ruleObj.Name("id").String(r.ID)
   135  		writeClauses(w, &ruleObj, r.Clauses)
   136  		ruleObj.Maybe("weight", r.Weight.IsDefined()).Int(r.Weight.IntValue())
   137  		writeAttrRef(ruleObj.Maybe("bucketBy", r.BucketBy.IsDefined()), &r.BucketBy, r.RolloutContextKind)
   138  		ruleObj.Maybe("rolloutContextKind", r.RolloutContextKind != "").String(string(r.RolloutContextKind))
   139  		ruleObj.End()
   140  	}
   141  	rulesArr.End()
   142  
   143  	obj.Maybe("unbounded", segment.Unbounded).Bool(segment.Unbounded)
   144  	obj.Maybe("unboundedContextKind", segment.UnboundedContextKind != "").String(string(segment.UnboundedContextKind))
   145  
   146  	obj.Name("version").Int(segment.Version)
   147  	segment.Generation.WriteToJSONWriter(obj.Name("generation"))
   148  	obj.Name("deleted").Bool(segment.Deleted)
   149  
   150  	obj.End()
   151  }
   152  
   153  func writeSegmentTargets(obj *jwriter.ObjectState, targets []SegmentTarget, name string) {
   154  	targetsArr := obj.Name(name).Array()
   155  	for _, t := range targets {
   156  		targetObj := targetsArr.Object()
   157  		if t.ContextKind != "" {
   158  			targetObj.Name("contextKind").String(string(t.ContextKind))
   159  		}
   160  		writeStringArray(&targetObj, "values", t.Values)
   161  		targetObj.End()
   162  	}
   163  	targetsArr.End()
   164  }
   165  
   166  func writeStringArray(obj *jwriter.ObjectState, name string, values []string) {
   167  	arr := obj.Name(name).Array()
   168  	for _, v := range values {
   169  		arr.String(v)
   170  	}
   171  	arr.End()
   172  }
   173  
   174  func writeVariationOrRolloutProperties(obj *jwriter.ObjectState, vr VariationOrRollout) {
   175  	obj.Maybe("variation", vr.Variation.IsDefined()).Int(vr.Variation.IntValue())
   176  	if len(vr.Rollout.Variations) > 0 {
   177  		rolloutObj := obj.Name("rollout").Object()
   178  		rolloutObj.Maybe("kind", vr.Rollout.Kind != "").String(string(vr.Rollout.Kind))
   179  		rolloutObj.Maybe("contextKind", vr.Rollout.ContextKind != "").String(string(vr.Rollout.ContextKind))
   180  		variationsArr := rolloutObj.Name("variations").Array()
   181  		for _, wv := range vr.Rollout.Variations {
   182  			variationObj := variationsArr.Object()
   183  			variationObj.Name("variation").Int(wv.Variation)
   184  			variationObj.Name("weight").Int(wv.Weight)
   185  			variationObj.Maybe("untracked", wv.Untracked).Bool(wv.Untracked)
   186  			variationObj.End()
   187  		}
   188  		variationsArr.End()
   189  		rolloutObj.Maybe("seed", vr.Rollout.Seed.IsDefined()).Int(vr.Rollout.Seed.IntValue())
   190  		writeAttrRef(rolloutObj.Maybe("bucketBy", vr.Rollout.BucketBy.IsDefined()),
   191  			&vr.Rollout.BucketBy, vr.Rollout.ContextKind)
   192  		rolloutObj.End()
   193  	}
   194  }
   195  
   196  func writeClauses(w *jwriter.Writer, obj *jwriter.ObjectState, clauses []Clause) {
   197  	clausesArr := obj.Name("clauses").Array()
   198  	for _, c := range clauses {
   199  		clauseObj := clausesArr.Object()
   200  		if c.ContextKind != "" {
   201  			clauseObj.Name("contextKind").String(string(c.ContextKind))
   202  		}
   203  
   204  		clauseObj.Name("attribute")
   205  		if !c.Attribute.IsDefined() {
   206  			// See comments in unmarshaling logic - for consistency with LD service behavior, we serialize
   207  			// this as an empty string even if it was undefined
   208  			w.String("")
   209  		} else {
   210  			writeAttrRef(w, &c.Attribute, c.ContextKind)
   211  		}
   212  
   213  		clauseObj.Name("op").String(string(c.Op))
   214  		valuesArr := clauseObj.Name("values").Array()
   215  		for _, v := range c.Values {
   216  			v.WriteToJSONWriter(w)
   217  		}
   218  		valuesArr.End()
   219  		clauseObj.Name("negate").Bool(c.Negate)
   220  		clauseObj.End()
   221  	}
   222  	clausesArr.End()
   223  }
   224  
   225  func writeAttrRef(w *jwriter.Writer, ref *ldattr.Ref, contextKind ldcontext.Kind) {
   226  	if contextKind == "" {
   227  		// If there was no context kind, then we received this as old-style data in which the attribute is
   228  		// interpreted as a plain name rather than an attribute reference. However, ref.String() will always
   229  		// return an attribute reference which could contain escape characters. We should instead get the
   230  		// unescaped attribute name, which AttrRef represents as the first element in a single-element path.
   231  		w.String(ref.Component(0))
   232  	} else {
   233  		w.String(ref.String())
   234  	}
   235  }
   236  

View as plain text