Skip to main content

tusks_lib/parsing/attribute/
parse.rs

1use syn::{Ident, LitBool, LitInt, LitStr, Token, parenthesized, parse::{Parse, ParseStream}};
2
3use crate::parsing::attribute::models::{TasksConfig, TusksAttr};
4
5impl Parse for TusksAttr {
6    /// Parses the `#[tusks(...)]` attribute and extracts all configuration options.
7    /// 
8    /// Supports the following syntax:
9    /// - Boolean flags: `debug`, `root`, `derive_debug_for_parameters`
10    ///   - Can be specified as just the flag name (implies `true`)
11    ///   - Or with explicit value: `debug = true` or `debug = false`
12    /// - Nested configuration: `tasks(max_groupsize=5, max_depth=20, separator=".")`
13    /// 
14    /// # Example
15    /// ```ignore
16    /// #[tusks(root, debug, tasks(max_groupsize=10, separator="/"))]
17    /// ```
18    /// 
19    /// # Errors
20    /// Returns an error if:
21    /// - An unknown attribute name is encountered
22    /// - The syntax is malformed (missing commas, invalid values, etc.)
23    fn parse(input: ParseStream) -> syn::Result<Self> {
24        let mut attr = TusksAttr::default();
25        
26        while !input.is_empty() {
27            let ident: Ident = input.parse()?;
28            
29            match ident.to_string().as_str() {
30                "debug" => attr.debug = parse_bool_flag(input)?,
31                "root" => attr.root = parse_bool_flag(input)?,
32                "derive_debug_for_parameters" => {
33                    attr.derive_debug_for_parameters = parse_bool_flag(input)?
34                },
35                "tasks" => {
36                    attr.tasks = Some(parse_optional_nested_config::<TasksConfig>(input)?);
37                },
38                other => return Err(unknown_attribute_error(&ident, other)),
39            }
40            
41            parse_trailing_comma(input)?;
42        }
43        
44        Ok(attr)
45    }
46}
47
48/// Parse an optional nested configuration:
49/// - `ident(...)` → parses `T`
50/// - `ident`      → returns `T::default()`
51fn parse_optional_nested_config<T>(input: ParseStream) -> syn::Result<T>
52where
53    T: Parse + Default,
54{
55    if input.peek(syn::token::Paren) {
56        parse_nested_config(input)
57    } else {
58        Ok(T::default())
59    }
60}
61
62impl Parse for TasksConfig {
63    /// Parses the task configuration parameters inside `tasks(...)`.
64    /// 
65    /// All parameters are optional and will use default values if not specified:
66    /// - `max_groupsize`: defaults to 5
67    /// - `max_depth`: defaults to 20
68    /// - `separator`: defaults to "."
69    /// 
70    /// # Example
71    /// ```ignore
72    /// tasks(max_groupsize=10, separator="/")
73    /// // Results in: max_groupsize=10, max_depth=20 (default), separator="/"
74    /// ```
75    /// 
76    /// # Errors
77    /// Returns an error if:
78    /// - An unknown parameter name is encountered
79    /// - A parameter value has the wrong type (e.g., string instead of integer)
80    /// - The syntax is malformed (missing `=`, invalid literals, etc.)
81    fn parse(input: ParseStream) -> syn::Result<Self> {
82        let mut config = TasksConfig::default();
83        
84        while !input.is_empty() {
85            let ident: Ident = input.parse()?;
86            
87            match ident.to_string().as_str() {
88                "max_groupsize" => config.max_groupsize = parse_required_value(input, parse_usize)?,
89                "max_depth" => config.max_depth = parse_required_value(input, parse_usize)?,
90                "separator" => config.separator = parse_required_value(input, parse_string)?,
91                "use_colors" => config.use_colors = parse_bool_flag(input)?,
92                other => return Err(unknown_parameter_error(&ident, other)),
93            }
94            
95            parse_trailing_comma(input)?;
96        }
97        
98        Ok(config)
99    }
100}
101
102// Helper functions
103
104/// Parse an optional boolean flag that can be either `flag` or `flag = true/false`
105fn parse_bool_flag(input: ParseStream) -> syn::Result<bool> {
106    if input.peek(Token![=]) {
107        input.parse::<Token![=]>()?;
108        let value: LitBool = input.parse()?;
109        Ok(value.value)
110    } else {
111        Ok(true)
112    }
113}
114
115/// Parse a required parameter value: `= <value>`
116fn parse_required_value<T, F>(input: ParseStream, parser: F) -> syn::Result<T>
117where
118    F: FnOnce(ParseStream) -> syn::Result<T>,
119{
120    input.parse::<Token![=]>()?;
121    parser(input)
122}
123
124/// Parse a nested configuration like `tasks(...)`
125fn parse_nested_config<T: Parse>(input: ParseStream) -> syn::Result<T> {
126    let content;
127    parenthesized!(content in input);
128    content.parse::<T>()
129}
130
131/// Parse a trailing comma if present
132fn parse_trailing_comma(input: ParseStream) -> syn::Result<()> {
133    if !input.is_empty() {
134        input.parse::<Token![,]>()?;
135    }
136    Ok(())
137}
138
139/// Parse a usize literal
140fn parse_usize(input: ParseStream) -> syn::Result<usize> {
141    let value: LitInt = input.parse()?;
142    value.base10_parse()
143}
144
145/// Parse a string literal
146fn parse_string(input: ParseStream) -> syn::Result<String> {
147    let value: LitStr = input.parse()?;
148    Ok(value.value())
149}
150
151/// Create error for unknown attribute
152fn unknown_attribute_error(ident: &Ident, name: &str) -> syn::Error {
153    syn::Error::new(
154        ident.span(),
155        format!("unknown tusks attribute: {}", name)
156    )
157}
158
159/// Create error for unknown parameter
160fn unknown_parameter_error(ident: &Ident, name: &str) -> syn::Error {
161    syn::Error::new(
162        ident.span(),
163        format!("unknown tasks parameter: {}", name)
164    )
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    fn parse_tusks_attr(input: &str) -> syn::Result<TusksAttr> {
172        syn::parse_str::<TusksAttr>(input)
173    }
174
175    fn parse_tasks_config(input: &str) -> syn::Result<TasksConfig> {
176        syn::parse_str::<TasksConfig>(input)
177    }
178
179    // --- TusksAttr parsing ---
180
181    #[test]
182    fn empty_attr() {
183        let attr = parse_tusks_attr("").unwrap();
184        assert!(!attr.debug);
185        assert!(!attr.root);
186        assert!(!attr.derive_debug_for_parameters);
187        assert!(attr.tasks.is_none());
188    }
189
190    #[test]
191    fn root_flag() {
192        let attr = parse_tusks_attr("root").unwrap();
193        assert!(attr.root);
194        assert!(!attr.debug);
195    }
196
197    #[test]
198    fn debug_flag() {
199        let attr = parse_tusks_attr("debug").unwrap();
200        assert!(attr.debug);
201    }
202
203    #[test]
204    fn derive_debug_flag() {
205        let attr = parse_tusks_attr("derive_debug_for_parameters").unwrap();
206        assert!(attr.derive_debug_for_parameters);
207    }
208
209    #[test]
210    fn explicit_bool_true() {
211        let attr = parse_tusks_attr("root = true").unwrap();
212        assert!(attr.root);
213    }
214
215    #[test]
216    fn explicit_bool_false() {
217        let attr = parse_tusks_attr("root = false").unwrap();
218        assert!(!attr.root);
219    }
220
221    #[test]
222    fn multiple_flags() {
223        let attr = parse_tusks_attr("root, debug").unwrap();
224        assert!(attr.root);
225        assert!(attr.debug);
226    }
227
228    #[test]
229    fn all_flags() {
230        let attr = parse_tusks_attr("root, debug, derive_debug_for_parameters").unwrap();
231        assert!(attr.root);
232        assert!(attr.debug);
233        assert!(attr.derive_debug_for_parameters);
234    }
235
236    #[test]
237    fn unknown_attribute_errors() {
238        let err = parse_tusks_attr("unknown").unwrap_err();
239        assert!(err.to_string().contains("unknown tusks attribute"));
240    }
241
242    #[test]
243    fn tasks_without_parens_uses_defaults() {
244        let attr = parse_tusks_attr("root, tasks").unwrap();
245        assert!(attr.root);
246        let tasks = attr.tasks.unwrap();
247        assert_eq!(tasks.max_groupsize, 5);
248        assert_eq!(tasks.max_depth, 20);
249        assert_eq!(tasks.separator, ".");
250        assert!(tasks.use_colors);
251    }
252
253    #[test]
254    fn tasks_with_custom_config() {
255        let attr = parse_tusks_attr(
256            "root, tasks(max_groupsize = 10, separator = \"/\", max_depth = 3)"
257        ).unwrap();
258        let tasks = attr.tasks.unwrap();
259        assert_eq!(tasks.max_groupsize, 10);
260        assert_eq!(tasks.max_depth, 3);
261        assert_eq!(tasks.separator, "/");
262    }
263
264    #[test]
265    fn tasks_use_colors_false() {
266        let attr = parse_tusks_attr("tasks(use_colors = false)").unwrap();
267        let tasks = attr.tasks.unwrap();
268        assert!(!tasks.use_colors);
269    }
270
271    #[test]
272    fn tasks_use_colors_flag_only() {
273        let attr = parse_tusks_attr("tasks(use_colors)").unwrap();
274        let tasks = attr.tasks.unwrap();
275        assert!(tasks.use_colors);
276    }
277
278    // --- TasksConfig parsing ---
279
280    #[test]
281    fn tasks_config_defaults() {
282        let config = parse_tasks_config("").unwrap();
283        assert_eq!(config.max_groupsize, 5);
284        assert_eq!(config.max_depth, 20);
285        assert_eq!(config.separator, ".");
286        assert!(config.use_colors);
287    }
288
289    #[test]
290    fn tasks_config_partial_override() {
291        let config = parse_tasks_config("max_groupsize = 3").unwrap();
292        assert_eq!(config.max_groupsize, 3);
293        assert_eq!(config.max_depth, 20); // default
294    }
295
296    #[test]
297    fn tasks_config_unknown_parameter_errors() {
298        let err = parse_tasks_config("unknown = 5").unwrap_err();
299        assert!(err.to_string().contains("unknown tasks parameter"));
300    }
301
302    #[test]
303    fn tasks_config_custom_separator() {
304        let config = parse_tasks_config("separator = \"::\"").unwrap();
305        assert_eq!(config.separator, "::");
306    }
307}