Skip to main content

nginx_discovery/ast/
directive.rs

1//! Directive AST nodes
2
3use super::{Span, Spanned, Value};
4
5/// A directive in the NGINX configuration
6#[derive(Debug, Clone, PartialEq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct Directive {
9    /// The directive content (simple or block)
10    pub item: DirectiveItem,
11    /// Source location
12    pub span: Span,
13}
14
15/// Directive content - either simple or block
16#[derive(Debug, Clone, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum DirectiveItem {
19    /// Simple directive: `name arg1 arg2 ...;`
20    Simple {
21        /// Directive name
22        name: String,
23        /// Arguments
24        args: Vec<Value>,
25    },
26    /// Block directive: `name arg1 arg2 { ... }`
27    Block {
28        /// Directive name
29        name: String,
30        /// Arguments before the block
31        args: Vec<Value>,
32        /// Child directives
33        children: Vec<Directive>,
34    },
35}
36
37impl Directive {
38    /// Create a new simple directive
39    pub fn simple(name: impl Into<String>, args: Vec<String>) -> Self {
40        Self {
41            item: DirectiveItem::Simple {
42                name: name.into(),
43                args: args.into_iter().map(Value::from).collect(),
44            },
45            span: Span::default(),
46        }
47    }
48
49    /// Create a new simple directive with span
50    pub fn simple_with_span(name: impl Into<String>, args: Vec<String>, span: Span) -> Self {
51        Self {
52            item: DirectiveItem::Simple {
53                name: name.into(),
54                args: args.into_iter().map(Value::from).collect(),
55            },
56            span,
57        }
58    }
59
60    /// Create a new simple directive with values
61    pub fn simple_with_values(name: impl Into<String>, args: Vec<Value>) -> Self {
62        Self {
63            item: DirectiveItem::Simple {
64                name: name.into(),
65                args,
66            },
67            span: Span::default(),
68        }
69    }
70
71    /// Create a new block directive
72    pub fn block(name: impl Into<String>, args: Vec<String>, children: Vec<Directive>) -> Self {
73        Self {
74            item: DirectiveItem::Block {
75                name: name.into(),
76                args: args.into_iter().map(Value::from).collect(),
77                children,
78            },
79            span: Span::default(),
80        }
81    }
82
83    /// Create a new block directive with span
84    pub fn block_with_span(
85        name: impl Into<String>,
86        args: Vec<String>,
87        children: Vec<Directive>,
88        span: Span,
89    ) -> Self {
90        Self {
91            item: DirectiveItem::Block {
92                name: name.into(),
93                args: args.into_iter().map(Value::from).collect(),
94                children,
95            },
96            span,
97        }
98    }
99
100    /// Create a new block directive with values
101    pub fn block_with_values(
102        name: impl Into<String>,
103        args: Vec<Value>,
104        children: Vec<Directive>,
105    ) -> Self {
106        Self {
107            item: DirectiveItem::Block {
108                name: name.into(),
109                args,
110                children,
111            },
112            span: Span::default(),
113        }
114    }
115
116    /// Get the directive name
117    #[must_use]
118    pub fn name(&self) -> &str {
119        match &self.item {
120            DirectiveItem::Simple { name, .. } | DirectiveItem::Block { name, .. } => name,
121        }
122    }
123
124    /// Get the directive arguments
125    #[must_use]
126    pub fn args(&self) -> &[Value] {
127        match &self.item {
128            DirectiveItem::Simple { args, .. } | DirectiveItem::Block { args, .. } => args,
129        }
130    }
131
132    /// Get children if this is a block directive
133    #[must_use]
134    pub fn children(&self) -> Option<&[Directive]> {
135        match &self.item {
136            DirectiveItem::Block { children, .. } => Some(children),
137            DirectiveItem::Simple { .. } => None,
138        }
139    }
140
141    /// Get mutable children if this is a block directive
142    pub fn children_mut(&mut self) -> Option<&mut Vec<Directive>> {
143        match &mut self.item {
144            DirectiveItem::Block { children, .. } => Some(children),
145            DirectiveItem::Simple { .. } => None,
146        }
147    }
148
149    /// Check if this is a block directive
150    #[must_use]
151    pub fn is_block(&self) -> bool {
152        matches!(self.item, DirectiveItem::Block { .. })
153    }
154
155    /// Check if this is a simple directive
156    #[must_use]
157    pub fn is_simple(&self) -> bool {
158        matches!(self.item, DirectiveItem::Simple { .. })
159    }
160
161    /// Get the first argument as a string, if it exists
162    #[must_use]
163    pub fn first_arg(&self) -> Option<String> {
164        self.args().first().map(std::string::ToString::to_string)
165    }
166
167    /// Get all arguments as strings
168    #[must_use]
169    pub fn args_as_strings(&self) -> Vec<String> {
170        self.args()
171            .iter()
172            .map(std::string::ToString::to_string)
173            .collect()
174    }
175
176    /// Find child directives with a specific name
177    #[must_use]
178    pub fn find_children(&self, name: &str) -> Vec<&Directive> {
179        match &self.item {
180            DirectiveItem::Block { children, .. } => {
181                children.iter().filter(|d| d.name() == name).collect()
182            }
183            DirectiveItem::Simple { .. } => Vec::new(),
184        }
185    }
186
187    /// Find child directives with a specific name (mutable)
188    pub fn find_children_mut(&mut self, name: &str) -> Vec<&mut Directive> {
189        match &mut self.item {
190            DirectiveItem::Block { children, .. } => {
191                children.iter_mut().filter(|d| d.name() == name).collect()
192            }
193            DirectiveItem::Simple { .. } => Vec::new(),
194        }
195    }
196
197    /// Recursively find all directives with a given name
198    #[must_use]
199    pub fn find_recursive(&self, name: &str) -> Vec<&Directive> {
200        let mut result = Vec::new();
201        self.find_recursive_impl(name, &mut result);
202        result
203    }
204
205    fn find_recursive_impl<'a>(&'a self, name: &str, result: &mut Vec<&'a Directive>) {
206        if self.name() == name {
207            result.push(self);
208        }
209        if let Some(children) = self.children() {
210            for child in children {
211                child.find_recursive_impl(name, result);
212            }
213        }
214    }
215}
216
217impl Spanned for Directive {
218    fn span(&self) -> Span {
219        self.span
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_simple_directive() {
229        let directive = Directive::simple("user", vec!["nginx".to_string()]);
230        assert_eq!(directive.name(), "user");
231        assert_eq!(directive.args().len(), 1);
232        assert!(directive.is_simple());
233        assert!(!directive.is_block());
234        assert_eq!(directive.first_arg(), Some("nginx".to_string()));
235    }
236
237    #[test]
238    fn test_simple_directive_with_span() {
239        let span = Span::new(0, 10, 1, 1);
240        let directive = Directive::simple_with_span("listen", vec!["80".to_string()], span);
241        assert_eq!(directive.span, span);
242        assert_eq!(directive.name(), "listen");
243    }
244
245    #[test]
246    fn test_block_directive() {
247        let directive = Directive::block("server", vec![], vec![]);
248        assert_eq!(directive.name(), "server");
249        assert!(directive.is_block());
250        assert!(!directive.is_simple());
251        assert_eq!(directive.children().unwrap().len(), 0);
252    }
253
254    #[test]
255    fn test_block_with_children() {
256        let children = vec![
257            Directive::simple("listen", vec!["80".to_string()]),
258            Directive::simple("server_name", vec!["example.com".to_string()]),
259        ];
260        let server = Directive::block("server", vec![], children);
261
262        assert_eq!(server.children().unwrap().len(), 2);
263        assert_eq!(server.children().unwrap()[0].name(), "listen");
264    }
265
266    #[test]
267    fn test_find_children() {
268        let children = vec![
269            Directive::simple("listen", vec!["80".to_string()]),
270            Directive::simple("server_name", vec!["example.com".to_string()]),
271            Directive::simple("listen", vec!["443".to_string()]),
272        ];
273        let server = Directive::block("server", vec![], children);
274
275        let listen_dirs = server.find_children("listen");
276        assert_eq!(listen_dirs.len(), 2);
277        assert_eq!(listen_dirs[0].first_arg(), Some("80".to_string()));
278        assert_eq!(listen_dirs[1].first_arg(), Some("443".to_string()));
279    }
280
281    #[test]
282    fn test_args_as_strings() {
283        let directive = Directive::simple(
284            "server_name",
285            vec!["example.com".to_string(), "www.example.com".to_string()],
286        );
287        let args = directive.args_as_strings();
288        assert_eq!(args, vec!["example.com", "www.example.com"]);
289    }
290
291    #[test]
292    fn test_find_recursive() {
293        // Build nested structure:
294        // http {
295        //   server {
296        //     location / {
297        //       access_log /var/log/1.log;
298        //     }
299        //   }
300        //   access_log /var/log/2.log;
301        // }
302        let location = Directive::block(
303            "location",
304            vec!["/".to_string()],
305            vec![Directive::simple(
306                "access_log",
307                vec!["/var/log/1.log".to_string()],
308            )],
309        );
310        let server = Directive::block("server", vec![], vec![location]);
311        let http = Directive::block(
312            "http",
313            vec![],
314            vec![
315                server,
316                Directive::simple("access_log", vec!["/var/log/2.log".to_string()]),
317            ],
318        );
319
320        let access_logs = http.find_recursive("access_log");
321        assert_eq!(access_logs.len(), 2);
322    }
323
324    #[test]
325    fn test_simple_directive_with_values() {
326        let args = vec![Value::literal("combined"), Value::variable("remote_addr")];
327        let directive = Directive::simple_with_values("log_format", args);
328
329        assert_eq!(directive.args().len(), 2);
330        assert!(directive.args()[1].is_variable());
331    }
332}