Skip to main content

nginx_discovery/types/
location.rs

1// src/types/location.rs
2use crate::types::AccessLog;
3use std::path::PathBuf;
4/// Represents an NGINX location block
5#[derive(Debug, Clone, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub struct Location {
8    /// Location path/pattern
9    pub path: String,
10
11    /// Location modifier
12    pub modifier: LocationModifier,
13
14    /// Root directory (if specified)
15    pub root: Option<PathBuf>,
16
17    /// Proxy pass upstream (if specified)
18    pub proxy_pass: Option<String>,
19
20    /// Access logs for this location
21    pub access_logs: Vec<AccessLog>,
22}
23
24impl Location {
25    /// Create a new location
26    pub fn new(path: impl Into<String>, modifier: LocationModifier) -> Self {
27        Self {
28            path: path.into(),
29            modifier,
30            root: None,
31            proxy_pass: None,
32            access_logs: Vec::new(),
33        }
34    }
35
36    /// Check if this is a proxy location
37    #[must_use]
38    pub fn is_proxy(&self) -> bool {
39        self.proxy_pass.is_some()
40    }
41
42    /// Check if this serves static files
43    #[must_use]
44    pub fn is_static(&self) -> bool {
45        self.root.is_some() && self.proxy_pass.is_none()
46    }
47}
48
49/// Location modifier types
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum LocationModifier {
53    /// No modifier: `location /path`
54    None,
55
56    /// Exact match: `location = /path`
57    Exact,
58
59    /// Prefix match (priority): `location ^~ /path`
60    PrefixPriority,
61
62    /// Case-sensitive regex: `location ~ pattern`
63    Regex,
64
65    /// Case-insensitive regex: `location ~* pattern`
66    RegexCaseInsensitive,
67}
68
69impl LocationModifier {
70    /// Parse from location directive arguments
71    #[must_use]
72    pub fn from_args(args: &[String]) -> (Self, String) {
73        if args.is_empty() {
74            return (Self::None, "/".to_string());
75        }
76
77        // Check first argument for modifier
78        match args[0].as_str() {
79            "=" => {
80                let path = args.get(1).map_or("/", String::as_str);
81                (Self::Exact, path.to_string())
82            }
83            "^~" => {
84                let path = args.get(1).map_or("/", String::as_str);
85                (Self::PrefixPriority, path.to_string())
86            }
87            "~" => {
88                let pattern = args.get(1).map_or("", String::as_str);
89                (Self::Regex, pattern.to_string())
90            }
91            "~*" => {
92                let pattern = args.get(1).map_or("", String::as_str);
93                (Self::RegexCaseInsensitive, pattern.to_string())
94            }
95            _ => {
96                // No modifier, first arg is the path
97                (Self::None, args[0].clone())
98            }
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_location_new() {
109        let location = Location::new("/api", LocationModifier::None);
110        assert_eq!(location.path, "/api");
111        assert_eq!(location.modifier, LocationModifier::None);
112        assert!(location.root.is_none());
113        assert!(location.proxy_pass.is_none());
114        assert!(location.access_logs.is_empty());
115    }
116
117    #[test]
118    fn test_is_proxy_true() {
119        let mut location = Location::new("/api", LocationModifier::None);
120        location.proxy_pass = Some("http://backend:8080".to_string());
121
122        assert!(location.is_proxy());
123    }
124
125    #[test]
126    fn test_is_proxy_false() {
127        let location = Location::new("/api", LocationModifier::None);
128        assert!(!location.is_proxy());
129    }
130
131    #[test]
132    fn test_is_static_true() {
133        let mut location = Location::new("/static", LocationModifier::None);
134        location.root = Some(PathBuf::from("/var/www/static"));
135
136        assert!(location.is_static());
137    }
138
139    #[test]
140    fn test_is_static_false_no_root() {
141        let location = Location::new("/", LocationModifier::None);
142        assert!(!location.is_static());
143    }
144
145    #[test]
146    fn test_is_static_false_has_proxy() {
147        let mut location = Location::new("/api", LocationModifier::None);
148        location.root = Some(PathBuf::from("/var/www"));
149        location.proxy_pass = Some("http://backend".to_string());
150
151        assert!(!location.is_static());
152    }
153
154    #[test]
155    fn test_location_modifier_none() {
156        let args = vec!["/path".to_string()];
157        let (modifier, path) = LocationModifier::from_args(&args);
158
159        assert_eq!(modifier, LocationModifier::None);
160        assert_eq!(path, "/path");
161    }
162
163    #[test]
164    fn test_location_modifier_exact() {
165        let args = vec!["=".to_string(), "/exact".to_string()];
166        let (modifier, path) = LocationModifier::from_args(&args);
167
168        assert_eq!(modifier, LocationModifier::Exact);
169        assert_eq!(path, "/exact");
170    }
171
172    #[test]
173    fn test_location_modifier_prefix_priority() {
174        let args = vec!["^~".to_string(), "/static/".to_string()];
175        let (modifier, path) = LocationModifier::from_args(&args);
176
177        assert_eq!(modifier, LocationModifier::PrefixPriority);
178        assert_eq!(path, "/static/");
179    }
180
181    #[test]
182    fn test_location_modifier_regex() {
183        let args = vec!["~".to_string(), r"\.php$".to_string()];
184        let (modifier, path) = LocationModifier::from_args(&args);
185
186        assert_eq!(modifier, LocationModifier::Regex);
187        assert_eq!(path, r"\.php$");
188    }
189
190    #[test]
191    fn test_location_modifier_regex_case_insensitive() {
192        let args = vec!["~*".to_string(), r"\.(jpg|png|gif)$".to_string()];
193        let (modifier, path) = LocationModifier::from_args(&args);
194
195        assert_eq!(modifier, LocationModifier::RegexCaseInsensitive);
196        assert_eq!(path, r"\.(jpg|png|gif)$");
197    }
198
199    #[test]
200    fn test_location_modifier_empty_args() {
201        let args: Vec<String> = vec![];
202        let (modifier, path) = LocationModifier::from_args(&args);
203
204        assert_eq!(modifier, LocationModifier::None);
205        assert_eq!(path, "/");
206    }
207
208    #[test]
209    fn test_location_modifier_exact_no_path() {
210        let args = vec!["=".to_string()];
211        let (modifier, path) = LocationModifier::from_args(&args);
212
213        assert_eq!(modifier, LocationModifier::Exact);
214        assert_eq!(path, "/");
215    }
216
217    #[test]
218    fn test_location_modifier_regex_no_pattern() {
219        let args = vec!["~".to_string()];
220        let (modifier, path) = LocationModifier::from_args(&args);
221
222        assert_eq!(modifier, LocationModifier::Regex);
223        assert_eq!(path, "");
224    }
225
226    #[test]
227    fn test_location_with_root() {
228        let mut location = Location::new("/images", LocationModifier::None);
229        location.root = Some(PathBuf::from("/var/www/images"));
230
231        assert_eq!(location.root, Some(PathBuf::from("/var/www/images")));
232        assert!(location.is_static());
233    }
234
235    #[test]
236    fn test_location_with_proxy_pass() {
237        let mut location = Location::new("/api/v1", LocationModifier::None);
238        location.proxy_pass = Some("http://api-backend:3000".to_string());
239
240        assert_eq!(
241            location.proxy_pass,
242            Some("http://api-backend:3000".to_string())
243        );
244        assert!(location.is_proxy());
245    }
246
247    #[test]
248    fn test_location_modifiers_equality() {
249        assert_eq!(LocationModifier::None, LocationModifier::None);
250        assert_eq!(LocationModifier::Exact, LocationModifier::Exact);
251        assert_ne!(LocationModifier::None, LocationModifier::Exact);
252        assert_ne!(
253            LocationModifier::Regex,
254            LocationModifier::RegexCaseInsensitive
255        );
256    }
257}