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::from_literal_name(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 std::collections::HashSet;
871 use test_case::test_case;
872
873 use super::{ContextAttributes, Kind};
874
875 proptest! {
876 #[test]
877 fn all_generated_kinds_are_valid(kind in any_kind()) {
878 let maybe_kind = Kind::try_from(kind.as_ref());
879 assert!(maybe_kind.is_ok());
880 }
881 }
882
883 #[test_case("kind"; "Cannot set kind as kind")]
884 #[test_case("multi"; "Cannot set kind as multi")]
885 #[test_case("🦀"; "Cannot set kind as invalid character")]
886 #[test_case(" "; "Cannot set kind as only whitespace")]
887 fn invalid_kinds(kind: &str) {
888 assert!(Kind::try_from(kind).is_err());
889 }
890
891 #[test_case(Kind::user(), true)]
892 #[test_case(Kind::from("user"), true)]
893 #[test_case(Kind::multi(), false)]
894 #[test_case(Kind::from("foo"), false)]
895 fn is_user(kind: Kind, is_user: bool) {
896 assert_eq!(kind.is_user(), is_user);
897 }
898
899 #[test_case(Kind::multi(), true)]
900 #[test_case(Kind::from("multi"), true)]
901 #[test_case(Kind::user(), false)]
902 #[test_case(Kind::from("foo"), false)]
903 fn is_multi(kind: Kind, is_multi: bool) {
904 assert_eq!(kind.is_multi(), is_multi);
905 }
906
907 #[test]
908 fn kind_sorts_based_on_string() {
909 let mut kinds = vec![
910 Kind::user(),
911 Kind::multi(),
912 Kind::from("n"),
913 Kind::from("v"),
914 Kind::from("l"),
915 ];
916 kinds.sort();
917 assert_eq!(
918 kinds,
919 vec![
920 Kind::from("l"),
921 Kind::multi(),
922 Kind::from("n"),
923 Kind::user(),
924 Kind::from("v"),
925 ]
926 );
927 }
928
929 proptest! {
930 #[test]
931 fn kind_comparison_identity(kind in any_kind()) {
932 assert_eq!(kind, kind);
933 }
934 }
935
936 proptest! {
937 #[test]
938 fn kind_comparison_identity_str(kind in any_kind()) {
939 assert_eq!(kind, kind.as_ref());
940 assert_eq!(&kind, kind.as_ref());
941 }
942 }
943
944 proptest! {
945 #[test]
946 fn kind_comparison_different(a in any_kind(), b in any_kind()) {
947 if a.0 != b.0 {
948 assert_ne!(a, b);
949 }
950 }
951 }
952
953 proptest! {
954 #[test]
955 fn kind_serialize(kind in any_kind()) {
956 assert_eq!(format!("\"{}\"", kind.0), serde_json::to_string(&kind).unwrap());
957 }
958 }
959
960 proptest! {
961 #[test]
962 fn kind_deserialize(kind_str in any_kind_string()) {
963 let json_str = format!("\"{}\"", &kind_str);
964 let kind: Result<Kind, _> = serde_json::from_str(&json_str);
965 assert!(kind.is_ok());
966 }
967 }
968
969 #[test]
972 fn cannot_deserialize_multi_kind() {
973 let maybe_kind: Result<Kind, _> = serde_json::from_str("\"multi\"");
974 assert!(maybe_kind.is_err());
975 }
976
977 #[test_case("kind", Some(AttributeValue::String("org".to_string())))]
979 #[test_case("key", Some(AttributeValue::String("my-key".to_string())))]
980 #[test_case("name", Some(AttributeValue::String("my-name".to_string())))]
981 #[test_case("anonymous", Some(AttributeValue::Bool(true)))]
982 #[test_case("attr", Some(AttributeValue::String("my-attr".to_string())))]
983 #[test_case("/starts-with-slash", Some(AttributeValue::String("love that prefix".to_string())))]
984 #[test_case("/crazy~0name", Some(AttributeValue::String("still works".to_string())))]
985 #[test_case("/other", None)]
986 #[test_case("/", None; "Single slash")]
988 #[test_case("", None; "Empty reference")]
989 #[test_case("/a//b", None; "Double slash")]
990 #[test_case("privateAttributes", None)]
992 #[test_case("secondary", None)]
993 #[test_case("/my-map/array", Some(AttributeValue::Array(vec![AttributeValue::String("first".to_string()), AttributeValue::String("second".to_string())])))]
995 #[test_case("/my-map/1", Some(AttributeValue::Bool(true)))]
996 #[test_case("/my-map/missing", None)]
997 #[test_case("/starts-with-slash/1", None; "handles providing an index to a non-array value")]
998 fn context_can_get_value(input: &str, value: Option<AttributeValue>) {
999 let mut builder = ContextBuilder::new("my-key");
1000
1001 let array = vec![
1002 AttributeValue::String("first".to_string()),
1003 AttributeValue::String("second".to_string()),
1004 ];
1005 let map = hashmap! {
1006 "array".to_string() => AttributeValue::Array(array),
1007 "1".to_string() => AttributeValue::Bool(true)
1008 };
1009
1010 let context = builder
1011 .kind("org".to_string())
1012 .name("my-name")
1013 .anonymous(true)
1014 .secondary("my-secondary")
1015 .set_string("attr", "my-attr")
1016 .set_string("starts-with-slash", "love that prefix")
1017 .set_string("crazy~name", "still works")
1018 .set_value("my-map", AttributeValue::Object(map))
1019 .add_private_attribute("attr")
1020 .build()
1021 .expect("Failed to build context");
1022
1023 assert_eq!(context.get_value(&Reference::new(input)), value);
1024 }
1025
1026 #[test_case("kind", Some(AttributeValue::String("multi".to_string())))]
1027 #[test_case("key", None)]
1028 #[test_case("name", None)]
1029 #[test_case("anonymous", None)]
1030 #[test_case("attr", None)]
1031 fn multi_context_get_value(input: &str, value: Option<AttributeValue>) {
1032 let mut multi_builder = MultiContextBuilder::new();
1033 let mut builder = ContextBuilder::new("user");
1034
1035 multi_builder.add_context(builder.build().expect("Failed to create context"));
1036
1037 builder
1038 .key("org")
1039 .kind("org".to_string())
1040 .name("my-name")
1041 .anonymous(true)
1042 .set_string("attr", "my-attr");
1043 multi_builder.add_context(builder.build().expect("Failed to create context"));
1044
1045 let context = multi_builder.build().expect("Failed to create context");
1046
1047 assert_eq!(context.get_value(&Reference::new(input)), value);
1048 }
1049
1050 #[test]
1051 fn can_retrieve_context_from_multi_context() {
1052 let user_context = ContextBuilder::new("user").build().unwrap();
1053 let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
1054
1055 assert!(org_context.as_kind(&Kind::user()).is_none());
1056
1057 let multi_context = MultiContextBuilder::new()
1058 .add_context(user_context)
1059 .add_context(org_context)
1060 .build()
1061 .unwrap();
1062
1063 assert!(multi_context
1064 .as_kind(&Kind::user())
1065 .unwrap()
1066 .kind()
1067 .is_user());
1068
1069 assert_eq!(
1070 "org",
1071 multi_context
1072 .as_kind(&Kind::from("org"))
1073 .unwrap()
1074 .kind()
1075 .as_ref()
1076 );
1077
1078 assert!(multi_context.as_kind(&Kind::from("custom")).is_none());
1079 }
1080
1081 #[test]
1082 fn redacting_anon_from_anon_is_invalid() {
1083 let anon_context = ContextBuilder::new("user")
1084 .anonymous(true)
1085 .build()
1086 .expect("context build failed");
1087 let result = anon_context.without_anonymous_contexts();
1088
1089 assert!(result.is_err());
1090 }
1091
1092 #[test]
1093 fn redacting_anon_from_nonanon_results_in_no_change() {
1094 let context = ContextBuilder::new("user")
1095 .build()
1096 .expect("context build failed");
1097 let result = context.without_anonymous_contexts();
1098
1099 assert_eq!(context, result.unwrap());
1100 }
1101
1102 #[test]
1103 fn can_redact_anon_from_multi() {
1104 let user_context = ContextBuilder::new("user")
1105 .anonymous(true)
1106 .build()
1107 .expect("Failed to create context");
1108 let org_context = ContextBuilder::new("org")
1109 .kind("org")
1110 .build()
1111 .expect("Failed to create context");
1112
1113 let multi_context = MultiContextBuilder::new()
1114 .add_context(user_context)
1115 .add_context(org_context)
1116 .build()
1117 .expect("Failed to create context");
1118
1119 let context = multi_context
1120 .without_anonymous_contexts()
1121 .expect("Failed to redact anon");
1122
1123 assert_eq!("org", context.kind().as_ref());
1124 }
1125
1126 #[test]
1127 fn redact_anon_from_all_anon_multi_is_invalid() {
1128 let user_context = ContextBuilder::new("user")
1129 .anonymous(true)
1130 .build()
1131 .expect("Failed to create context");
1132 let org_context = ContextBuilder::new("org")
1133 .kind("org")
1134 .anonymous(true)
1135 .build()
1136 .expect("Failed to create context");
1137
1138 let multi_context = MultiContextBuilder::new()
1139 .add_context(user_context)
1140 .add_context(org_context)
1141 .build()
1142 .expect("Failed to create context");
1143
1144 let context = multi_context.without_anonymous_contexts();
1145
1146 assert!(context.is_err());
1147 }
1148
1149 fn redacted_attributes(json: &serde_json::Value) -> HashSet<String> {
1152 json["_meta"]["redactedAttributes"]
1153 .as_array()
1154 .expect("redactedAttributes should be an array")
1155 .iter()
1156 .map(|v| v.as_str().unwrap().to_string())
1157 .collect()
1158 }
1159
1160 #[test_case("/ssn", "/~1ssn"; "slash prefix is escaped")]
1161 #[test_case("/a~b", "/~1a~0b"; "slash prefix with tilde escapes both")]
1162 fn all_attributes_private_reports_escaped_references(name: &str, expected_ref: &str) {
1163 let context = ContextBuilder::new("my-key")
1164 .set_string(name, "secret")
1165 .build()
1166 .expect("Failed to build context");
1167
1168 let attributes = ContextAttributes::from_context(context, true, HashSet::new());
1169 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1170
1171 assert_eq!(
1172 redacted_attributes(&json),
1173 HashSet::from([expected_ref.to_string()])
1174 );
1175 assert!(json.get(name).is_none());
1177 }
1178
1179 #[test]
1180 fn anonymous_redaction_reports_slash_prefixed_names_as_escaped_references() {
1181 let context = ContextBuilder::new("my-key")
1182 .anonymous(true)
1183 .set_string("/ssn", "123-45-6789")
1184 .build()
1185 .expect("Failed to build context");
1186
1187 let attributes = ContextAttributes::from_context_with_anonymous_redaction(
1188 context,
1189 false,
1190 HashSet::new(),
1191 );
1192 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1193
1194 assert_eq!(
1195 redacted_attributes(&json),
1196 HashSet::from(["/~1ssn".to_string()])
1197 );
1198 assert!(json.get("/ssn").is_none());
1199 }
1200
1201 #[test]
1202 fn slash_prefixed_attribute_is_serialized_under_its_literal_name_when_not_private() {
1203 let context = ContextBuilder::new("my-key")
1204 .set_string("/ssn", "123-45-6789")
1205 .build()
1206 .expect("Failed to build context");
1207
1208 let attributes = ContextAttributes::from_context(context, false, HashSet::new());
1209 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1210
1211 assert_eq!(json["/ssn"], serde_json::json!("123-45-6789"));
1212 assert!(json.get("_meta").is_none());
1213 }
1214}