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(
542 references: impl IntoIterator<Item = Reference>,
543 ) -> HashMap<String, Box<PrivateAttributeLookupNode>> {
544 let mut return_value = HashMap::new();
545
546 for reference in references.into_iter() {
547 let mut parent_map = &mut return_value;
548 for i in 0..reference.depth() {
549 if let Some(name) = reference.component(i) {
550 let node = parent_map.entry(name.to_owned()).or_insert_with(|| {
551 Box::new(PrivateAttributeLookupNode {
552 reference: None,
553 children: HashMap::new(),
554 })
555 });
556
557 if i == reference.depth() - 1 {
558 node.reference = Some(reference.clone());
559 }
560
561 parent_map = &mut node.children;
562 }
563 }
564 }
565
566 return_value
567 }
568
569 fn write_multi_context(&self) -> HashMap<String, AttributeValue> {
570 let mut map: HashMap<String, AttributeValue> = HashMap::new();
571 map.insert("kind".to_string(), self.context.kind().to_string().into());
572
573 if let Some(contexts) = &self.context.contexts {
574 for context in contexts.iter() {
575 let context_map = self.write_single_context(context, false);
576 map.insert(
577 context.kind().to_string(),
578 AttributeValue::Object(context_map),
579 );
580 }
581 }
582
583 map
584 }
585
586 fn write_single_context(
587 &self,
588 context: &Context,
589 include_kind: bool,
590 ) -> HashMap<String, AttributeValue> {
591 let mut map: HashMap<String, AttributeValue> = HashMap::new();
592
593 if include_kind {
594 map.insert("kind".into(), context.kind().to_string().into());
595 }
596
597 map.insert(
598 "key".to_string(),
599 AttributeValue::String(context.key().to_owned()),
600 );
601
602 let optional_attribute_names = context.get_optional_attribute_names();
603 let mut redacted_attributes = Vec::<String>::with_capacity(20);
604
605 let redact_all =
606 self.all_attributes_private || (self.redact_anonymous && context.anonymous);
607
608 for key in optional_attribute_names.iter() {
609 let reference = Reference::from_literal_name(key);
610 if let Some(value) = context.get_value(&reference) {
611 if redact_all {
615 redacted_attributes.push(String::from(reference));
616 continue;
617 }
618
619 let path = Vec::with_capacity(10);
620 self.write_filter_attribute(
621 context,
622 &mut map,
623 path,
624 key,
625 value,
626 &mut redacted_attributes,
627 )
628 }
629 }
630
631 if context.anonymous {
632 map.insert("anonymous".to_string(), true.into());
633 }
634
635 if context.secondary.is_some() || !redacted_attributes.is_empty() {
636 let mut meta: HashMap<String, AttributeValue> = HashMap::new();
637 if let Some(secondary) = &context.secondary {
638 meta.insert(
639 "secondary".to_string(),
640 AttributeValue::String(secondary.to_string()),
641 );
642 }
643
644 if !redacted_attributes.is_empty() {
645 meta.insert(
646 "redactedAttributes".to_string(),
647 AttributeValue::Array(
648 redacted_attributes
649 .into_iter()
650 .map(AttributeValue::String)
651 .collect(),
652 ),
653 );
654 }
655
656 map.insert("_meta".to_string(), AttributeValue::Object(meta));
657 }
658
659 map
660 }
661
662 fn write_filter_attribute(
676 &self,
677 context: &Context,
678 map: &mut HashMap<String, AttributeValue>,
679 parent_path: Vec<String>,
680 key: &str,
681 value: AttributeValue,
682 redacted_attributes: &mut Vec<String>,
683 ) {
684 let mut path = parent_path;
685 path.push(key.to_string());
686
687 let (is_redacted, nested_properties_are_redacted) =
688 self.maybe_redact(context, &path, &value, redacted_attributes);
689
690 match value {
698 AttributeValue::Object(_) if is_redacted => (), AttributeValue::Object(ref object_map) => {
700 if !nested_properties_are_redacted {
702 map.insert(key.to_string(), value.clone());
703 return;
704 }
705
706 let mut sub_map = HashMap::new();
708 for (k, v) in object_map.iter() {
709 self.write_filter_attribute(
710 context,
711 &mut sub_map,
712 path.clone(),
713 k,
714 v.clone(),
715 redacted_attributes,
716 );
717 }
718 map.insert(key.to_string(), AttributeValue::Object(sub_map));
719 }
720 _ if !is_redacted => {
721 map.insert(key.to_string(), value);
722 }
723 _ => (),
724 };
725 }
726
727 fn maybe_redact(
748 &self,
749 context: &Context,
750 parent_path: &[String],
751 value: &AttributeValue,
752 redacted_attributes: &mut Vec<String>,
753 ) -> (bool, bool) {
754 let (redacted_attr_ref, mut nested_properties_are_redacted) =
755 self.check_global_private_attribute_refs(parent_path);
756
757 if let Some(redacted_attr_ref) = redacted_attr_ref {
758 redacted_attributes.push(String::from(redacted_attr_ref));
759 return (true, false);
760 }
761
762 let should_check_for_nested_properties = matches!(value, AttributeValue::Object(..));
763
764 if let Some(private_attributes) = &context.private_attributes {
765 for private_attribute in private_attributes.iter() {
766 let depth = private_attribute.depth();
767 if depth < parent_path.len() {
768 continue;
772 }
773
774 if !should_check_for_nested_properties && depth > parent_path.len() {
775 continue;
776 }
777
778 let mut has_match = true;
779 for (i, parent_part) in parent_path.iter().enumerate() {
780 match private_attribute.component(i) {
781 None => break,
782 Some(name) if name != parent_part => {
783 has_match = false;
784 break;
785 }
786 _ => continue,
787 };
788 }
789
790 if has_match {
791 if depth == parent_path.len() {
792 redacted_attributes.push(private_attribute.to_string());
793 return (true, false);
794 }
795 nested_properties_are_redacted = true;
796 }
797 }
798 }
799
800 (false, nested_properties_are_redacted)
801 }
802
803 fn check_global_private_attribute_refs(
820 &self,
821 parent_path: &[String],
822 ) -> (Option<Reference>, bool) {
823 let mut lookup = &self.global_private_attributes;
824
825 if self.global_private_attributes.is_empty() {
826 return (None, false);
827 }
828
829 for (index, path) in parent_path.iter().enumerate() {
830 let next_node = match lookup.get(path.as_str()) {
831 None => break,
832 Some(v) => v,
833 };
834
835 if index == parent_path.len() - 1 {
836 let var_name = (next_node.reference.clone(), next_node.reference.is_none());
837 return var_name;
838 } else if !next_node.children.is_empty() {
839 lookup = &next_node.children;
840 }
841 }
842
843 (None, false)
844 }
845}
846
847impl ser::Serialize for ContextAttributes {
848 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
849 where
850 S: ser::Serializer,
851 {
852 let mut serialize_map = serializer.serialize_map(None)?;
853
854 let map = match self.context.is_multi() {
855 true => self.write_multi_context(),
856 false => self.write_single_context(&self.context, true),
857 };
858
859 for (k, v) in map.iter().sorted_by_key(|p| p.0) {
860 serialize_map.serialize_entry(k, v)?;
861 }
862
863 serialize_map.end()
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::proptest_generators::*;
870 use crate::{AttributeValue, ContextBuilder, MultiContextBuilder, Reference};
871 use maplit::hashmap;
872 use proptest::proptest;
873 use std::collections::HashSet;
874 use test_case::test_case;
875
876 use super::{ContextAttributes, Kind};
877
878 proptest! {
879 #[test]
880 fn all_generated_kinds_are_valid(kind in any_kind()) {
881 let maybe_kind = Kind::try_from(kind.as_ref());
882 assert!(maybe_kind.is_ok());
883 }
884 }
885
886 #[test_case("kind"; "Cannot set kind as kind")]
887 #[test_case("multi"; "Cannot set kind as multi")]
888 #[test_case("🦀"; "Cannot set kind as invalid character")]
889 #[test_case(" "; "Cannot set kind as only whitespace")]
890 fn invalid_kinds(kind: &str) {
891 assert!(Kind::try_from(kind).is_err());
892 }
893
894 #[test_case(Kind::user(), true)]
895 #[test_case(Kind::from("user"), true)]
896 #[test_case(Kind::multi(), false)]
897 #[test_case(Kind::from("foo"), false)]
898 fn is_user(kind: Kind, is_user: bool) {
899 assert_eq!(kind.is_user(), is_user);
900 }
901
902 #[test_case(Kind::multi(), true)]
903 #[test_case(Kind::from("multi"), true)]
904 #[test_case(Kind::user(), false)]
905 #[test_case(Kind::from("foo"), false)]
906 fn is_multi(kind: Kind, is_multi: bool) {
907 assert_eq!(kind.is_multi(), is_multi);
908 }
909
910 #[test]
911 fn kind_sorts_based_on_string() {
912 let mut kinds = vec![
913 Kind::user(),
914 Kind::multi(),
915 Kind::from("n"),
916 Kind::from("v"),
917 Kind::from("l"),
918 ];
919 kinds.sort();
920 assert_eq!(
921 kinds,
922 vec![
923 Kind::from("l"),
924 Kind::multi(),
925 Kind::from("n"),
926 Kind::user(),
927 Kind::from("v"),
928 ]
929 );
930 }
931
932 proptest! {
933 #[test]
934 fn kind_comparison_identity(kind in any_kind()) {
935 assert_eq!(kind, kind);
936 }
937 }
938
939 proptest! {
940 #[test]
941 fn kind_comparison_identity_str(kind in any_kind()) {
942 assert_eq!(kind, kind.as_ref());
943 assert_eq!(&kind, kind.as_ref());
944 }
945 }
946
947 proptest! {
948 #[test]
949 fn kind_comparison_different(a in any_kind(), b in any_kind()) {
950 if a.0 != b.0 {
951 assert_ne!(a, b);
952 }
953 }
954 }
955
956 proptest! {
957 #[test]
958 fn kind_serialize(kind in any_kind()) {
959 assert_eq!(format!("\"{}\"", kind.0), serde_json::to_string(&kind).unwrap());
960 }
961 }
962
963 proptest! {
964 #[test]
965 fn kind_deserialize(kind_str in any_kind_string()) {
966 let json_str = format!("\"{}\"", &kind_str);
967 let kind: Result<Kind, _> = serde_json::from_str(&json_str);
968 assert!(kind.is_ok());
969 }
970 }
971
972 #[test]
975 fn cannot_deserialize_multi_kind() {
976 let maybe_kind: Result<Kind, _> = serde_json::from_str("\"multi\"");
977 assert!(maybe_kind.is_err());
978 }
979
980 #[test_case("kind", Some(AttributeValue::String("org".to_string())))]
982 #[test_case("key", Some(AttributeValue::String("my-key".to_string())))]
983 #[test_case("name", Some(AttributeValue::String("my-name".to_string())))]
984 #[test_case("anonymous", Some(AttributeValue::Bool(true)))]
985 #[test_case("attr", Some(AttributeValue::String("my-attr".to_string())))]
986 #[test_case("/starts-with-slash", Some(AttributeValue::String("love that prefix".to_string())))]
987 #[test_case("/crazy~0name", Some(AttributeValue::String("still works".to_string())))]
988 #[test_case("/other", None)]
989 #[test_case("/", None; "Single slash")]
991 #[test_case("", None; "Empty reference")]
992 #[test_case("/a//b", None; "Double slash")]
993 #[test_case("privateAttributes", None)]
995 #[test_case("secondary", None)]
996 #[test_case("/my-map/array", Some(AttributeValue::Array(vec![AttributeValue::String("first".to_string()), AttributeValue::String("second".to_string())])))]
998 #[test_case("/my-map/1", Some(AttributeValue::Bool(true)))]
999 #[test_case("/my-map/missing", None)]
1000 #[test_case("/starts-with-slash/1", None; "handles providing an index to a non-array value")]
1001 fn context_can_get_value(input: &str, value: Option<AttributeValue>) {
1002 let mut builder = ContextBuilder::new("my-key");
1003
1004 let array = vec![
1005 AttributeValue::String("first".to_string()),
1006 AttributeValue::String("second".to_string()),
1007 ];
1008 let map = hashmap! {
1009 "array".to_string() => AttributeValue::Array(array),
1010 "1".to_string() => AttributeValue::Bool(true)
1011 };
1012
1013 let context = builder
1014 .kind("org".to_string())
1015 .name("my-name")
1016 .anonymous(true)
1017 .secondary("my-secondary")
1018 .set_string("attr", "my-attr")
1019 .set_string("starts-with-slash", "love that prefix")
1020 .set_string("crazy~name", "still works")
1021 .set_value("my-map", AttributeValue::Object(map))
1022 .add_private_attribute("attr")
1023 .build()
1024 .expect("Failed to build context");
1025
1026 assert_eq!(context.get_value(&Reference::new(input)), value);
1027 }
1028
1029 #[test_case("kind", Some(AttributeValue::String("multi".to_string())))]
1030 #[test_case("key", None)]
1031 #[test_case("name", None)]
1032 #[test_case("anonymous", None)]
1033 #[test_case("attr", None)]
1034 fn multi_context_get_value(input: &str, value: Option<AttributeValue>) {
1035 let mut multi_builder = MultiContextBuilder::new();
1036 let mut builder = ContextBuilder::new("user");
1037
1038 multi_builder.add_context(builder.build().expect("Failed to create context"));
1039
1040 builder
1041 .key("org")
1042 .kind("org".to_string())
1043 .name("my-name")
1044 .anonymous(true)
1045 .set_string("attr", "my-attr");
1046 multi_builder.add_context(builder.build().expect("Failed to create context"));
1047
1048 let context = multi_builder.build().expect("Failed to create context");
1049
1050 assert_eq!(context.get_value(&Reference::new(input)), value);
1051 }
1052
1053 #[test]
1054 fn can_retrieve_context_from_multi_context() {
1055 let user_context = ContextBuilder::new("user").build().unwrap();
1056 let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
1057
1058 assert!(org_context.as_kind(&Kind::user()).is_none());
1059
1060 let multi_context = MultiContextBuilder::new()
1061 .add_context(user_context)
1062 .add_context(org_context)
1063 .build()
1064 .unwrap();
1065
1066 assert!(multi_context
1067 .as_kind(&Kind::user())
1068 .unwrap()
1069 .kind()
1070 .is_user());
1071
1072 assert_eq!(
1073 "org",
1074 multi_context
1075 .as_kind(&Kind::from("org"))
1076 .unwrap()
1077 .kind()
1078 .as_ref()
1079 );
1080
1081 assert!(multi_context.as_kind(&Kind::from("custom")).is_none());
1082 }
1083
1084 #[test]
1085 fn redacting_anon_from_anon_is_invalid() {
1086 let anon_context = ContextBuilder::new("user")
1087 .anonymous(true)
1088 .build()
1089 .expect("context build failed");
1090 let result = anon_context.without_anonymous_contexts();
1091
1092 assert!(result.is_err());
1093 }
1094
1095 #[test]
1096 fn redacting_anon_from_nonanon_results_in_no_change() {
1097 let context = ContextBuilder::new("user")
1098 .build()
1099 .expect("context build failed");
1100 let result = context.without_anonymous_contexts();
1101
1102 assert_eq!(context, result.unwrap());
1103 }
1104
1105 #[test]
1106 fn can_redact_anon_from_multi() {
1107 let user_context = ContextBuilder::new("user")
1108 .anonymous(true)
1109 .build()
1110 .expect("Failed to create context");
1111 let org_context = ContextBuilder::new("org")
1112 .kind("org")
1113 .build()
1114 .expect("Failed to create context");
1115
1116 let multi_context = MultiContextBuilder::new()
1117 .add_context(user_context)
1118 .add_context(org_context)
1119 .build()
1120 .expect("Failed to create context");
1121
1122 let context = multi_context
1123 .without_anonymous_contexts()
1124 .expect("Failed to redact anon");
1125
1126 assert_eq!("org", context.kind().as_ref());
1127 }
1128
1129 #[test]
1130 fn redact_anon_from_all_anon_multi_is_invalid() {
1131 let user_context = ContextBuilder::new("user")
1132 .anonymous(true)
1133 .build()
1134 .expect("Failed to create context");
1135 let org_context = ContextBuilder::new("org")
1136 .kind("org")
1137 .anonymous(true)
1138 .build()
1139 .expect("Failed to create context");
1140
1141 let multi_context = MultiContextBuilder::new()
1142 .add_context(user_context)
1143 .add_context(org_context)
1144 .build()
1145 .expect("Failed to create context");
1146
1147 let context = multi_context.without_anonymous_contexts();
1148
1149 assert!(context.is_err());
1150 }
1151
1152 fn redacted_attributes(json: &serde_json::Value) -> HashSet<String> {
1155 json["_meta"]["redactedAttributes"]
1156 .as_array()
1157 .expect("redactedAttributes should be an array")
1158 .iter()
1159 .map(|v| v.as_str().unwrap().to_string())
1160 .collect()
1161 }
1162
1163 #[test_case("/ssn", "/~1ssn"; "slash prefix is escaped")]
1164 #[test_case("/a~b", "/~1a~0b"; "slash prefix with tilde escapes both")]
1165 fn all_attributes_private_reports_escaped_references(name: &str, expected_ref: &str) {
1166 let context = ContextBuilder::new("my-key")
1167 .set_string(name, "secret")
1168 .build()
1169 .expect("Failed to build context");
1170
1171 let attributes = ContextAttributes::from_context(context, true, HashSet::new());
1172 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1173
1174 assert_eq!(
1175 redacted_attributes(&json),
1176 HashSet::from([expected_ref.to_string()])
1177 );
1178 assert!(json.get(name).is_none());
1180 }
1181
1182 #[test]
1183 fn anonymous_redaction_reports_slash_prefixed_names_as_escaped_references() {
1184 let context = ContextBuilder::new("my-key")
1185 .anonymous(true)
1186 .set_string("/ssn", "123-45-6789")
1187 .build()
1188 .expect("Failed to build context");
1189
1190 let attributes = ContextAttributes::from_context_with_anonymous_redaction(
1191 context,
1192 false,
1193 HashSet::new(),
1194 );
1195 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1196
1197 assert_eq!(
1198 redacted_attributes(&json),
1199 HashSet::from(["/~1ssn".to_string()])
1200 );
1201 assert!(json.get("/ssn").is_none());
1202 }
1203
1204 #[test_case(&["/address", "/address/street"]; "parent first")]
1207 #[test_case(&["/address/street", "/address"]; "child first")]
1208 fn global_private_parent_is_redacted_when_a_child_is_also_private(references: &[&str]) {
1209 let address = hashmap! {
1210 "street".to_string() => AttributeValue::String("123 Main St".to_string()),
1211 "city".to_string() => AttributeValue::String("Springfield".to_string()),
1212 };
1213 let context = ContextBuilder::new("my-key")
1214 .set_value("address", AttributeValue::Object(address))
1215 .build()
1216 .expect("Failed to build context");
1217
1218 let attributes = ContextAttributes {
1219 context,
1220 all_attributes_private: false,
1221 global_private_attributes: ContextAttributes::make_private_attribute_lookup_data(
1222 references.iter().map(Reference::new),
1223 ),
1224 redact_anonymous: false,
1225 };
1226 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1227
1228 assert_eq!(
1229 redacted_attributes(&json),
1230 HashSet::from(["/address".to_string()])
1231 );
1232 assert!(json.get("address").is_none());
1233 }
1234
1235 #[test]
1236 fn slash_prefixed_attribute_is_serialized_under_its_literal_name_when_not_private() {
1237 let context = ContextBuilder::new("my-key")
1238 .set_string("/ssn", "123-45-6789")
1239 .build()
1240 .expect("Failed to build context");
1241
1242 let attributes = ContextAttributes::from_context(context, false, HashSet::new());
1243 let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1244
1245 assert_eq!(json["/ssn"], serde_json::json!("123-45-6789"));
1246 assert!(json.get("_meta").is_none());
1247 }
1248}