alembic_engine/
endpoint.rs1pub fn normalize_endpoint(endpoint: &str, is_object_id: impl Fn(&str) -> bool) -> Option<String> {
7 let trimmed = endpoint.trim();
8 if trimmed.is_empty() {
9 return None;
10 }
11
12 let mut path = trimmed;
13 if let Some(idx) = trimmed.find("/api/") {
14 path = &trimmed[idx + 5..];
15 }
16 let path = path.trim_start_matches('/');
17 let path = path.strip_prefix("api/").unwrap_or(path);
18 let trimmed = path.trim_end_matches('/');
19 let mut segments: Vec<&str> = trimmed.split('/').collect();
20 if let Some(last) = segments.last().copied() {
21 if !last.is_empty() && is_object_id(last) {
22 segments.pop();
23 }
24 }
25 if segments.is_empty() {
26 return None;
27 }
28 let mut normalized = segments.join("/");
29 normalized.push('/');
30 Some(normalized)
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36
37 fn is_digits(s: &str) -> bool {
38 s.chars().all(|c| c.is_ascii_digit())
39 }
40
41 #[test]
42 fn strips_scheme_host_and_api_prefix() {
43 assert_eq!(
44 normalize_endpoint("https://netbox.example.com/api/dcim/sites/", is_digits),
45 Some("dcim/sites/".to_string())
46 );
47 assert_eq!(
48 normalize_endpoint("http://localhost/api/dcim/devices/", is_digits),
49 Some("dcim/devices/".to_string())
50 );
51 assert_eq!(
52 normalize_endpoint("/api/ipam/prefixes/", is_digits),
53 Some("ipam/prefixes/".to_string())
54 );
55 assert_eq!(
56 normalize_endpoint("api/dcim/devices/", is_digits),
57 Some("dcim/devices/".to_string())
58 );
59 }
60
61 #[test]
62 fn adds_trailing_slash_and_trims_whitespace() {
63 assert_eq!(
64 normalize_endpoint("dcim/devices", is_digits),
65 Some("dcim/devices/".to_string())
66 );
67 assert_eq!(
68 normalize_endpoint(" dcim/devices/ ", is_digits),
69 Some("dcim/devices/".to_string())
70 );
71 }
72
73 #[test]
74 fn drops_trailing_id_only_when_predicate_matches() {
75 assert_eq!(
78 normalize_endpoint("dcim/sites/5/", is_digits),
79 Some("dcim/sites/".to_string())
80 );
81 assert_eq!(
82 normalize_endpoint("dcim/sites/5/", |_| false),
83 Some("dcim/sites/5/".to_string())
84 );
85 }
86
87 #[test]
88 fn empty_or_id_only_inputs_return_none() {
89 assert_eq!(normalize_endpoint("", is_digits), None);
90 assert_eq!(normalize_endpoint(" ", is_digits), None);
91 assert_eq!(normalize_endpoint("/api/5/", is_digits), None);
93 }
94}