launchdarkly_server_sdk_evaluation/contexts/
context.rs

1use super::attribute_reference::Reference;
2use crate::contexts::context_serde::ContextVariant;
3use crate::{AttributeValue, MultiContextBuilder};
4use itertools::Itertools;
5use log::warn;
6use maplit::hashmap;
7use serde::de::Error;
8use serde::ser::SerializeMap;
9use serde::{ser, Deserialize, Serialize};
10use sha1::{Digest, Sha1};
11use std::borrow::{Cow, ToOwned};
12use std::cmp::Ordering;
13use std::collections::{HashMap, HashSet};
14use std::convert::{TryFrom, TryInto};
15use std::fmt;
16use std::fmt::Formatter;
17use std::string::ToString;
18
19const BUCKET_SCALE_INT: i64 = 0x0FFF_FFFF_FFFF_FFFF;
20const BUCKET_SCALE: f32 = BUCKET_SCALE_INT as f32;
21
22/// Kind describes the type of entity represented by a [Context].
23/// The meaning of a kind is entirely up to the application. To construct a custom kind other than
24/// ["user"](Kind::user), see [Kind::try_from].
25#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26pub struct Kind(Cow<'static, str>);
27
28impl Kind {
29    /// Returns true if the kind is "user". Users are the default context kind created by [crate::ContextBuilder].
30    pub fn is_user(&self) -> bool {
31        self == "user"
32    }
33
34    /// Returns true if the kind is "multi". Multi-contexts are created by [crate::MultiContextBuilder].
35    pub fn is_multi(&self) -> bool {
36        self == "multi"
37    }
38
39    /// Constructs a kind of type "user". See also [Kind::try_from] to create a custom kind, which may
40    /// then be passed to [crate::ContextBuilder::kind].
41    pub fn user() -> Self {
42        Self(Cow::Borrowed("user"))
43    }
44
45    pub(crate) fn multi() -> Self {
46        Self(Cow::Borrowed("multi"))
47    }
48
49    #[cfg(test)]
50    // Constructs a Kind from an arbitrary string, which may result in a Kind that
51    // violates the normal requirements.
52    pub(crate) fn from(s: &str) -> Self {
53        Kind(Cow::Owned(s.to_owned()))
54    }
55}
56
57impl AsRef<str> for Kind {
58    /// Returns a reference to the kind's underlying string.
59    fn as_ref(&self) -> &str {
60        &self.0
61    }
62}
63
64impl Ord for Kind {
65    fn cmp(&self, other: &Self) -> Ordering {
66        self.as_ref().cmp(other.as_ref())
67    }
68}
69
70impl PartialOrd for Kind {
71    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
72        Some(self.cmp(other))
73    }
74}
75
76impl PartialEq<&str> for Kind {
77    fn eq(&self, other: &&str) -> bool {
78        self.as_ref() == *other
79    }
80}
81
82impl PartialEq<str> for Kind {
83    fn eq(&self, other: &str) -> bool {
84        self.as_ref() == other
85    }
86}
87
88impl Default for Kind {
89    /// Kind defaults to "user".
90    fn default() -> Self {
91        Kind::user()
92    }
93}
94
95impl TryFrom<String> for Kind {
96    type Error = String;
97
98    /// Fallibly constructs a kind from an owned string.
99    /// To be a valid kind, the value cannot be empty or equal to "kind".
100    /// Additionally, it must be composed entirely of ASCII alphanumeric characters, in
101    /// addition to `-`, `.` and `_`.
102    fn try_from(value: String) -> Result<Self, Self::Error> {
103        match value.as_str() {
104            "" => Err(String::from("context kind cannot be empty")),
105            "kind" => Err(String::from("context kind cannot be 'kind'")),
106            "multi" => Err(String::from("context kind cannot be 'multi'")),
107            "user" => Ok(Kind::user()),
108            k if !k
109                .chars()
110                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_')) =>
111            {
112                Err(String::from("context kind contains disallowed characters"))
113            }
114            _ => Ok(Kind(Cow::Owned(value))),
115        }
116    }
117}
118
119impl TryFrom<&str> for Kind {
120    type Error = String;
121
122    /// Fallibly constructs a kind from a string reference.
123    /// See [Kind::try_from].
124    fn try_from(value: &str) -> Result<Self, Self::Error> {
125        match value {
126            "user" => Ok(Kind::user()),
127            _ => Self::try_from(value.to_owned()),
128        }
129    }
130}
131
132impl fmt::Display for Kind {
133    /// Displays the string representation of a kind.
134    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", self.as_ref())
136    }
137}
138
139impl From<Kind> for String {
140    /// Converts a kind into its string representation.
141    fn from(k: Kind) -> Self {
142        k.0.into_owned()
143    }
144}
145
146impl Serialize for Kind {
147    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148    where
149        S: serde::Serializer,
150    {
151        serializer.serialize_str(self.as_ref())
152    }
153}
154
155impl<'de> Deserialize<'de> for Kind {
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: serde::Deserializer<'de>,
159    {
160        let s = String::deserialize(deserializer)?;
161        let kind = s.as_str().try_into().map_err(Error::custom)?;
162        Ok(kind)
163    }
164}
165
166#[cfg(test)]
167pub(crate) mod proptest_generators {
168    use super::Kind;
169    use proptest::prelude::*;
170
171    prop_compose! {
172        pub(crate) fn any_kind_string()(
173            s in "[-._a-zA-Z0-9]+".prop_filter("must not be 'kind' or 'multi'", |s| s != "kind" && s != "multi")
174        ) -> String {
175            s
176        }
177    }
178
179    prop_compose! {
180        pub(crate) fn any_kind()(s in any_kind_string()) -> Kind {
181            Kind::from(s.as_str())
182        }
183    }
184}
185
186/// Context is a collection of attributes that can be referenced in flag evaluations and analytics
187/// events. These attributes are described by one or more [Kind]s.
188///
189/// For example, a context might represent the user of a service, the description of an organization,
190/// IoT device metadata, or any combination of those at once.
191///
192/// To create a context of a single kind, such as a user, you may use [crate::ContextBuilder].
193///
194/// To create a context with multiple kinds, use [crate::MultiContextBuilder].
195#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
196#[serde(try_from = "ContextVariant", into = "ContextVariant")]
197pub struct Context {
198    // Kind is required. For multi-contexts, Kind will always be Kind::Multi.
199    pub(super) kind: Kind,
200    // Contexts is only present for a multi-context.
201    pub(super) contexts: Option<Vec<Context>>,
202    // Name may be optionally set.
203    pub(super) name: Option<String>,
204    // Anonymous may be optionally set, but is false by default.
205    pub(super) anonymous: bool,
206    // Private attributes may be optionally set.
207    pub(super) private_attributes: Option<Vec<Reference>>,
208    // All Contexts have a canonical key, which is a way of uniquely representing all the
209    // (kind, key) pairs in a Context.
210    pub(super) canonical_key: String,
211    // Attributes that aren't builtins may be optionally set.
212    pub(super) attributes: HashMap<String, AttributeValue>,
213    // Secondary serves as an additional key for bucketing purposes.
214    // It has been deprecated by the u2c specification, and can only be set by deserializing
215    // pre-Context data. Its presence is necessary for backwards-compatibility.
216    pub(super) secondary: Option<String>,
217    // Single contexts have keys, which serve as identifiers. For a multi-context,
218    // key is an empty string.
219    pub(super) key: String,
220}
221
222impl Context {
223    /// Returns true if the context is a multi-context.
224    pub fn is_multi(&self) -> bool {
225        self.kind.is_multi()
226    }
227
228    /// For a multi-kind context:
229    ///
230    /// A multi-kind context is made up of two or more single-kind contexts. This method will first
231    /// discard any single-kind contexts which are anonymous. It will then create a new multi-kind
232    /// context from the remaining single-kind contexts. This may result in an invalid context
233    /// (e.g. all single-kind contexts are anonymous).
234    ///
235    /// For a single-kind context:
236    ///
237    /// If the context is not anonymous, this method will return the current context as is and
238    /// unmodified.
239    ///
240    /// If the context is anonymous, this method will return an `Err(String)` result.
241    pub fn without_anonymous_contexts(&self) -> Result<Context, String> {
242        let contexts = match &self.contexts {
243            Some(contexts) => contexts.clone(),
244            None => vec![self.clone()],
245        };
246        let contexts = contexts.into_iter().filter(|c| !c.anonymous).collect_vec();
247
248        MultiContextBuilder::of(contexts).build()
249    }
250
251    /// Looks up the value of any attribute of the context, or a value contained within an
252    /// attribute, based on the given reference.
253    ///
254    /// This lookup includes only attributes that are addressable in evaluations-- not metadata
255    /// such as private attributes.
256    ///
257    /// This method implements the same behavior that the SDK uses to resolve attribute references during a flag
258    /// evaluation. In a single context, the reference can represent a simple attribute name-- either a
259    /// built-in one like "name" or "key", or a custom attribute that was set by methods like
260    /// [crate::ContextBuilder::set_string]-- or, it can be a slash-delimited path.
261    ///
262    /// For a multi-context, the only supported attribute name is "kind". Use
263    /// [Context::as_kind] to inspect a context for a particular [Kind] and then get its
264    /// attributes.
265    pub fn get_value(&self, reference: &Reference) -> Option<AttributeValue> {
266        if !reference.is_valid() {
267            return None;
268        }
269
270        let first_path_component = reference.component(0)?;
271
272        if self.is_multi() {
273            if reference.depth() == 1 && first_path_component == "kind" {
274                return Some(AttributeValue::String(self.kind.to_string()));
275            }
276
277            warn!("Multi-contexts only support retrieving the 'kind' attribute");
278            return None;
279        }
280
281        let mut attribute =
282            self.get_top_level_addressable_attribute_single_kind(first_path_component)?;
283
284        for i in 1..reference.depth() {
285            let name = reference.component(i)?;
286            if let AttributeValue::Object(map) = attribute {
287                attribute = map.get(name).cloned()?;
288            } else {
289                return None;
290            }
291        }
292
293        Some(attribute)
294    }
295
296    /// Returns the "key" attribute.
297    ///
298    /// For a single context, this value is set by the [crate::ContextBuilder::new] or
299    /// [crate::ContextBuilder::key] methods.
300    ///
301    /// For a multi-context, there is no single key, so [Context::key] returns an empty string; use
302    /// [Context::as_kind] to inspect a context for a particular kind and call [Context::key] on it.
303    pub fn key(&self) -> &str {
304        &self.key
305    }
306
307    /// Returns the canonical key.
308    ///
309    /// 1. For a single context of kind "user", the canonical key is equivalent to the key.
310    /// 2. For other kinds of single contexts, the canonical key is "kind:key".
311    /// 3. For a multi-context, the canonical key is the concatenation of its constituent contexts'
312    ///    canonical keys with `:` according to (2) (including kind "user").
313    pub fn canonical_key(&self) -> &str {
314        &self.canonical_key
315    }
316
317    /// Returns the "kind" attribute.
318    pub fn kind(&self) -> &Kind {
319        &self.kind
320    }
321
322    /// If the specified kind exists within the context, returns a reference to it.
323    /// Otherwise, returns None.
324    pub fn as_kind(&self, kind: &Kind) -> Option<&Context> {
325        match &self.contexts {
326            Some(contexts) => contexts.iter().find(|c| c.kind() == kind),
327            None => self.kind.eq(kind).then_some(self),
328        }
329    }
330
331    /// Returns a map of all (kind, key) pairs contained in this context.
332    pub fn context_keys(&self) -> HashMap<&Kind, &str> {
333        match &self.contexts {
334            Some(contexts) => contexts
335                .iter()
336                .map(|context| (context.kind(), context.key()))
337                .collect(),
338            None => hashmap! { self.kind() => self.key() },
339        }
340    }
341
342    /// Returns a list of all kinds represented by this context.
343    pub fn kinds(&self) -> Vec<&Kind> {
344        if !self.is_multi() {
345            return vec![self.kind()];
346        }
347
348        match &self.contexts {
349            Some(contexts) => contexts.iter().map(|context| context.kind()).collect(),
350            None => Vec::new(),
351        }
352    }
353
354    fn get_optional_attribute_names(&self) -> Vec<String> {
355        if self.is_multi() {
356            return Vec::new();
357        }
358
359        let mut names = Vec::with_capacity(self.attributes.len() + 1);
360        names.extend(self.attributes.keys().cloned());
361
362        if self.name.is_some() {
363            names.push(String::from("name"));
364        }
365
366        names
367    }
368
369    pub(crate) fn bucket(
370        &self,
371        by_attr: &Option<Reference>,
372        prefix: BucketPrefix,
373        is_experiment: bool,
374        context_kind: &Kind,
375    ) -> Result<(f32, bool), String> {
376        let reference = match (is_experiment, by_attr) {
377            (true, _) | (false, None) => Reference::new("key"),
378            (false, Some(reference)) => reference.clone(),
379        };
380
381        if !reference.is_valid() {
382            return Err(reference.error());
383        }
384
385        match self.as_kind(context_kind) {
386            Some(context) => {
387                let attr_value = context.get_value(&reference);
388
389                Ok((
390                    self._bucket(attr_value.as_ref(), prefix, is_experiment)
391                        .unwrap_or(0.0),
392                    false,
393                ))
394            }
395            // If the required context wasn't found, we still want the bucket to be 0, but we need
396            // to show that the context was missing. This will affect the inExperiment field
397            // upstream.
398            _ => Ok((0.0, true)),
399        }
400    }
401
402    fn _bucket(
403        &self,
404        value: Option<&AttributeValue>,
405        prefix: BucketPrefix,
406        is_experiment: bool,
407    ) -> Option<f32> {
408        let mut id = value?.as_bucketable()?;
409
410        if cfg!(feature = "secondary_key_bucketing") && !is_experiment {
411            if let Some(secondary) = &self.secondary {
412                id.push('.');
413                id.push_str(secondary);
414            }
415        }
416
417        let mut hash = Sha1::new();
418        prefix.write_hash(&mut hash);
419        hash.update(b".");
420        hash.update(id.as_bytes());
421
422        let digest = hash.finalize();
423        let hexhash = base16ct::lower::encode_string(&digest);
424
425        let hexhash_15 = &hexhash[..15]; // yes, 15 chars, not 16
426        let numhash = i64::from_str_radix(hexhash_15, 16).unwrap();
427
428        Some(numhash as f32 / BUCKET_SCALE)
429    }
430
431    fn get_top_level_addressable_attribute_single_kind(
432        &self,
433        name: &str,
434    ) -> Option<AttributeValue> {
435        match name {
436            "kind" => Some(AttributeValue::String(self.kind.to_string())),
437            "key" => Some(AttributeValue::String(self.key.clone())),
438            "name" => self.name.clone().map(AttributeValue::String),
439            "anonymous" => Some(AttributeValue::Bool(self.anonymous)),
440            _ => self.attributes.get(name).map(|v| v.to_owned()),
441        }
442    }
443}
444
445#[derive(Clone, Copy)]
446pub(crate) enum BucketPrefix<'a> {
447    KeyAndSalt(&'a str, &'a str),
448    Seed(i64),
449}
450
451impl BucketPrefix<'_> {
452    pub(crate) fn write_hash(&self, hash: &mut Sha1) {
453        match self {
454            BucketPrefix::KeyAndSalt(key, salt) => {
455                hash.update(key.as_bytes());
456                hash.update(b".");
457                hash.update(salt.as_bytes());
458            }
459            BucketPrefix::Seed(seed) => {
460                let seed_str = seed.to_string();
461                hash.update(seed_str.as_bytes());
462            }
463        }
464    }
465}
466
467#[derive(Debug)]
468struct PrivateAttributeLookupNode {
469    reference: Option<Reference>,
470    children: HashMap<String, Box<PrivateAttributeLookupNode>>,
471}
472
473/// ContextAttributes is used to handle redaction of select context properties when serializing a
474/// [Context] that will be sent to LaunchDarkly.
475#[derive(Debug)]
476pub struct ContextAttributes {
477    context: Context,
478    all_attributes_private: bool,
479    global_private_attributes: HashMap<String, Box<PrivateAttributeLookupNode>>,
480    redact_anonymous: bool,
481}
482
483impl ContextAttributes {
484    /// Construct from a source context, indicating if all attributes should be private,
485    /// and providing a set of attribute references that should be selectively marked private.
486    pub fn from_context(
487        context: Context,
488        all_attributes_private: bool,
489        private_attributes: HashSet<Reference>,
490    ) -> Self {
491        Self {
492            context,
493            all_attributes_private,
494            global_private_attributes: Self::make_private_attribute_lookup_data(private_attributes),
495            redact_anonymous: false,
496        }
497    }
498
499    /// Construct from a source context, indicating if all attributes should be private,
500    /// and providing a set of attribute references that should be selectively marked private.
501    ///
502    /// If a provided context is anonymous, all attributes will be redacted except for key, kind,
503    /// and anonymous.
504    pub fn from_context_with_anonymous_redaction(
505        context: Context,
506        all_attributes_private: bool,
507        private_attributes: HashSet<Reference>,
508    ) -> Self {
509        Self {
510            context,
511            all_attributes_private,
512            global_private_attributes: Self::make_private_attribute_lookup_data(private_attributes),
513            redact_anonymous: true,
514        }
515    }
516
517    // This function transforms a set of [Reference]s into a data structure that allows for more
518    // efficient check_global_private_attribute_refs.
519    //
520    // For instance, if the original references were "/name", "/address/street", and
521    // "/address/city", it would produce the following map:
522    //
523    // "name": {
524    //   attribute: Reference::new("/name"),
525    // },
526    // "address": {
527    //   children: {
528    //     "street": {
529    //       attribute: Reference::new("/address/street/"),
530    //     },
531    //     "city": {
532    //       attribute: Reference::new("/address/city/"),
533    //     },
534    //   },
535    // }
536    fn make_private_attribute_lookup_data(
537        references: HashSet<Reference>,
538    ) -> HashMap<String, Box<PrivateAttributeLookupNode>> {
539        let mut return_value = HashMap::new();
540
541        for reference in references.into_iter() {
542            let mut parent_map = &mut return_value;
543            for i in 0..reference.depth() {
544                if let Some(name) = reference.component(i) {
545                    if !parent_map.contains_key(name) {
546                        let mut next_node = Box::new(PrivateAttributeLookupNode {
547                            reference: None,
548                            children: HashMap::new(),
549                        });
550
551                        if i == reference.depth() - 1 {
552                            next_node.reference = Some(reference.clone());
553                        }
554
555                        parent_map.insert(name.to_owned(), next_node);
556                    }
557
558                    parent_map = &mut parent_map.get_mut(name).unwrap().children;
559                }
560            }
561        }
562
563        return_value
564    }
565
566    fn write_multi_context(&self) -> HashMap<String, AttributeValue> {
567        let mut map: HashMap<String, AttributeValue> = HashMap::new();
568        map.insert("kind".to_string(), self.context.kind().to_string().into());
569
570        if let Some(contexts) = &self.context.contexts {
571            for context in contexts.iter() {
572                let context_map = self.write_single_context(context, false);
573                map.insert(
574                    context.kind().to_string(),
575                    AttributeValue::Object(context_map),
576                );
577            }
578        }
579
580        map
581    }
582
583    fn write_single_context(
584        &self,
585        context: &Context,
586        include_kind: bool,
587    ) -> HashMap<String, AttributeValue> {
588        let mut map: HashMap<String, AttributeValue> = HashMap::new();
589
590        if include_kind {
591            map.insert("kind".into(), context.kind().to_string().into());
592        }
593
594        map.insert(
595            "key".to_string(),
596            AttributeValue::String(context.key().to_owned()),
597        );
598
599        let optional_attribute_names = context.get_optional_attribute_names();
600        let mut redacted_attributes = Vec::<String>::with_capacity(20);
601
602        let redact_all =
603            self.all_attributes_private || (self.redact_anonymous && context.anonymous);
604
605        for key in optional_attribute_names.iter() {
606            let reference = Reference::new(key);
607            if let Some(value) = context.get_value(&reference) {
608                // If redact_all is true, then there's no complex filtering or
609                // recursing to be done: all of these values are by definition private, so just add
610                // their names to the redacted list.
611                if redact_all {
612                    redacted_attributes.push(String::from(reference));
613                    continue;
614                }
615
616                let path = Vec::with_capacity(10);
617                self.write_filter_attribute(
618                    context,
619                    &mut map,
620                    path,
621                    key,
622                    value,
623                    &mut redacted_attributes,
624                )
625            }
626        }
627
628        if context.anonymous {
629            map.insert("anonymous".to_string(), true.into());
630        }
631
632        if context.secondary.is_some() || !redacted_attributes.is_empty() {
633            let mut meta: HashMap<String, AttributeValue> = HashMap::new();
634            if let Some(secondary) = &context.secondary {
635                meta.insert(
636                    "secondary".to_string(),
637                    AttributeValue::String(secondary.to_string()),
638                );
639            }
640
641            if !redacted_attributes.is_empty() {
642                meta.insert(
643                    "redactedAttributes".to_string(),
644                    AttributeValue::Array(
645                        redacted_attributes
646                            .into_iter()
647                            .map(AttributeValue::String)
648                            .collect(),
649                    ),
650                );
651            }
652
653            map.insert("_meta".to_string(), AttributeValue::Object(meta));
654        }
655
656        map
657    }
658
659    // Checks whether a given value should be considered private, and then either writes the
660    // attribute to the output HashMap if it is *not* private, or adds the corresponding attribute
661    // reference to the redacted_attributes list if it is private.
662    //
663    // The parent_path parameter indicates where we are in the context data structure. If it is
664    // empty, we are at the top level and "key" is an attribute name. If it is not empty, we are
665    // recursing into the properties of an attribute value that is a JSON object: for instance, if
666    // parent_path is ["billing", "address"] and key is "street", then the top-level attribute is
667    // "billing" and has a value in the form {"address": {"street": ...}} and we are now deciding
668    // whether to write the "street" property. See maybe_redact for the logic involved in that
669    // decision.
670    //
671    // If all_attributes_private is true, this method is never called.
672    fn write_filter_attribute(
673        &self,
674        context: &Context,
675        map: &mut HashMap<String, AttributeValue>,
676        parent_path: Vec<String>,
677        key: &str,
678        value: AttributeValue,
679        redacted_attributes: &mut Vec<String>,
680    ) {
681        let mut path = parent_path;
682        path.push(key.to_string());
683
684        let (is_redacted, nested_properties_are_redacted) =
685            self.maybe_redact(context, &path, &value, redacted_attributes);
686
687        // If the value is an object, then there are three possible outcomes:
688        //
689        // 1. this value is completely redacted, so drop it and do not recurse;
690        // 2. the value is not redacted, and and neither are any subproperties within it, so output
691        //    the whole thing as-is;
692        // 3. the value itself is not redacted, but some subproperties within it are, so we'll need
693        //    to recurse through it and filter as we go.
694        match value {
695            AttributeValue::Object(_) if is_redacted => (), // outcome 1
696            AttributeValue::Object(ref object_map) => {
697                // outcome 2
698                if !nested_properties_are_redacted {
699                    map.insert(key.to_string(), value.clone());
700                    return;
701                }
702
703                // outcome 3
704                let mut sub_map = HashMap::new();
705                for (k, v) in object_map.iter() {
706                    self.write_filter_attribute(
707                        context,
708                        &mut sub_map,
709                        path.clone(),
710                        k,
711                        v.clone(),
712                        redacted_attributes,
713                    );
714                }
715                map.insert(key.to_string(), AttributeValue::Object(sub_map));
716            }
717            _ if !is_redacted => {
718                map.insert(key.to_string(), value);
719            }
720            _ => (),
721        };
722    }
723
724    // Called by write_filter_attribute to decide whether or not a given value (or, possibly,
725    // properties within it) should be considered private, based on the private attribute
726    // references.
727    //
728    // If the value should be private, then the first return value is true, and also the attribute
729    // reference is added to redacted_attributes.
730    //
731    // The second return value indicates whether there are any private attribute references
732    // designating properties *within* this value. That is, if parent_path is ["address"], and the
733    // configuration says that "/address/street" is private, then the second return value will be
734    // true, which tells us that we can't just dump the value of the "address" object directly into
735    // the output but will need to filter its properties.
736    //
737    // Note that even though a Reference can contain numeric path components to represent an array
738    // element lookup, for the purposes of flag evaluations (like "/animals/0" which conceptually
739    // represents context.animals[0]), those will not work as private attribute references since
740    // we do not recurse to redact anything within an array value. A reference like "/animals/0"
741    // would only work if context.animals were an object with a property named "0".
742    //
743    // If all_attributes_private is true, this method is never called.
744    fn maybe_redact(
745        &self,
746        context: &Context,
747        parent_path: &[String],
748        value: &AttributeValue,
749        redacted_attributes: &mut Vec<String>,
750    ) -> (bool, bool) {
751        let (redacted_attr_ref, mut nested_properties_are_redacted) =
752            self.check_global_private_attribute_refs(parent_path);
753
754        if let Some(redacted_attr_ref) = redacted_attr_ref {
755            redacted_attributes.push(String::from(redacted_attr_ref));
756            return (true, false);
757        }
758
759        let should_check_for_nested_properties = matches!(value, AttributeValue::Object(..));
760
761        if let Some(private_attributes) = &context.private_attributes {
762            for private_attribute in private_attributes.iter() {
763                let depth = private_attribute.depth();
764                if depth < parent_path.len() {
765                    // If the attribute reference is shorter than the current path, then it can't
766                    // possibly be a match, because if it had matched the first part of our path,
767                    // we wouldn't have recursed this far.
768                    continue;
769                }
770
771                if !should_check_for_nested_properties && depth > parent_path.len() {
772                    continue;
773                }
774
775                let mut has_match = true;
776                for (i, parent_part) in parent_path.iter().enumerate() {
777                    match private_attribute.component(i) {
778                        None => break,
779                        Some(name) if name != parent_part => {
780                            has_match = false;
781                            break;
782                        }
783                        _ => continue,
784                    };
785                }
786
787                if has_match {
788                    if depth == parent_path.len() {
789                        redacted_attributes.push(private_attribute.to_string());
790                        return (true, false);
791                    }
792                    nested_properties_are_redacted = true;
793                }
794            }
795        }
796
797        (false, nested_properties_are_redacted)
798    }
799
800    // Checks whether the given attribute or subproperty matches any Reference that was designated
801    // as private in the SDK options.
802    //
803    // If parent_path has just one element, it is the name of a top-level attribute. If it has
804    // multiple elements, it is a path to a property within a custom object attribute: for
805    // instance, if you represented the overall context as a JSON object, the parent_path
806    // ["billing", "address", "street"] would refer to the street property within something like
807    // {"billing": {"address": {"street": "x"}}}.
808    //
809    // The first return value is None if the attribute does not need to be redacted; otherwise it
810    // is the specific attribute reference that was matched.
811    //
812    // The second return value is true if and only if there's at least one configured private
813    // attribute reference for *children* of parent_path (and there is not one for parent_path
814    // itself, since if there was, we would not bother recursing to write the children). See
815    // comments on write_filter_attribute.
816    fn check_global_private_attribute_refs(
817        &self,
818        parent_path: &[String],
819    ) -> (Option<Reference>, bool) {
820        let mut lookup = &self.global_private_attributes;
821
822        if self.global_private_attributes.is_empty() {
823            return (None, false);
824        }
825
826        for (index, path) in parent_path.iter().enumerate() {
827            let next_node = match lookup.get(path.as_str()) {
828                None => break,
829                Some(v) => v,
830            };
831
832            if index == parent_path.len() - 1 {
833                let var_name = (next_node.reference.clone(), next_node.reference.is_none());
834                return var_name;
835            } else if !next_node.children.is_empty() {
836                lookup = &next_node.children;
837            }
838        }
839
840        (None, false)
841    }
842}
843
844impl ser::Serialize for ContextAttributes {
845    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
846    where
847        S: ser::Serializer,
848    {
849        let mut serialize_map = serializer.serialize_map(None)?;
850
851        let map = match self.context.is_multi() {
852            true => self.write_multi_context(),
853            false => self.write_single_context(&self.context, true),
854        };
855
856        for (k, v) in map.iter().sorted_by_key(|p| p.0) {
857            serialize_map.serialize_entry(k, v)?;
858        }
859
860        serialize_map.end()
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::proptest_generators::*;
867    use crate::{AttributeValue, ContextBuilder, MultiContextBuilder, Reference};
868    use maplit::hashmap;
869    use proptest::proptest;
870    use test_case::test_case;
871
872    use super::Kind;
873
874    proptest! {
875        #[test]
876        fn all_generated_kinds_are_valid(kind in any_kind()) {
877            let maybe_kind = Kind::try_from(kind.as_ref());
878            assert!(maybe_kind.is_ok());
879        }
880    }
881
882    #[test_case("kind"; "Cannot set kind as kind")]
883    #[test_case("multi"; "Cannot set kind as multi")]
884    #[test_case("🦀"; "Cannot set kind as invalid character")]
885    #[test_case(" "; "Cannot set kind as only whitespace")]
886    fn invalid_kinds(kind: &str) {
887        assert!(Kind::try_from(kind).is_err());
888    }
889
890    #[test_case(Kind::user(), true)]
891    #[test_case(Kind::from("user"), true)]
892    #[test_case(Kind::multi(), false)]
893    #[test_case(Kind::from("foo"), false)]
894    fn is_user(kind: Kind, is_user: bool) {
895        assert_eq!(kind.is_user(), is_user);
896    }
897
898    #[test_case(Kind::multi(), true)]
899    #[test_case(Kind::from("multi"), true)]
900    #[test_case(Kind::user(), false)]
901    #[test_case(Kind::from("foo"), false)]
902    fn is_multi(kind: Kind, is_multi: bool) {
903        assert_eq!(kind.is_multi(), is_multi);
904    }
905
906    #[test]
907    fn kind_sorts_based_on_string() {
908        let mut kinds = vec![
909            Kind::user(),
910            Kind::multi(),
911            Kind::from("n"),
912            Kind::from("v"),
913            Kind::from("l"),
914        ];
915        kinds.sort();
916        assert_eq!(
917            kinds,
918            vec![
919                Kind::from("l"),
920                Kind::multi(),
921                Kind::from("n"),
922                Kind::user(),
923                Kind::from("v"),
924            ]
925        );
926    }
927
928    proptest! {
929        #[test]
930        fn kind_comparison_identity(kind in any_kind()) {
931            assert_eq!(kind, kind);
932        }
933    }
934
935    proptest! {
936        #[test]
937        fn kind_comparison_identity_str(kind in any_kind()) {
938            assert_eq!(kind, kind.as_ref());
939            assert_eq!(&kind, kind.as_ref());
940        }
941    }
942
943    proptest! {
944        #[test]
945        fn kind_comparison_different(a in any_kind(), b in any_kind()) {
946            if a.0 != b.0 {
947                assert_ne!(a, b);
948            }
949        }
950    }
951
952    proptest! {
953        #[test]
954        fn kind_serialize(kind in any_kind()) {
955            assert_eq!(format!("\"{}\"", kind.0), serde_json::to_string(&kind).unwrap());
956        }
957    }
958
959    proptest! {
960        #[test]
961        fn kind_deserialize(kind_str in any_kind_string()) {
962            let json_str = format!("\"{}\"", &kind_str);
963            let kind: Result<Kind, _> = serde_json::from_str(&json_str);
964            assert!(kind.is_ok());
965        }
966    }
967
968    // Since "multi" is reserved as the signifier for multi-contexts,
969    // it cannot be constructed directly.
970    #[test]
971    fn cannot_deserialize_multi_kind() {
972        let maybe_kind: Result<Kind, _> = serde_json::from_str("\"multi\"");
973        assert!(maybe_kind.is_err());
974    }
975
976    // Basic simple attribute retrievals
977    #[test_case("kind", Some(AttributeValue::String("org".to_string())))]
978    #[test_case("key", Some(AttributeValue::String("my-key".to_string())))]
979    #[test_case("name", Some(AttributeValue::String("my-name".to_string())))]
980    #[test_case("anonymous", Some(AttributeValue::Bool(true)))]
981    #[test_case("attr", Some(AttributeValue::String("my-attr".to_string())))]
982    #[test_case("/starts-with-slash", Some(AttributeValue::String("love that prefix".to_string())))]
983    #[test_case("/crazy~0name", Some(AttributeValue::String("still works".to_string())))]
984    #[test_case("/other", None)]
985    // Invalid reference retrieval
986    #[test_case("/", None; "Single slash")]
987    #[test_case("", None; "Empty reference")]
988    #[test_case("/a//b", None; "Double slash")]
989    // Hidden meta attributes
990    #[test_case("privateAttributes", None)]
991    #[test_case("secondary", None)]
992    // Can index objects
993    #[test_case("/my-map/array", Some(AttributeValue::Array(vec![AttributeValue::String("first".to_string()), AttributeValue::String("second".to_string())])))]
994    #[test_case("/my-map/1", Some(AttributeValue::Bool(true)))]
995    #[test_case("/my-map/missing", None)]
996    #[test_case("/starts-with-slash/1", None; "handles providing an index to a non-array value")]
997    fn context_can_get_value(input: &str, value: Option<AttributeValue>) {
998        let mut builder = ContextBuilder::new("my-key");
999
1000        let array = vec![
1001            AttributeValue::String("first".to_string()),
1002            AttributeValue::String("second".to_string()),
1003        ];
1004        let map = hashmap! {
1005            "array".to_string() => AttributeValue::Array(array),
1006            "1".to_string() => AttributeValue::Bool(true)
1007        };
1008
1009        let context = builder
1010            .kind("org".to_string())
1011            .name("my-name")
1012            .anonymous(true)
1013            .secondary("my-secondary")
1014            .set_string("attr", "my-attr")
1015            .set_string("starts-with-slash", "love that prefix")
1016            .set_string("crazy~name", "still works")
1017            .set_value("my-map", AttributeValue::Object(map))
1018            .add_private_attribute("attr")
1019            .build()
1020            .expect("Failed to build context");
1021
1022        assert_eq!(context.get_value(&Reference::new(input)), value);
1023    }
1024
1025    #[test_case("kind", Some(AttributeValue::String("multi".to_string())))]
1026    #[test_case("key", None)]
1027    #[test_case("name", None)]
1028    #[test_case("anonymous", None)]
1029    #[test_case("attr", None)]
1030    fn multi_context_get_value(input: &str, value: Option<AttributeValue>) {
1031        let mut multi_builder = MultiContextBuilder::new();
1032        let mut builder = ContextBuilder::new("user");
1033
1034        multi_builder.add_context(builder.build().expect("Failed to create context"));
1035
1036        builder
1037            .key("org")
1038            .kind("org".to_string())
1039            .name("my-name")
1040            .anonymous(true)
1041            .set_string("attr", "my-attr");
1042        multi_builder.add_context(builder.build().expect("Failed to create context"));
1043
1044        let context = multi_builder.build().expect("Failed to create context");
1045
1046        assert_eq!(context.get_value(&Reference::new(input)), value);
1047    }
1048
1049    #[test]
1050    fn can_retrieve_context_from_multi_context() {
1051        let user_context = ContextBuilder::new("user").build().unwrap();
1052        let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
1053
1054        assert!(org_context.as_kind(&Kind::user()).is_none());
1055
1056        let multi_context = MultiContextBuilder::new()
1057            .add_context(user_context)
1058            .add_context(org_context)
1059            .build()
1060            .unwrap();
1061
1062        assert!(multi_context
1063            .as_kind(&Kind::user())
1064            .unwrap()
1065            .kind()
1066            .is_user());
1067
1068        assert_eq!(
1069            "org",
1070            multi_context
1071                .as_kind(&Kind::from("org"))
1072                .unwrap()
1073                .kind()
1074                .as_ref()
1075        );
1076
1077        assert!(multi_context.as_kind(&Kind::from("custom")).is_none());
1078    }
1079
1080    #[test]
1081    fn redacting_anon_from_anon_is_invalid() {
1082        let anon_context = ContextBuilder::new("user")
1083            .anonymous(true)
1084            .build()
1085            .expect("context build failed");
1086        let result = anon_context.without_anonymous_contexts();
1087
1088        assert!(result.is_err());
1089    }
1090
1091    #[test]
1092    fn redacting_anon_from_nonanon_results_in_no_change() {
1093        let context = ContextBuilder::new("user")
1094            .build()
1095            .expect("context build failed");
1096        let result = context.without_anonymous_contexts();
1097
1098        assert_eq!(context, result.unwrap());
1099    }
1100
1101    #[test]
1102    fn can_redact_anon_from_multi() {
1103        let user_context = ContextBuilder::new("user")
1104            .anonymous(true)
1105            .build()
1106            .expect("Failed to create context");
1107        let org_context = ContextBuilder::new("org")
1108            .kind("org")
1109            .build()
1110            .expect("Failed to create context");
1111
1112        let multi_context = MultiContextBuilder::new()
1113            .add_context(user_context)
1114            .add_context(org_context)
1115            .build()
1116            .expect("Failed to create context");
1117
1118        let context = multi_context
1119            .without_anonymous_contexts()
1120            .expect("Failed to redact anon");
1121
1122        assert_eq!("org", context.kind().as_ref());
1123    }
1124
1125    #[test]
1126    fn redact_anon_from_all_anon_multi_is_invalid() {
1127        let user_context = ContextBuilder::new("user")
1128            .anonymous(true)
1129            .build()
1130            .expect("Failed to create context");
1131        let org_context = ContextBuilder::new("org")
1132            .kind("org")
1133            .anonymous(true)
1134            .build()
1135            .expect("Failed to create context");
1136
1137        let multi_context = MultiContextBuilder::new()
1138            .add_context(user_context)
1139            .add_context(org_context)
1140            .build()
1141            .expect("Failed to create context");
1142
1143        let context = multi_context.without_anonymous_contexts();
1144
1145        assert!(context.is_err());
1146    }
1147}