1use std::fmt;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use relay_base_schema::metrics::MetricNamespace;
6use relay_base_schema::organization::OrganizationId;
7use relay_base_schema::project::{ProjectId, ProjectKey};
8use serde::{Deserialize, Serialize};
9use smallvec::SmallVec;
10
11#[doc(inline)]
12pub use relay_base_schema::data_category::DataCategory;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct Scoping {
21 pub organization_id: OrganizationId,
23
24 pub project_id: ProjectId,
26
27 pub project_key: ProjectKey,
29
30 pub key_id: Option<u64>,
32}
33
34impl Scoping {
35 pub fn item(&self, category: DataCategory) -> ItemScoping {
41 ItemScoping {
42 category,
43 scoping: *self,
44 namespace: MetricNamespaceScoping::None,
45 }
46 }
47
48 pub fn metric_bucket(&self, namespace: MetricNamespace) -> ItemScoping {
54 ItemScoping {
55 category: DataCategory::MetricBucket,
56 scoping: *self,
57 namespace: MetricNamespaceScoping::Some(namespace),
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, PartialOrd)]
67pub enum MetricNamespaceScoping {
68 #[default]
72 None,
73
74 Some(MetricNamespace),
76
77 Any,
82}
83
84impl MetricNamespaceScoping {
85 pub fn matches(&self, namespace: MetricNamespace) -> bool {
91 match self {
92 Self::None => false,
93 Self::Some(ns) => *ns == namespace,
94 Self::Any => true,
95 }
96 }
97}
98
99impl From<MetricNamespace> for MetricNamespaceScoping {
100 fn from(namespace: MetricNamespace) -> Self {
101 Self::Some(namespace)
102 }
103}
104
105#[derive(Debug, Copy, Clone, Eq, PartialEq)]
110pub struct ItemScoping {
111 pub category: DataCategory,
113
114 pub scoping: Scoping,
116
117 pub namespace: MetricNamespaceScoping,
119}
120
121impl std::ops::Deref for ItemScoping {
122 type Target = Scoping;
123
124 fn deref(&self) -> &Self::Target {
125 &self.scoping
126 }
127}
128
129impl ItemScoping {
130 pub fn scope_id(&self, scope: QuotaScope) -> Option<u64> {
135 match scope {
136 QuotaScope::Global => None,
137 QuotaScope::Organization => Some(self.organization_id.value()),
138 QuotaScope::Project => Some(self.project_id.value()),
139 QuotaScope::Key => self.key_id,
140 QuotaScope::Unknown => None,
141 }
142 }
143
144 pub(crate) fn matches_categories(&self, categories: &[DataCategory]) -> bool {
146 categories.is_empty() || categories.contains(&self.category)
151 }
152
153 pub(crate) fn matches_namespaces<'a, I>(&self, namespaces: I) -> bool
167 where
168 I: IntoIterator<Item = &'a MetricNamespace>,
169 {
170 let mut iter = namespaces.into_iter().peekable();
171 iter.peek().is_none() || iter.any(|ns| self.namespace.matches(*ns))
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum CategoryUnit {
181 Count,
183 Bytes,
185 Milliseconds,
187}
188
189impl CategoryUnit {
190 fn from(category: &DataCategory) -> Option<Self> {
191 match category {
192 DataCategory::Default
193 | DataCategory::Error
194 | DataCategory::Transaction
195 | DataCategory::Replay
196 | DataCategory::DoNotUseReplayVideo
197 | DataCategory::Security
198 | DataCategory::Profile
199 | DataCategory::ProfileIndexed
200 | DataCategory::TransactionProcessed
201 | DataCategory::TransactionIndexed
202 | DataCategory::LogItem
203 | DataCategory::Span
204 | DataCategory::SpanIndexed
205 | DataCategory::MonitorSeat
206 | DataCategory::Monitor
207 | DataCategory::MetricBucket
208 | DataCategory::UserReportV2
209 | DataCategory::ProfileChunk
210 | DataCategory::ProfileChunkUi
211 | DataCategory::Uptime
212 | DataCategory::MetricSecond
213 | DataCategory::AttachmentItem
214 | DataCategory::SeerAutofix
215 | DataCategory::SeerScanner
216 | DataCategory::PreventUser
217 | DataCategory::PreventReview
218 | DataCategory::Session
219 | DataCategory::SizeAnalysis
220 | DataCategory::InstallableBuild
221 | DataCategory::TraceMetric
222 | DataCategory::SeerUser => Some(Self::Count),
223 DataCategory::Attachment | DataCategory::LogByte => Some(Self::Bytes),
224 DataCategory::ProfileDuration | DataCategory::ProfileDurationUi => {
225 Some(Self::Milliseconds)
226 }
227
228 DataCategory::Unknown => None,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
237#[serde(transparent)]
238pub struct DataCategories(Arc<[DataCategory]>);
239
240impl DataCategories {
241 pub fn new() -> Self {
243 Default::default()
244 }
245
246 fn new_sort_and_dedup<const N: usize>(mut s: SmallVec<[DataCategory; N]>) -> Self {
250 s.sort_unstable();
251 s.dedup();
252 Self(s.as_slice().into())
253 }
254
255 pub fn add(&self, category: DataCategory) -> Option<Self> {
260 if self.0.contains(&category) {
263 return None;
264 }
265
266 let mut new = SmallVec::<[DataCategory; 12]>::from(&*self.0);
267 new.push(category);
268 Some(new.into())
269 }
270}
271
272impl std::ops::Deref for DataCategories {
273 type Target = [DataCategory];
274
275 fn deref(&self) -> &Self::Target {
276 &self.0
277 }
278}
279
280impl<'de> Deserialize<'de> for DataCategories {
281 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
282 where
283 D: serde::Deserializer<'de>,
284 {
285 SmallVec::<[DataCategory; 12]>::deserialize(deserializer).map(Self::new_sort_and_dedup)
286 }
287}
288
289impl<const N: usize> From<SmallVec<[DataCategory; N]>> for DataCategories {
290 fn from(categories: SmallVec<[DataCategory; N]>) -> Self {
291 Self::new_sort_and_dedup(categories)
292 }
293}
294
295impl<const N: usize> From<[DataCategory; N]> for DataCategories {
296 fn from(categories: [DataCategory; N]) -> Self {
297 Self::new_sort_and_dedup(SmallVec::from_buf(categories))
298 }
299}
300
301impl FromIterator<DataCategory> for DataCategories {
302 fn from_iter<T: IntoIterator<Item = DataCategory>>(iter: T) -> Self {
303 let v: SmallVec<[DataCategory; 12]> = iter.into_iter().collect();
304 Self::new_sort_and_dedup(v)
305 }
306}
307
308#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
317#[serde(rename_all = "lowercase")]
318pub enum QuotaScope {
319 Global,
321 Organization,
325
326 Project,
330
331 Key,
335
336 #[serde(other)]
338 Unknown,
339}
340
341impl QuotaScope {
342 pub fn from_name(string: &str) -> Self {
346 match string {
347 "global" => Self::Global,
348 "organization" => Self::Organization,
349 "project" => Self::Project,
350 "key" => Self::Key,
351 _ => Self::Unknown,
352 }
353 }
354
355 pub fn name(self) -> &'static str {
359 match self {
360 Self::Global => "global",
361 Self::Key => "key",
362 Self::Project => "project",
363 Self::Organization => "organization",
364 Self::Unknown => "unknown",
365 }
366 }
367}
368
369impl fmt::Display for QuotaScope {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 write!(f, "{}", self.name())
372 }
373}
374
375impl FromStr for QuotaScope {
376 type Err = ();
377
378 fn from_str(string: &str) -> Result<Self, Self::Err> {
379 Ok(Self::from_name(string))
380 }
381}
382
383fn default_scope() -> QuotaScope {
384 QuotaScope::Organization
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
392pub struct ReasonCode(Arc<str>);
393
394impl ReasonCode {
395 pub fn new<S: Into<Arc<str>>>(code: S) -> Self {
401 Self(code.into())
402 }
403
404 pub fn as_str(&self) -> &str {
406 &self.0
407 }
408}
409
410impl fmt::Display for ReasonCode {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 self.0.fmt(f)
413 }
414}
415
416#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
429#[serde(rename_all = "camelCase")]
430pub struct Quota {
431 #[serde(default)]
435 pub id: Option<Arc<str>>,
436
437 #[serde(default)]
441 pub categories: DataCategories,
442
443 #[serde(default = "default_scope")]
448 pub scope: QuotaScope,
449
450 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub scope_id: Option<Arc<str>>,
456
457 #[serde(default)]
466 pub limit: Option<u64>,
467
468 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub window: Option<u64>,
474
475 pub namespace: Option<MetricNamespace>,
479
480 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub reason_code: Option<ReasonCode>,
486}
487
488impl Quota {
489 pub fn is_valid(&self) -> bool {
496 if self.namespace == Some(MetricNamespace::Unsupported) {
497 return false;
498 }
499
500 let mut units = self.categories.iter().filter_map(CategoryUnit::from);
501
502 match units.next() {
503 None if !self.categories.is_empty() => false,
505 _ if self.limit == Some(0) => true,
507 None => false,
509 Some(unit) => units.all(|u| u == unit),
511 }
512 }
513
514 fn matches_scope(&self, scoping: ItemScoping) -> bool {
521 if self.scope == QuotaScope::Global {
522 return true;
523 }
524
525 let Some(scope_id) = self.scope_id.as_ref() else {
529 return true;
530 };
531
532 let Ok(parsed) = scope_id.parse::<u64>() else {
535 return false;
536 };
537
538 scoping.scope_id(self.scope) == Some(parsed)
540 }
541
542 pub fn matches(&self, scoping: ItemScoping) -> bool {
547 self.matches_scope(scoping)
548 && scoping.matches_categories(&self.categories)
549 && scoping.matches_namespaces(&self.namespace)
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn test_parse_quota_reject_all() {
559 let json = r#"{
560 "limit": 0,
561 "reasonCode": "not_yet"
562 }"#;
563
564 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
565
566 insta::assert_ron_snapshot!(quota, @r###"
567 Quota(
568 id: None,
569 categories: [],
570 scope: organization,
571 limit: Some(0),
572 namespace: None,
573 reasonCode: Some(ReasonCode("not_yet")),
574 )
575 "###);
576 }
577
578 #[test]
579 fn test_parse_quota_reject_transactions() {
580 let json = r#"{
581 "limit": 0,
582 "categories": ["transaction"],
583 "reasonCode": "not_yet"
584 }"#;
585
586 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
587
588 insta::assert_ron_snapshot!(quota, @r###"
589 Quota(
590 id: None,
591 categories: [
592 transaction,
593 ],
594 scope: organization,
595 limit: Some(0),
596 namespace: None,
597 reasonCode: Some(ReasonCode("not_yet")),
598 )
599 "###);
600 }
601
602 #[test]
603 fn test_parse_quota_limited() {
604 let json = r#"{
605 "id": "o",
606 "limit": 4711,
607 "window": 42,
608 "reasonCode": "not_so_fast"
609 }"#;
610
611 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
612
613 insta::assert_ron_snapshot!(quota, @r###"
614 Quota(
615 id: Some("o"),
616 categories: [],
617 scope: organization,
618 limit: Some(4711),
619 window: Some(42),
620 namespace: None,
621 reasonCode: Some(ReasonCode("not_so_fast")),
622 )
623 "###);
624 }
625
626 #[test]
627 fn test_parse_quota_project() {
628 let json = r#"{
629 "id": "p",
630 "scope": "project",
631 "scopeId": "1",
632 "limit": 4711,
633 "window": 42,
634 "reasonCode": "not_so_fast"
635 }"#;
636
637 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
638
639 insta::assert_ron_snapshot!(quota, @r###"
640 Quota(
641 id: Some("p"),
642 categories: [],
643 scope: project,
644 scopeId: Some("1"),
645 limit: Some(4711),
646 window: Some(42),
647 namespace: None,
648 reasonCode: Some(ReasonCode("not_so_fast")),
649 )
650 "###);
651 }
652
653 #[test]
654 fn test_parse_quota_project_large() {
655 let json = r#"{
656 "id": "p",
657 "scope": "project",
658 "scopeId": "1",
659 "limit": 4294967296,
660 "window": 42,
661 "reasonCode": "not_so_fast"
662 }"#;
663
664 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
665
666 insta::assert_ron_snapshot!(quota, @r###"
667 Quota(
668 id: Some("p"),
669 categories: [],
670 scope: project,
671 scopeId: Some("1"),
672 limit: Some(4294967296),
673 window: Some(42),
674 namespace: None,
675 reasonCode: Some(ReasonCode("not_so_fast")),
676 )
677 "###);
678 }
679
680 #[test]
681 fn test_parse_quota_key() {
682 let json = r#"{
683 "id": "k",
684 "scope": "key",
685 "scopeId": "1",
686 "limit": 4711,
687 "window": 42,
688 "reasonCode": "not_so_fast"
689 }"#;
690
691 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
692
693 insta::assert_ron_snapshot!(quota, @r###"
694 Quota(
695 id: Some("k"),
696 categories: [],
697 scope: key,
698 scopeId: Some("1"),
699 limit: Some(4711),
700 window: Some(42),
701 namespace: None,
702 reasonCode: Some(ReasonCode("not_so_fast")),
703 )
704 "###);
705 }
706
707 #[test]
708 fn test_parse_quota_unknown_variants() {
709 let json = r#"{
710 "id": "f",
711 "categories": ["future"],
712 "scope": "future",
713 "scopeId": "1",
714 "limit": 4711,
715 "window": 42,
716 "reasonCode": "not_so_fast"
717 }"#;
718
719 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
720
721 insta::assert_ron_snapshot!(quota, @r###"
722 Quota(
723 id: Some("f"),
724 categories: [
725 unknown,
726 ],
727 scope: unknown,
728 scopeId: Some("1"),
729 limit: Some(4711),
730 window: Some(42),
731 namespace: None,
732 reasonCode: Some(ReasonCode("not_so_fast")),
733 )
734 "###);
735 }
736
737 #[test]
738 fn test_parse_quota_unlimited() {
739 let json = r#"{
740 "id": "o",
741 "window": 42
742 }"#;
743
744 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
745
746 insta::assert_ron_snapshot!(quota, @r###"
747 Quota(
748 id: Some("o"),
749 categories: [],
750 scope: organization,
751 limit: None,
752 window: Some(42),
753 namespace: None,
754 )
755 "###);
756 }
757
758 #[test]
759 fn test_quota_valid_reject_all() {
760 let quota = Quota {
761 id: None,
762 categories: Default::default(),
763 scope: QuotaScope::Organization,
764 scope_id: None,
765 limit: Some(0),
766 window: None,
767 reason_code: None,
768 namespace: None,
769 };
770
771 assert!(quota.is_valid());
772 }
773
774 #[test]
775 fn test_quota_invalid_only_unknown() {
776 let quota = Quota {
777 id: None,
778 categories: [DataCategory::Unknown, DataCategory::Unknown].into(),
779 scope: QuotaScope::Organization,
780 scope_id: None,
781 limit: Some(0),
782 window: None,
783 reason_code: None,
784 namespace: None,
785 };
786
787 assert!(!quota.is_valid());
788 }
789
790 #[test]
791 fn test_quota_valid_reject_all_mixed() {
792 let quota = Quota {
793 id: None,
794 categories: [DataCategory::Error, DataCategory::Attachment].into(),
795 scope: QuotaScope::Organization,
796 scope_id: None,
797 limit: Some(0),
798 window: None,
799 reason_code: None,
800 namespace: None,
801 };
802
803 assert!(quota.is_valid());
804 }
805
806 #[test]
807 fn test_quota_invalid_limited_mixed() {
808 let quota = Quota {
809 id: None,
810 categories: [DataCategory::Error, DataCategory::Attachment].into(),
811 scope: QuotaScope::Organization,
812 scope_id: None,
813 limit: Some(1000),
814 window: None,
815 reason_code: None,
816 namespace: None,
817 };
818
819 assert!(!quota.is_valid());
821 }
822
823 #[test]
824 fn test_quota_invalid_unlimited_mixed() {
825 let quota = Quota {
826 id: None,
827 categories: [DataCategory::Error, DataCategory::Attachment].into(),
828 scope: QuotaScope::Organization,
829 scope_id: None,
830 limit: None,
831 window: None,
832 reason_code: None,
833 namespace: None,
834 };
835
836 assert!(!quota.is_valid());
838 }
839
840 #[test]
841 fn test_quota_matches_no_categories() {
842 let quota = Quota {
843 id: None,
844 categories: Default::default(),
845 scope: QuotaScope::Organization,
846 scope_id: None,
847 limit: None,
848 window: None,
849 reason_code: None,
850 namespace: None,
851 };
852
853 assert!(quota.matches(ItemScoping {
854 category: DataCategory::Error,
855 scoping: Scoping {
856 organization_id: OrganizationId::new(42),
857 project_id: ProjectId::new(21),
858 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
859 key_id: Some(17),
860 },
861 namespace: MetricNamespaceScoping::None,
862 }));
863 }
864
865 #[test]
866 fn test_quota_matches_unknown_category() {
867 let quota = Quota {
868 id: None,
869 categories: [DataCategory::Unknown].into(),
870 scope: QuotaScope::Organization,
871 scope_id: None,
872 limit: None,
873 window: None,
874 reason_code: None,
875 namespace: None,
876 };
877
878 assert!(!quota.matches(ItemScoping {
879 category: DataCategory::Error,
880 scoping: Scoping {
881 organization_id: OrganizationId::new(42),
882 project_id: ProjectId::new(21),
883 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
884 key_id: Some(17),
885 },
886 namespace: MetricNamespaceScoping::None,
887 }));
888 }
889
890 #[test]
891 fn test_quota_matches_multiple_categores() {
892 let quota = Quota {
893 id: None,
894 categories: [DataCategory::Unknown, DataCategory::Error].into(),
895 scope: QuotaScope::Organization,
896 scope_id: None,
897 limit: None,
898 window: None,
899 reason_code: None,
900 namespace: None,
901 };
902
903 assert!(quota.matches(ItemScoping {
904 category: DataCategory::Error,
905 scoping: Scoping {
906 organization_id: OrganizationId::new(42),
907 project_id: ProjectId::new(21),
908 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
909 key_id: Some(17),
910 },
911 namespace: MetricNamespaceScoping::None,
912 }));
913
914 assert!(!quota.matches(ItemScoping {
915 category: DataCategory::Transaction,
916 scoping: Scoping {
917 organization_id: OrganizationId::new(42),
918 project_id: ProjectId::new(21),
919 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
920 key_id: Some(17),
921 },
922 namespace: MetricNamespaceScoping::None,
923 }));
924 }
925
926 #[test]
927 fn test_quota_matches_no_invalid_scope() {
928 let quota = Quota {
929 id: None,
930 categories: Default::default(),
931 scope: QuotaScope::Organization,
932 scope_id: Some("not_a_number".into()),
933 limit: None,
934 window: None,
935 reason_code: None,
936 namespace: None,
937 };
938
939 assert!(!quota.matches(ItemScoping {
940 category: DataCategory::Error,
941 scoping: Scoping {
942 organization_id: OrganizationId::new(42),
943 project_id: ProjectId::new(21),
944 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
945 key_id: Some(17),
946 },
947 namespace: MetricNamespaceScoping::None,
948 }));
949 }
950
951 #[test]
952 fn test_quota_matches_organization_scope() {
953 let quota = Quota {
954 id: None,
955 categories: Default::default(),
956 scope: QuotaScope::Organization,
957 scope_id: Some("42".into()),
958 limit: None,
959 window: None,
960 reason_code: None,
961 namespace: None,
962 };
963
964 assert!(quota.matches(ItemScoping {
965 category: DataCategory::Error,
966 scoping: Scoping {
967 organization_id: OrganizationId::new(42),
968 project_id: ProjectId::new(21),
969 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
970 key_id: Some(17),
971 },
972 namespace: MetricNamespaceScoping::None,
973 }));
974
975 assert!(!quota.matches(ItemScoping {
976 category: DataCategory::Error,
977 scoping: Scoping {
978 organization_id: OrganizationId::new(0),
979 project_id: ProjectId::new(21),
980 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
981 key_id: Some(17),
982 },
983 namespace: MetricNamespaceScoping::None,
984 }));
985 }
986
987 #[test]
988 fn test_quota_matches_project_scope() {
989 let quota = Quota {
990 id: None,
991 categories: Default::default(),
992 scope: QuotaScope::Project,
993 scope_id: Some("21".into()),
994 limit: None,
995 window: None,
996 reason_code: None,
997 namespace: None,
998 };
999
1000 assert!(quota.matches(ItemScoping {
1001 category: DataCategory::Error,
1002 scoping: Scoping {
1003 organization_id: OrganizationId::new(42),
1004 project_id: ProjectId::new(21),
1005 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1006 key_id: Some(17),
1007 },
1008 namespace: MetricNamespaceScoping::None,
1009 }));
1010
1011 assert!(!quota.matches(ItemScoping {
1012 category: DataCategory::Error,
1013 scoping: Scoping {
1014 organization_id: OrganizationId::new(42),
1015 project_id: ProjectId::new(0),
1016 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1017 key_id: Some(17),
1018 },
1019 namespace: MetricNamespaceScoping::None,
1020 }));
1021 }
1022
1023 #[test]
1024 fn test_quota_matches_key_scope() {
1025 let quota = Quota {
1026 id: None,
1027 categories: Default::default(),
1028 scope: QuotaScope::Key,
1029 scope_id: Some("17".into()),
1030 limit: None,
1031 window: None,
1032 reason_code: None,
1033 namespace: None,
1034 };
1035
1036 assert!(quota.matches(ItemScoping {
1037 category: DataCategory::Error,
1038 scoping: Scoping {
1039 organization_id: OrganizationId::new(42),
1040 project_id: ProjectId::new(21),
1041 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1042 key_id: Some(17),
1043 },
1044 namespace: MetricNamespaceScoping::None,
1045 }));
1046
1047 assert!(!quota.matches(ItemScoping {
1048 category: DataCategory::Error,
1049 scoping: Scoping {
1050 organization_id: OrganizationId::new(42),
1051 project_id: ProjectId::new(21),
1052 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1053 key_id: Some(0),
1054 },
1055 namespace: MetricNamespaceScoping::None,
1056 }));
1057
1058 assert!(!quota.matches(ItemScoping {
1059 category: DataCategory::Error,
1060 scoping: Scoping {
1061 organization_id: OrganizationId::new(42),
1062 project_id: ProjectId::new(21),
1063 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1064 key_id: None,
1065 },
1066 namespace: MetricNamespaceScoping::None,
1067 }));
1068 }
1069
1070 #[test]
1071 fn test_data_categories_sorted_deduplicated() {
1072 let a = DataCategories::from([
1073 DataCategory::Transaction,
1074 DataCategory::Span,
1075 DataCategory::Transaction,
1076 ]);
1077 let b = DataCategories::from([
1078 DataCategory::Span,
1079 DataCategory::Transaction,
1080 DataCategory::Span,
1081 ]);
1082 let c = DataCategories::from([DataCategory::Span, DataCategory::Transaction]);
1083
1084 assert_eq!(a, b);
1085 assert_eq!(b, c);
1086 assert_eq!(a, c);
1087 }
1088
1089 #[test]
1090 fn test_data_categories_serde() {
1091 let s: DataCategories = serde_json::from_str(r#"["span", "transaction", "span"]"#).unwrap();
1092 insta::assert_json_snapshot!(s, @r#"
1093 [
1094 "transaction",
1095 "span"
1096 ]
1097 "#);
1098 }
1099
1100 #[test]
1101 fn test_data_categories_add() {
1102 let c = DataCategories::new();
1103 let c = c.add(DataCategory::Span).unwrap();
1104 assert!(c.add(DataCategory::Span).is_none());
1105 let c = c.add(DataCategory::Transaction).unwrap();
1106 assert_eq!(c, [DataCategory::Span, DataCategory::Transaction].into());
1107 }
1108}