braze_sync/values/
placeholder.rs1use regex_lite::Regex;
9use std::sync::OnceLock;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum PlaceholderType {
13 Lid,
14 CbId,
15}
16
17impl PlaceholderType {
18 pub fn as_str(&self) -> &'static str {
19 match self {
20 PlaceholderType::Lid => "lid",
21 PlaceholderType::CbId => "cb_id",
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Placeholder {
29 pub ty: Option<PlaceholderType>,
32 pub start: usize,
34 pub end: usize,
36}
37
38pub const TOKEN: &str = "__BRAZESYNC__";
39
40#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ResolutionError {
44 UnresolvedLid {
46 start: usize,
47 anchor: Option<String>,
48 },
49 UnresolvedCbId { start: usize, name: Option<String> },
52 UnknownContext { start: usize },
55 RetiredNamespace { token: String },
59}
60
61fn lid_prefix_re() -> &'static Regex {
62 static RE: OnceLock<Regex> = OnceLock::new();
63 RE.get_or_init(|| Regex::new(r#"\|\s*lid:\s*['"]$"#).expect("lid prefix regex is valid"))
64}
65
66fn cb_id_prefix_re() -> &'static Regex {
67 static RE: OnceLock<Regex> = OnceLock::new();
68 RE.get_or_init(|| Regex::new(r#"\|\s*id:\s*['"]$"#).expect("cb_id prefix regex is valid"))
69}
70
71pub fn infer_type(prefix: &str) -> Option<PlaceholderType> {
74 if lid_prefix_re().is_match(prefix) {
75 return Some(PlaceholderType::Lid);
76 }
77 if cb_id_prefix_re().is_match(prefix) {
78 return Some(PlaceholderType::CbId);
79 }
80 None
81}
82
83pub fn extract_placeholders(body: &str) -> Vec<Placeholder> {
86 let mut out = Vec::new();
87 let mut i = 0;
88 while let Some(rel) = body[i..].find(TOKEN) {
89 let start = i + rel;
90 let end = start + TOKEN.len();
91 let ty = infer_type(&body[..start]);
92 out.push(Placeholder { ty, start, end });
93 i = end;
94 }
95 out
96}
97
98pub fn has_placeholders(body: &str) -> bool {
101 body.contains(TOKEN)
102}
103
104fn retired_envelope_re() -> &'static Regex {
108 static RE: OnceLock<Regex> = OnceLock::new();
109 RE.get_or_init(|| {
110 Regex::new(r"__BRAZE?SYNC\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+__")
111 .expect("retired envelope regex is valid")
112 })
113}
114
115fn typo_token_re() -> &'static Regex {
118 static RE: OnceLock<Regex> = OnceLock::new();
119 RE.get_or_init(|| Regex::new(r"__BRAZE?SYNC[A-Z]*__").expect("typo token regex is valid"))
120}
121
122pub fn find_suspicious_placeholders(body: &str) -> Vec<String> {
125 let mut out = Vec::new();
126 for m in retired_envelope_re().find_iter(body) {
127 out.push(m.as_str().to_string());
128 }
129 for m in typo_token_re().find_iter(body) {
130 let s = m.as_str();
131 if s == TOKEN {
132 continue;
133 }
134 if out.iter().any(|o: &String| o.starts_with(s)) {
137 continue;
138 }
139 out.push(s.to_string());
140 }
141 out
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn extracts_anonymous_lid_token() {
150 let body = "x | lid: '__BRAZESYNC__' y";
151 let ps = extract_placeholders(body);
152 assert_eq!(ps.len(), 1);
153 assert_eq!(ps[0].ty, Some(PlaceholderType::Lid));
154 }
155
156 #[test]
157 fn extracts_anonymous_cb_id_token() {
158 let body = "x | id: '__BRAZESYNC__' y";
159 let ps = extract_placeholders(body);
160 assert_eq!(ps.len(), 1);
161 assert_eq!(ps[0].ty, Some(PlaceholderType::CbId));
162 }
163
164 #[test]
165 fn double_quoted_context_recognized() {
166 let body = r#"| lid: "__BRAZESYNC__""#;
167 let ps = extract_placeholders(body);
168 assert_eq!(ps[0].ty, Some(PlaceholderType::Lid));
169 }
170
171 #[test]
172 fn token_without_filter_context_has_none_type() {
173 let body = "bare __BRAZESYNC__ token";
174 let ps = extract_placeholders(body);
175 assert_eq!(ps.len(), 1);
176 assert!(ps[0].ty.is_none());
177 }
178
179 #[test]
180 fn token_outside_quotes_has_none_type() {
181 let body = "| lid: __BRAZESYNC__";
182 let ps = extract_placeholders(body);
183 assert!(ps[0].ty.is_none());
184 }
185
186 #[test]
187 fn multiple_tokens_in_order() {
188 let body = "a | lid: '__BRAZESYNC__' b | id: '__BRAZESYNC__' c";
189 let ps = extract_placeholders(body);
190 assert_eq!(ps.len(), 2);
191 assert_eq!(ps[0].ty, Some(PlaceholderType::Lid));
192 assert_eq!(ps[1].ty, Some(PlaceholderType::CbId));
193 assert!(ps[0].start < ps[1].start);
194 }
195
196 #[test]
197 fn retired_envelope_is_suspicious() {
198 let body = "stuff __BRAZESYNC.lid.foo__ stuff";
199 let warns = find_suspicious_placeholders(body);
200 assert_eq!(warns, vec!["__BRAZESYNC.lid.foo__".to_string()]);
201 }
202
203 #[test]
204 fn retired_custom_namespace_is_suspicious() {
205 let body = "x __BRAZESYNC.custom.foo__ y";
206 let warns = find_suspicious_placeholders(body);
207 assert_eq!(warns, vec!["__BRAZESYNC.custom.foo__".to_string()]);
208 }
209
210 #[test]
211 fn typo_token_is_suspicious() {
212 let body = "x __BRAZSYNC__ y";
213 let warns = find_suspicious_placeholders(body);
214 assert!(warns.iter().any(|w| w.contains("BRAZSYNC")));
215 }
216
217 #[test]
218 fn strict_token_is_not_suspicious() {
219 let body = "x __BRAZESYNC__ y";
220 assert!(find_suspicious_placeholders(body).is_empty());
221 }
222}