1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum DimensionProfile {
5 TenantScoped,
6 PermissionScoped,
7 TenantPermissionScoped,
8 TenantPermissionSearch,
9 PagedSearch,
10 CursorList,
11 LocaleRegionScoped,
12 FeatureFlagScoped,
13 Custom(CustomProfile),
14}
15
16impl DimensionProfile {
17 pub fn name(&self) -> &str {
18 match self {
19 Self::TenantScoped => "tenant_scoped",
20 Self::PermissionScoped => "permission_scoped",
21 Self::TenantPermissionScoped => "tenant_permission_scoped",
22 Self::TenantPermissionSearch => "tenant_permission_search",
23 Self::PagedSearch => "paged_search",
24 Self::CursorList => "cursor_list",
25 Self::LocaleRegionScoped => "locale_region_scoped",
26 Self::FeatureFlagScoped => "feature_flag_scoped",
27 Self::Custom(profile) => profile.name(),
28 }
29 }
30
31 pub fn requirements(&self) -> Vec<DimensionRequirement> {
32 match self {
33 Self::TenantScoped => vec![DimensionRequirement::linked("tenant")],
34 Self::PermissionScoped => vec![DimensionRequirement::linked("permission")],
35 Self::TenantPermissionScoped => vec![
36 DimensionRequirement::linked("tenant"),
37 DimensionRequirement::linked("permission"),
38 ],
39 Self::TenantPermissionSearch => vec![
40 DimensionRequirement::linked("tenant"),
41 DimensionRequirement::linked("permission"),
42 DimensionRequirement::linked("q"),
43 DimensionRequirement::linked("page"),
44 DimensionRequirement::linked("sort"),
45 ],
46 Self::PagedSearch => vec![
47 DimensionRequirement::linked("q"),
48 DimensionRequirement::linked("page"),
49 DimensionRequirement::linked("sort"),
50 ],
51 Self::CursorList => vec![DimensionRequirement::linked("cursor")],
52 Self::LocaleRegionScoped => vec![
53 DimensionRequirement::linked("locale"),
54 DimensionRequirement::linked("region"),
55 ],
56 Self::FeatureFlagScoped => vec![DimensionRequirement::linked("feature")],
57 Self::Custom(profile) => profile.requirements().to_vec(),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CustomProfile {
64 name: String,
65 requirements: Vec<DimensionRequirement>,
66}
67
68impl CustomProfile {
69 pub fn new(
70 name: impl Into<String>,
71 requirements: impl IntoIterator<Item = DimensionRequirement>,
72 ) -> Self {
73 Self {
74 name: name.into(),
75 requirements: requirements.into_iter().collect(),
76 }
77 }
78
79 pub fn name(&self) -> &str {
80 &self.name
81 }
82
83 pub fn requirements(&self) -> &[DimensionRequirement] {
84 &self.requirements
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct DimensionRequirement {
90 label: String,
91 require_key_tag_link: bool,
92}
93
94impl DimensionRequirement {
95 pub fn linked(label: impl Into<String>) -> Self {
96 Self {
97 label: label.into(),
98 require_key_tag_link: true,
99 }
100 }
101
102 pub fn key_only(label: impl Into<String>) -> Self {
103 Self {
104 label: label.into(),
105 require_key_tag_link: false,
106 }
107 }
108
109 pub fn label(&self) -> &str {
110 &self.label
111 }
112
113 pub fn require_key_tag_link(&self) -> bool {
114 self.require_key_tag_link
115 }
116}
117
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub enum DimensionValidationMode {
120 #[default]
121 Warn,
122 Deny,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum ProfileValidation {
127 Pass,
128 MissingDimensions(Vec<String>),
129 UnlinkedDimensions(Vec<String>),
130 Allowed {
131 status: Box<ProfileValidation>,
132 reason: String,
133 },
134}
135
136impl ProfileValidation {
137 pub fn is_pass(&self) -> bool {
138 matches!(self, Self::Pass | Self::Allowed { .. })
139 }
140}
141
142impl fmt::Display for ProfileValidation {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::Pass => formatter.write_str("pass"),
146 Self::MissingDimensions(labels) => {
147 write!(formatter, "missing dimensions: {}", labels.join(", "))
148 }
149 Self::UnlinkedDimensions(labels) => {
150 write!(formatter, "unlinked dimensions: {}", labels.join(", "))
151 }
152 Self::Allowed { status, reason } => {
153 write!(formatter, "allowed {status} because {reason}")
154 }
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct DimensionAllow {
161 label: String,
162 reason: String,
163}
164
165impl DimensionAllow {
166 pub fn new(
167 label: impl Into<String>,
168 reason: impl Into<String>,
169 ) -> Result<Self, DimensionAllowError> {
170 let reason = reason.into();
171 if reason.trim().is_empty() {
172 return Err(DimensionAllowError::EmptyReason);
173 }
174 Ok(Self {
175 label: label.into(),
176 reason,
177 })
178 }
179
180 pub fn label(&self) -> &str {
181 &self.label
182 }
183
184 pub fn reason(&self) -> &str {
185 &self.reason
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum DimensionAllowError {
191 EmptyReason,
192}
193
194impl fmt::Display for DimensionAllowError {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 Self::EmptyReason => formatter.write_str("dimension allow reason cannot be empty"),
198 }
199 }
200}
201
202impl std::error::Error for DimensionAllowError {}