package ldmodel import ( "github.com/launchdarkly/go-sdk-common/v3/ldattr" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldtime" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-jsonstream/v3/jreader" ) func unmarshalFeatureFlagFromBytes(data []byte) (FeatureFlag, error) { r := jreader.NewReader(data) parsed := unmarshalFeatureFlagFromReader(&r) if err := r.Error(); err != nil { return FeatureFlag{}, jreader.ToJSONError(err, &parsed) } return parsed, nil } func unmarshalFeatureFlagFromReader(r *jreader.Reader) FeatureFlag { var parsed FeatureFlag readFeatureFlag(r, &parsed) if r.Error() == nil { PreprocessFlag(&parsed) } return parsed } func unmarshalSegmentFromBytes(data []byte) (Segment, error) { r := jreader.NewReader(data) parsed := unmarshalSegmentFromReader(&r) if err := r.Error(); err != nil { return Segment{}, jreader.ToJSONError(err, &parsed) } return parsed, nil } func unmarshalSegmentFromReader(r *jreader.Reader) Segment { var parsed Segment readSegment(r, &parsed) if r.Error() == nil { PreprocessSegment(&parsed) } return parsed } func readFeatureFlag(r *jreader.Reader, flag *FeatureFlag) { deprecatedClientSide := false for obj := r.Object(); obj.Next(); { name := obj.Name() switch string(name) { case "key": flag.Key = r.String() case "on": flag.On = r.Bool() case "prerequisites": readPrerequisites(r, &flag.Prerequisites) case "targets": readTargets(r, &flag.Targets) case "contextTargets": readTargets(r, &flag.ContextTargets) case "rules": readFlagRules(r, &flag.Rules) case "fallthrough": readVariationOrRollout(r, &flag.Fallthrough) case "offVariation": flag.OffVariation.ReadFromJSONReader(r) case "variations": readValueList(r, &flag.Variations) case "clientSideAvailability": readClientSideAvailability(r, &flag.ClientSideAvailability) case "clientSide": deprecatedClientSide = r.Bool() case "salt": flag.Salt = r.String() case "trackEvents": flag.TrackEvents = r.Bool() case "trackEventsFallthrough": flag.TrackEventsFallthrough = r.Bool() case "debugEventsUntilDate": val, _ := r.Float64OrNull() // val will be zero if null flag.DebugEventsUntilDate = ldtime.UnixMillisecondTime(val) case "version": flag.Version = r.Int() case "deleted": flag.Deleted = r.Bool() } } if !flag.ClientSideAvailability.Explicit { flag.ClientSideAvailability = ClientSideAvailability{ UsingMobileKey: true, // always assumed to be true in the old schema UsingEnvironmentID: deprecatedClientSide, Explicit: false, } } } func readPrerequisites(r *jreader.Reader, out *[]Prerequisite) { for arr := r.ArrayOrNull(); arr.Next(); { var prereq Prerequisite for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "key": prereq.Key = r.String() case "variation": prereq.Variation = r.Int() } } *out = append(*out, prereq) } } func readTargets(r *jreader.Reader, out *[]Target) { for arr := r.ArrayOrNull(); arr.Next(); { var t Target for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "contextKind": t.ContextKind = ldcontext.Kind(r.String()) case "values": readStringList(r, &t.Values) case "variation": t.Variation = r.Int() } } *out = append(*out, t) } } func readFlagRules(r *jreader.Reader, out *[]FlagRule) { for arr := r.ArrayOrNull(); arr.Next(); { rule := FlagRule{} for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "id": rule.ID = r.String() case "variation": rule.Variation.ReadFromJSONReader(r) case "rollout": readRollout(r, &rule.Rollout) case "clauses": readClauses(r, &rule.Clauses) case "trackEvents": rule.TrackEvents = r.Bool() } } *out = append(*out, rule) } } func readClauses(r *jreader.Reader, out *[]Clause) { for arr := r.ArrayOrNull(); arr.Next(); { var clause Clause var attrStr string for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "contextKind": clause.ContextKind = ldcontext.Kind(r.String()) case "attribute": attrStr, _ = r.StringOrNull() case "op": clause.Op = Operator(r.String()) case "values": readValueList(r, &clause.Values) case "negate": clause.Negate = r.Bool() } } setAttrNameOrRef(attrStr, clause.ContextKind, &clause.Attribute) *out = append(*out, clause) } } func readVariationOrRollout(r *jreader.Reader, out *VariationOrRollout) { for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "variation": out.Variation.ReadFromJSONReader(r) case "rollout": readRollout(r, &out.Rollout) } } } func readRollout(r *jreader.Reader, out *Rollout) { obj := r.ObjectOrNull() if !obj.IsDefined() { *out = Rollout{} return } var bucketByStr string for obj.Next() { switch string(obj.Name()) { case "kind": out.Kind = RolloutKind(r.String()) case "contextKind": out.ContextKind = ldcontext.Kind(r.String()) case "variations": for arr := r.Array(); arr.Next(); { var wv WeightedVariation for wrObj := r.Object(); wrObj.Next(); { switch string(wrObj.Name()) { case "variation": wv.Variation = r.Int() case "weight": wv.Weight = r.Int() case "untracked": wv.Untracked = r.Bool() } } out.Variations = append(out.Variations, wv) } case "bucketBy": bucketByStr, _ = r.StringOrNull() case "seed": if n, ok := r.IntOrNull(); ok { out.Seed = ldvalue.NewOptionalInt(n) } } } setAttrNameOrRef(bucketByStr, out.ContextKind, &out.BucketBy) } func readClientSideAvailability(r *jreader.Reader, out *ClientSideAvailability) { obj := r.ObjectOrNull() out.Explicit = obj.IsDefined() for obj.Next() { switch string(obj.Name()) { case "usingEnvironmentId": out.UsingEnvironmentID = r.Bool() case "usingMobileKey": out.UsingMobileKey = r.Bool() } } } func readSegment(r *jreader.Reader, segment *Segment) { for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "key": segment.Key = r.String() case "version": segment.Version = r.Int() case "generation": segment.Generation.ReadFromJSONReader(r) case "deleted": segment.Deleted = r.Bool() case "included": readStringList(r, &segment.Included) case "excluded": readStringList(r, &segment.Excluded) case "includedContexts": readSegmentTargets(r, &segment.IncludedContexts) case "excludedContexts": readSegmentTargets(r, &segment.ExcludedContexts) case "rules": for rulesArr := r.ArrayOrNull(); rulesArr.Next(); { rule := SegmentRule{} var bucketByStr string for ruleObj := r.Object(); ruleObj.Next(); { switch string(ruleObj.Name()) { case "id": rule.ID = r.String() case "clauses": readClauses(r, &rule.Clauses) case "weight": if v, ok := r.IntOrNull(); ok { rule.Weight = ldvalue.NewOptionalInt(v) } case "bucketBy": bucketByStr, _ = r.StringOrNull() case "rolloutContextKind": rule.RolloutContextKind = ldcontext.Kind(r.String()) } } setAttrNameOrRef(bucketByStr, rule.RolloutContextKind, &rule.BucketBy) segment.Rules = append(segment.Rules, rule) } case "salt": segment.Salt = r.String() case "unbounded": segment.Unbounded = r.Bool() case "unboundedContextKind": segment.UnboundedContextKind = ldcontext.Kind(r.String()) } } } func readSegmentTargets(r *jreader.Reader, out *[]SegmentTarget) { for arr := r.ArrayOrNull(); arr.Next(); { var t SegmentTarget for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case "contextKind": t.ContextKind = ldcontext.Kind(r.String()) case "values": readStringList(r, &t.Values) } } *out = append(*out, t) } } func readStringList(r *jreader.Reader, out *[]string) { for arr := r.ArrayOrNull(); arr.Next(); { *out = append(*out, r.String()) } } func readValueList(r *jreader.Reader, out *[]ldvalue.Value) { for arr := r.ArrayOrNull(); arr.Next(); { var v ldvalue.Value v.ReadFromJSONReader(r) *out = append(*out, v) } } func setAttrNameOrRef(value string, contextKind ldcontext.Kind, out *ldattr.Ref) { switch { case value == "": *out = ldattr.Ref{} // Note that we're not distinguishing here between an empty string and an omitted property; // "" would not be valid parameter to NewRef, and historically been a value LD may send for // these fields so we are treating either "" or null as "undefined". case contextKind == "": // If the context kind was not specified in this clause/rollout/etc., then this is old-style // data and we must interpret the attribute property as a plain attribute name, not an attribute // reference (in other words, a leading slash would be just part of the name). *out = ldattr.NewLiteralRef(value) default: *out = ldattr.NewRef(value) } }