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#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26pub struct Kind(Cow<'static, str>);
27
28impl Kind {
29 pub fn is_user(&self) -> bool {
31 self == "user"
32 }
33
34 pub fn is_multi(&self) -> bool {
36 self == "multi"
37 }
38
39 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 pub(crate) fn from(s: &str) -> Self {
53 Kind(Cow::Owned(s.to_owned()))
54 }
55}
56
57impl AsRef<str> for Kind {
58 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 fn default() -> Self {
91 Kind::user()
92 }
93}
94
95impl TryFrom<String> for Kind {
96 type Error = String;
97
98 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 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 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135 write!(f, "{}", self.as_ref())
136 }
137}
138
139impl From<Kind> for String {
140 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
196#[serde(try_from = "ContextVariant", into = "ContextVariant")]
197pub struct Context {
198 pub(super) kind: Kind,
200 pub(super) contexts: Option<Vec<Context>>,
202 pub(super) name: Option<String>,
204 pub(super) anonymous: bool,
206 pub(super) private_attributes: Option<Vec<Reference>>,
208 pub(super) canonical_key: String,
211 pub(super) attributes: HashMap<String, AttributeValue>,
213 pub(super) secondary: Option<String>,
217 pub(super) key: String,
220}
221
222impl Context {
223 pub fn is_multi(&self) -> bool {
225 self.kind.is_multi()
226 }
227
228 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 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 pub fn key(&self) -> &str {
304 &self.key
305 }
306
307 pub fn canonical_key(&self) -> &str {
314 &self.canonical_key
315 }
316
317 pub fn kind(&self) -> &Kind {
319 &self.kind
320 }
321
322 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 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 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 _ => 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]; 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#[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 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 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 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 {
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 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 match value {
695 AttributeValue::Object(_) if is_redacted => (), AttributeValue::Object(ref object_map) => {
697 if !nested_properties_are_redacted {
699 map.insert(key.to_string(), value.clone());
700 return;
701 }
702
703 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 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 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 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 #[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 #[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 #[test_case("/", None; "Single slash")]
987 #[test_case("", None; "Empty reference")]
988 #[test_case("/a//b", None; "Double slash")]
989 #[test_case("privateAttributes", None)]
991 #[test_case("secondary", None)]
992 #[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}