Skip to main content

tusks_lib/parsing/
module.rs

1use crate::{models::{Attributes, ExternalModule, Tusk, TusksModule, TusksParameters}, parsing::util::get_attribute_value::AttributeValue};
2use syn::spanned::Spanned;
3use crate::parsing::util::attr::AttributeCheck;
4
5use syn::{ItemMod, ItemStruct};
6
7impl TusksModule {
8    /// Parses a syn::ItemMod into a TusksModule
9    pub fn from_module(module: ItemMod, is_tusks_root: bool, is_root: bool) -> syn::Result<Option<Self>> {
10        let allow_external_subcommands = module.get_attribute_bool(
11            "command",
12            "allow_external_subcommands"
13        );
14
15        let name = module.ident.clone();
16        let span = module.span();
17
18        // Validate that the module is public
19        if !matches!(module.vis, syn::Visibility::Public(_)) {
20            if module.has_attr("tusks") {
21                return Err(syn::Error::new_spanned(&name, "tusks module must be public"));
22            }
23            return Ok(None);
24        }
25
26        if module.has_attr("skip") {
27            return Ok(None);
28        }
29        
30        // Check if module has content (inline module)
31        let items = match module.content {
32            Some(content) => content.1, // content is (brace_token, Vec<Item>)
33            None => {
34                return Err(syn::Error::new(
35                    span,
36                    "tusks module must be inline (not a file reference)"
37                ));
38            }
39        };
40
41        let mut tusks_module = TusksModule {
42            name,
43            attrs: Attributes(module.attrs),
44            external_parent: None,
45            parameters: None,
46            tusks: Vec::new(),
47            submodules: Vec::new(),
48            external_modules: Vec::new(),
49            allow_external_subcommands,
50            value_enums: Vec::new(),
51        };
52        
53        tusks_module.extract_module_items(items, is_root)?;
54
55        tusks_module.validate_is_root_or_has_parent(is_tusks_root, is_root)?;
56        
57        Ok(Some(tusks_module))
58    }
59
60    fn validate_is_root_or_has_parent(&self, is_tusks_root: bool, is_root: bool) -> syn::Result<()> {
61        if !is_root {
62            return Ok(());
63        }
64
65        if !is_tusks_root {
66            if self.external_parent.is_none() {
67                return Err(syn::Error::new_spanned(
68                    &self.name,
69                    "A tusks module must either be root \
70                        or must declare a parent via `pub use path::to::parent::module as parent_`."
71                ));
72            }
73        }
74        else {
75            if let Some(parent) = &self.external_parent {
76                let mut err = syn::Error::new_spanned(
77                    &self.name,
78                    "A tusks module must either be root or declare a parent but not both."
79                );
80                err.combine(syn::Error::new_spanned(&parent.alias, "Parent is declared here."));
81                return Err(err);
82            }
83        }
84
85        Ok(())
86    }
87    
88    /// Extract all relevant items from a module
89    fn extract_module_items(&mut self, items: Vec<syn::Item>, is_root: bool) -> syn::Result<()> {
90        let mut has_default_tusk = false;
91        for item in items {
92            match item {
93                syn::Item::Struct(item_struct) => {
94                    self.parse_struct(item_struct.clone())?;
95                }
96
97                syn::Item::Fn(item_fn) => {
98                    if let Some(tusk) = Tusk::from_fn(
99                        item_fn.clone(),
100                        has_default_tusk,
101                        self.allow_external_subcommands
102                    )? {
103                        has_default_tusk = has_default_tusk || tusk.is_default;
104                        self.tusks.push(tusk);
105                    }
106                }
107                
108                syn::Item::Mod(item_mod) => {
109                    if let Some(module) = Self::from_module(item_mod.clone(), false, false)? {
110                        self.submodules.push(module);
111                    }
112                }
113                
114                syn::Item::Use(item_use) => {
115                    // Only consider pub use
116                    if matches!(item_use.vis, syn::Visibility::Public(_)) {
117                        // Extract external modules
118                        self.extract_external_modules(&item_use.tree, &item_use, is_root);
119                    }
120                }
121                
122                syn::Item::Enum(item_enum) => {
123                    if matches!(item_enum.vis, syn::Visibility::Public(_)) {
124                        self.value_enums.push(item_enum);
125                    }
126                }
127
128                _ => {
129                    // Ignore other items
130                }
131            }
132        }
133        Ok(())
134    }
135    
136    /// Parse a struct and check if it's a parameters struct
137    fn parse_struct(&mut self, item_struct: ItemStruct) -> syn::Result<()> {
138        if let Some(params) = TusksParameters::from_struct(item_struct)? {
139            self.parameters = Some(params);
140        }
141        Ok(())
142    }
143    
144/// Extract external module names from a use tree
145fn extract_external_modules(
146    &mut self,
147    tree: &syn::UseTree,
148    item_use: &syn::ItemUse,
149    is_root: bool
150) {
151    match tree {
152        syn::UseTree::Path(use_path) => {
153            // use foo::<rest>
154            self.extract_external_modules(&use_path.tree, item_use, is_root);
155        }
156        syn::UseTree::Name(use_name) => {
157            // Check if it's parent_
158            if use_name.ident == "parent_" && is_root {
159                self.external_parent = Some(ExternalModule {
160                    alias: use_name.ident.clone(),
161                    item_use: item_use.clone(),
162                });
163            } else {
164                self.external_modules.push(ExternalModule {
165                    alias: use_name.ident.clone(),
166                    item_use: item_use.clone(),
167                });
168            }
169        }
170        syn::UseTree::Rename(use_rename) => {
171            // Check if this is the reference to the parent module
172            if use_rename.rename == "parent_" {
173                self.external_parent = Some(ExternalModule {
174                    alias: use_rename.rename.clone(),
175                    item_use: item_use.clone(),
176                });
177            } else {
178                self.external_modules.push(ExternalModule {
179                    alias: use_rename.rename.clone(),
180                    item_use: item_use.clone(),
181                });
182            }
183        }
184        syn::UseTree::Glob(_) => {
185            // use foo::* => ignore
186        }
187        syn::UseTree::Group(use_group) => {
188            // e.g. use foo::{bar, baz};
189            for item in &use_group.items {
190                self.extract_external_modules(item, item_use, is_root);
191            }
192        }
193    }
194}
195}