1use crate::canonicalize::ash_canonicalize_query;
22use crate::errors::{AshError, AshErrorCode};
23use crate::proof::ash_hash_body;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CanonicalQueryResult {
32 pub canonical: String,
34
35 pub pairs_count: usize,
37
38 pub had_fragment: bool,
40
41 pub had_leading_question_mark: bool,
43
44 pub unique_keys: usize,
46}
47
48pub fn ash_canonicalize_query_enriched(input: &str) -> Result<CanonicalQueryResult, AshError> {
65 let had_leading_question_mark = input.starts_with('?');
66 let had_fragment = input.contains('#');
67
68 let canonical = ash_canonicalize_query(input)?;
69
70 let pairs_count = if canonical.is_empty() {
71 0
72 } else {
73 canonical.split('&').count()
74 };
75
76 let unique_keys = if canonical.is_empty() {
77 0
78 } else {
79 let mut keys: Vec<&str> = canonical
80 .split('&')
81 .filter_map(|pair| pair.split('=').next())
82 .collect();
83 keys.sort();
84 keys.dedup();
85 keys.len()
86 };
87
88 Ok(CanonicalQueryResult {
89 canonical,
90 pairs_count,
91 had_fragment,
92 had_leading_question_mark,
93 unique_keys,
94 })
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct BodyHashResult {
104 pub hash: String,
106
107 pub input_bytes: usize,
109
110 pub is_empty: bool,
112}
113
114pub fn ash_hash_body_enriched(canonical_body: &str) -> BodyHashResult {
133 let hash = ash_hash_body(canonical_body);
134 let input_bytes = canonical_body.len();
135
136 BodyHashResult {
137 hash,
138 input_bytes,
139 is_empty: canonical_body.is_empty(),
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct NormalizedBinding {
150 pub binding: String,
152
153 pub method: String,
155
156 pub path: String,
158
159 pub canonical_query: String,
161
162 pub had_query: bool,
164}
165
166pub fn ash_normalize_binding_enriched(
183 method: &str,
184 path: &str,
185 query: &str,
186) -> Result<NormalizedBinding, AshError> {
187 let binding = crate::ash_normalize_binding(method, path, query)?;
188
189 let parsed = ash_parse_binding(&binding)?;
192 Ok(NormalizedBinding {
193 had_query: !query.trim().is_empty(),
194 ..parsed
195 })
196}
197
198pub fn ash_parse_binding(binding: &str) -> Result<NormalizedBinding, AshError> {
214 let parts: Vec<&str> = binding.splitn(3, '|').collect();
215 if parts.len() != 3 {
216 return Err(AshError::new(
217 AshErrorCode::ValidationError,
218 format!(
219 "Invalid binding format: expected METHOD|PATH|QUERY, got {} parts",
220 parts.len()
221 ),
222 ));
223 }
224
225 let canonical_query = parts[2].to_string();
226 let had_query = !canonical_query.is_empty();
227
228 Ok(NormalizedBinding {
229 binding: binding.to_string(),
230 method: parts[0].to_string(),
231 path: parts[1].to_string(),
232 canonical_query,
233 had_query,
234 })
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
244 fn test_query_enriched_basic() {
245 let result = ash_canonicalize_query_enriched("a=1&b=2").unwrap();
246 assert_eq!(result.canonical, "a=1&b=2");
247 assert_eq!(result.pairs_count, 2);
248 assert_eq!(result.unique_keys, 2);
249 assert!(!result.had_fragment);
250 assert!(!result.had_leading_question_mark);
251 }
252
253 #[test]
254 fn test_query_enriched_with_fragment() {
255 let result = ash_canonicalize_query_enriched("a=1#section").unwrap();
256 assert_eq!(result.canonical, "a=1");
257 assert!(result.had_fragment);
258 }
259
260 #[test]
261 fn test_query_enriched_with_question_mark() {
262 let result = ash_canonicalize_query_enriched("?a=1").unwrap();
263 assert_eq!(result.canonical, "a=1");
264 assert!(result.had_leading_question_mark);
265 }
266
267 #[test]
268 fn test_query_enriched_duplicate_keys() {
269 let result = ash_canonicalize_query_enriched("a=1&a=2&b=3").unwrap();
270 assert_eq!(result.pairs_count, 3);
271 assert_eq!(result.unique_keys, 2); }
273
274 #[test]
275 fn test_query_enriched_empty() {
276 let result = ash_canonicalize_query_enriched("").unwrap();
277 assert_eq!(result.canonical, "");
278 assert_eq!(result.pairs_count, 0);
279 assert_eq!(result.unique_keys, 0);
280 }
281
282 #[test]
283 fn test_query_enriched_sorting() {
284 let result = ash_canonicalize_query_enriched("z=3&a=1&m=2").unwrap();
285 assert_eq!(result.canonical, "a=1&m=2&z=3");
286 assert_eq!(result.pairs_count, 3);
287 }
288
289 #[test]
290 fn test_query_enriched_full_metadata() {
291 let result = ash_canonicalize_query_enriched("?z=3&a=1&a=2#frag").unwrap();
292 assert_eq!(result.canonical, "a=1&a=2&z=3");
293 assert_eq!(result.pairs_count, 3);
294 assert_eq!(result.unique_keys, 2);
295 assert!(result.had_fragment);
296 assert!(result.had_leading_question_mark);
297 }
298
299 #[test]
302 fn test_body_hash_enriched_basic() {
303 let result = ash_hash_body_enriched(r#"{"amount":100}"#);
304 assert_eq!(result.hash.len(), 64);
305 assert_eq!(result.input_bytes, 14);
306 assert!(!result.is_empty);
307 }
308
309 #[test]
310 fn test_body_hash_enriched_empty() {
311 let result = ash_hash_body_enriched("");
312 assert_eq!(result.hash.len(), 64);
313 assert_eq!(result.input_bytes, 0);
314 assert!(result.is_empty);
315 }
316
317 #[test]
318 fn test_body_hash_enriched_matches_base() {
319 let body = r#"{"test":"value"}"#;
320 let base = ash_hash_body(body);
321 let enriched = ash_hash_body_enriched(body);
322 assert_eq!(base, enriched.hash);
323 }
324
325 #[test]
328 fn test_binding_enriched_basic() {
329 let result = ash_normalize_binding_enriched("POST", "/api/users", "").unwrap();
330 assert_eq!(result.binding, "POST|/api/users|");
331 assert_eq!(result.method, "POST");
332 assert_eq!(result.path, "/api/users");
333 assert_eq!(result.canonical_query, "");
334 assert!(!result.had_query);
335 }
336
337 #[test]
338 fn test_binding_enriched_with_query() {
339 let result =
340 ash_normalize_binding_enriched("GET", "/api/search", "z=3&a=1").unwrap();
341 assert_eq!(result.binding, "GET|/api/search|a=1&z=3");
342 assert_eq!(result.method, "GET");
343 assert_eq!(result.path, "/api/search");
344 assert_eq!(result.canonical_query, "a=1&z=3");
345 assert!(result.had_query);
346 }
347
348 #[test]
349 fn test_binding_enriched_normalization() {
350 let result =
351 ash_normalize_binding_enriched("post", "/api//users/", "").unwrap();
352 assert_eq!(result.method, "POST");
353 assert_eq!(result.path, "/api/users");
354 }
355
356 #[test]
357 fn test_binding_enriched_matches_base() {
358 let base = crate::ash_normalize_binding("GET", "/api/test", "b=2&a=1").unwrap();
359 let enriched =
360 ash_normalize_binding_enriched("GET", "/api/test", "b=2&a=1").unwrap();
361 assert_eq!(base, enriched.binding);
362 }
363
364 #[test]
367 fn test_parse_binding_basic() {
368 let result = ash_parse_binding("POST|/api/users|page=1").unwrap();
369 assert_eq!(result.method, "POST");
370 assert_eq!(result.path, "/api/users");
371 assert_eq!(result.canonical_query, "page=1");
372 assert!(result.had_query);
373 }
374
375 #[test]
376 fn test_parse_binding_no_query() {
377 let result = ash_parse_binding("GET|/|").unwrap();
378 assert_eq!(result.method, "GET");
379 assert_eq!(result.path, "/");
380 assert_eq!(result.canonical_query, "");
381 assert!(!result.had_query);
382 }
383
384 #[test]
385 fn test_parse_binding_invalid_format() {
386 assert!(ash_parse_binding("invalid").is_err());
387 assert!(ash_parse_binding("GET|/api").is_err());
388 }
389
390 #[test]
391 fn test_parse_binding_roundtrip() {
392 let binding = crate::ash_normalize_binding("PUT", "/api/resource", "id=5").unwrap();
393 let parsed = ash_parse_binding(&binding).unwrap();
394 assert_eq!(parsed.binding, binding);
395 assert_eq!(parsed.method, "PUT");
396 }
397}