dynamo_runtime/
namespace.rs1pub const GLOBAL_NAMESPACE: &str = "dynamo";
5
6#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum NamespaceFilter {
13 Global,
15 Exact(String),
17 Prefix(String),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum NamespacePrefixMode {
24 Literal,
25 WorkerGeneration,
26}
27
28impl NamespacePrefixMode {
29 pub fn from_env() -> Self {
30 if crate::config::env_is_truthy("DYN_NAMESPACE_PREFIX_STRICT") {
31 Self::WorkerGeneration
32 } else {
33 Self::Literal
34 }
35 }
36}
37
38impl NamespaceFilter {
39 pub fn from_namespace_and_prefix(
42 namespace: Option<&str>,
43 namespace_prefix: Option<&str>,
44 ) -> Self {
45 if let Some(prefix) = namespace_prefix {
47 if prefix.is_empty() || is_global_namespace(prefix) {
48 return NamespaceFilter::Global;
49 }
50 return NamespaceFilter::Prefix(prefix.to_string());
51 }
52
53 if let Some(ns) = namespace {
54 if ns.is_empty() || is_global_namespace(ns) {
55 return NamespaceFilter::Global;
56 }
57 return NamespaceFilter::Exact(ns.to_string());
58 }
59
60 NamespaceFilter::Global
61 }
62
63 pub fn matches(&self, namespace: &str) -> bool {
65 match self {
66 NamespaceFilter::Global => true,
67 NamespaceFilter::Exact(target) => namespace == target,
68 NamespaceFilter::Prefix(prefix) => namespace.starts_with(prefix),
69 }
70 }
71
72 pub fn matches_with_prefix_mode(&self, namespace: &str, mode: NamespacePrefixMode) -> bool {
74 match (self, mode) {
75 (NamespaceFilter::Prefix(prefix), NamespacePrefixMode::WorkerGeneration) => {
76 if namespace == prefix {
77 return true;
78 }
79 let Some(suffix) = namespace
80 .strip_prefix(prefix.as_str())
81 .and_then(|rest| rest.strip_prefix('-'))
82 else {
83 return false;
84 };
85 suffix == "legacy"
87 || (suffix.len() == 8
88 && suffix
89 .bytes()
90 .all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f')))
91 }
92 _ => self.matches(namespace),
93 }
94 }
95
96 pub fn is_global(&self) -> bool {
98 matches!(self, NamespaceFilter::Global)
99 }
100}
101
102pub fn is_global_namespace(namespace: &str) -> bool {
103 namespace == GLOBAL_NAMESPACE || namespace.is_empty()
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_from_namespace_and_prefix_global() {
112 assert_eq!(
113 NamespaceFilter::from_namespace_and_prefix(None, None),
114 NamespaceFilter::Global
115 );
116 assert_eq!(
117 NamespaceFilter::from_namespace_and_prefix(Some(""), None),
118 NamespaceFilter::Global
119 );
120 assert_eq!(
121 NamespaceFilter::from_namespace_and_prefix(Some(GLOBAL_NAMESPACE), None),
122 NamespaceFilter::Global
123 );
124 }
125
126 #[test]
127 fn test_from_namespace_and_prefix_exact() {
128 assert_eq!(
129 NamespaceFilter::from_namespace_and_prefix(Some("my-namespace"), None),
130 NamespaceFilter::Exact("my-namespace".to_string())
131 );
132 }
133
134 #[test]
135 fn test_from_namespace_and_prefix_prefix_takes_precedence() {
136 assert_eq!(
137 NamespaceFilter::from_namespace_and_prefix(Some("exact"), Some("prefix")),
138 NamespaceFilter::Prefix("prefix".to_string())
139 );
140 }
141
142 #[test]
143 fn test_matches_global() {
144 let filter = NamespaceFilter::Global;
145 assert!(filter.matches("anything"));
146 assert!(filter.matches(""));
147 assert!(filter.matches("default"));
148 assert!(filter.matches("ns-abc123"));
149 }
150
151 #[test]
152 fn test_matches_exact() {
153 let filter = NamespaceFilter::Exact("my-namespace".to_string());
154 assert!(filter.matches("my-namespace"));
155 assert!(!filter.matches("my-namespace-abc123"));
156 assert!(!filter.matches("other"));
157 assert!(!filter.matches(""));
158 }
159
160 #[test]
161 fn test_matches_prefix() {
162 let filter = NamespaceFilter::Prefix("ns".to_string());
163 assert!(filter.matches("ns"));
164 assert!(filter.matches("ns-abc123"));
165 assert!(filter.matches("ns-def456"));
166 assert!(!filter.matches("other-ns"));
167 assert!(!filter.matches(""));
168 }
169
170 #[test]
171 fn test_is_global() {
172 assert!(NamespaceFilter::Global.is_global());
173 assert!(!NamespaceFilter::Exact("ns".to_string()).is_global());
174 assert!(!NamespaceFilter::Prefix("ns".to_string()).is_global());
175 }
176
177 #[test]
178 fn operator_prefix_excludes_sibling_deployments() {
179 let literal = NamespaceFilter::from_namespace_and_prefix(None, Some("default-foo"));
180 for namespace in ["default-foo", "default-foo-1a2b3c4d", "default-foo-legacy"] {
181 assert!(
182 literal.matches_with_prefix_mode(namespace, NamespacePrefixMode::WorkerGeneration),
183 "{namespace}"
184 );
185 }
186 for namespace in [
187 "default-foo-bar",
188 "default-foo-bar-1a2b3c4d",
189 "default-foobar",
190 "default-foo-DEADBEEF",
191 "default-foo-1a2b3c4g",
192 "default-foo-1a2b3c4",
193 ] {
194 assert!(literal.matches(namespace), "manual prefix: {namespace}");
195 assert!(
196 !literal.matches_with_prefix_mode(namespace, NamespacePrefixMode::WorkerGeneration),
197 "operator prefix: {namespace}"
198 );
199 }
200 }
201
202 #[test]
203 fn strict_prefix_preserves_exact_and_global_scopes() {
204 for (namespace, prefix) in [
205 (None, None),
206 (Some("default-foo"), None),
207 (None, Some("dynamo")),
208 (None, Some("")),
209 ] {
210 let filter = NamespaceFilter::from_namespace_and_prefix(namespace, prefix);
211 for candidate in ["default-foo", "default-foo-1a2b3c4d", "default-foo-bar"] {
212 assert_eq!(
213 filter
214 .matches_with_prefix_mode(candidate, NamespacePrefixMode::WorkerGeneration),
215 filter.matches(candidate)
216 );
217 }
218 }
219 }
220
221 #[test]
222 fn prefix_mode_uses_canonical_environment_flag() {
223 for (value, expected) in [
224 (None, NamespacePrefixMode::Literal),
225 (Some("false"), NamespacePrefixMode::Literal),
226 (Some(" TRUE "), NamespacePrefixMode::WorkerGeneration),
227 (Some("on"), NamespacePrefixMode::WorkerGeneration),
228 ] {
229 temp_env::with_var("DYN_NAMESPACE_PREFIX_STRICT", value, || {
230 assert_eq!(NamespacePrefixMode::from_env(), expected);
231 });
232 }
233 }
234}