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        };
51        
52        tusks_module.extract_module_items(items, is_root)?;
53
54        tusks_module.validate_is_root_or_has_parent(is_tusks_root, is_root)?;
55        
56        Ok(Some(tusks_module))
57    }
58
59    fn validate_is_root_or_has_parent(&self, is_tusks_root: bool, is_root: bool) -> syn::Result<()> {
60        if !is_root {
61            return Ok(());
62        }
63
64        if !is_tusks_root {
65            if !self.external_parent.is_some() {
66                return Err(syn::Error::new_spanned(
67                    &self.name,
68                    "A tusks module must either be root \
69                        or must declare a parent via `pub use path::to::parent::module as parent_`."
70                ));
71            }
72        }
73        else {
74            if let Some(parent) = &self.external_parent {
75                let mut err = syn::Error::new_spanned(
76                    &self.name,
77                    "A tusks module must either be root or declare a parent but not both."
78                );
79                err.combine(syn::Error::new_spanned(&parent.alias, "Parent is declared here."));
80                return Err(err);
81            }
82        }
83
84        return Ok(());
85    }
86    
87    /// Extract all relevant items from a module
88    fn extract_module_items(&mut self, items: Vec<syn::Item>, is_root: bool) -> syn::Result<()> {
89        let mut has_default_tusk = false;
90        for item in items {
91            match item {
92                syn::Item::Struct(item_struct) => {
93                    self.parse_struct(item_struct.clone())?;
94                }
95
96                syn::Item::Fn(item_fn) => {
97                    if let Some(tusk) = Tusk::from_fn(
98                        item_fn.clone(),
99                        has_default_tusk,
100                        self.allow_external_subcommands
101                    )? {
102                        has_default_tusk = has_default_tusk || tusk.is_default;
103                        self.tusks.push(tusk);
104                    }
105                }
106                
107                syn::Item::Mod(item_mod) => {
108                    if let Some(module) = Self::from_module(item_mod.clone(), false, false)? {
109                        self.submodules.push(module);
110                    }
111                }
112                
113                syn::Item::Use(item_use) => {
114                    // Only consider pub use
115                    if matches!(item_use.vis, syn::Visibility::Public(_)) {
116                        // Extract external modules
117                        self.extract_external_modules(&item_use.tree, &item_use, is_root);
118                    }
119                }
120                
121                _ => {
122                    // Ignore other items
123                }
124            }
125        }
126        Ok(())
127    }
128    
129    /// Parse a struct and check if it's a parameters struct
130    fn parse_struct(&mut self, item_struct: ItemStruct) -> syn::Result<()> {
131        if let Some(params) = TusksParameters::from_struct(item_struct)? {
132            self.parameters = Some(params);
133        }
134        Ok(())
135    }
136    
137/// Extract external module names from a use tree
138fn extract_external_modules(
139    &mut self,
140    tree: &syn::UseTree,
141    item_use: &syn::ItemUse,
142    is_root: bool
143) {
144    match tree {
145        syn::UseTree::Path(use_path) => {
146            // use foo::<rest>
147            self.extract_external_modules(&use_path.tree, item_use, is_root);
148        }
149        syn::UseTree::Name(use_name) => {
150            // Check if it's parent_
151            if use_name.ident == "parent_" && is_root {
152                self.external_parent = Some(ExternalModule {
153                    alias: use_name.ident.clone(),
154                    item_use: item_use.clone(),
155                });
156            } else {
157                self.external_modules.push(ExternalModule {
158                    alias: use_name.ident.clone(),
159                    item_use: item_use.clone(),
160                });
161            }
162        }
163        syn::UseTree::Rename(use_rename) => {
164            // Check if this is the reference to the parent module
165            if use_rename.rename == "parent_" {
166                self.external_parent = Some(ExternalModule {
167                    alias: use_rename.rename.clone(),
168                    item_use: item_use.clone(),
169                });
170            } else {
171                self.external_modules.push(ExternalModule {
172                    alias: use_rename.rename.clone(),
173                    item_use: item_use.clone(),
174                });
175            }
176        }
177        syn::UseTree::Glob(_) => {
178            // use foo::* => ignore
179        }
180        syn::UseTree::Group(use_group) => {
181            // e.g. use foo::{bar, baz};
182            for item in &use_group.items {
183                self.extract_external_modules(item, item_use, is_root);
184            }
185        }
186    }
187}
188}