Skip to main content

kinetics_parser/
parser.rs

1use crate::{
2    params::{Cron, Endpoint, Params, Worker},
3    ParsedFunction, Role,
4};
5use color_eyre::eyre;
6use std::path::{Path, PathBuf};
7use syn::{parse::Parse, visit::Visit, Attribute, ItemFn};
8use walkdir::WalkDir;
9
10/// For a workspace represents its member.
11/// For a standalone crate represents the crate itself.
12#[derive(Clone, Debug, Default)]
13pub struct Package {
14    /// Name from Cargo.toml
15    pub name: String,
16
17    /// Relative path to the package from workspace root
18    pub relative_path: PathBuf,
19}
20
21#[derive(Debug, Default)]
22pub struct Parser<'a> {
23    /// All found functions in the source code
24    pub functions: Vec<ParsedFunction>,
25
26    /// Relative path to currently processing file
27    fn_rel_path: PathBuf,
28
29    /// The package being processed
30    pkg: Option<&'a Package>,
31}
32
33impl<'a> Parser<'a> {
34    /// Init new Parser
35    ///
36    /// And optionally parse the requested dir
37    pub fn new(root_path: &Path, pkg: Option<&'a Package>) -> eyre::Result<Self> {
38        let mut parser: Parser = Default::default();
39
40        if let Some(pkg) = pkg {
41            parser.set_pkg(Some(pkg));
42            parser.walk_dir(&root_path.join(&pkg.relative_path))?;
43        }
44
45        Ok(parser)
46    }
47
48    pub fn walk_dir(&mut self, path: &Path) -> eyre::Result<()> {
49        for entry in WalkDir::new(path)
50            .into_iter()
51            .filter_map(|e| e.ok())
52            .filter(|e| {
53                e.path().strip_prefix(path).is_ok_and(|p| p.starts_with("src/")) // only src folder
54                && e.path().extension().is_some_and(|ext| ext == "rs") // only rust files
55            })
56        {
57            let content = std::fs::read_to_string(entry.path())?;
58            let syntax = syn::parse_file(&content)?;
59
60            // Set current file relative path for further imports resolution
61            // WARN It prevents to implement parallel parsing of files and requires rework in the future
62            self.set_fn_rel_path(Some(entry.path().strip_prefix(path)?));
63
64            self.visit_file(&syntax);
65        }
66
67        Ok(())
68    }
69
70    fn set_pkg(&mut self, pkg: Option<&'a Package>) {
71        self.pkg = pkg.to_owned();
72    }
73
74    fn set_fn_rel_path(&mut self, file_path: Option<&Path>) {
75        self.fn_rel_path = file_path.map(PathBuf::from).unwrap_or_default();
76    }
77
78    fn parse_endpoint(&mut self, attr: &Attribute) -> syn::Result<Endpoint> {
79        attr.parse_args_with(Endpoint::parse)
80    }
81
82    fn parse_worker(&mut self, attr: &Attribute) -> syn::Result<Worker> {
83        attr.parse_args_with(Worker::parse)
84    }
85
86    fn parse_cron(&mut self, attr: &Attribute) -> syn::Result<Cron> {
87        attr.parse_args_with(Cron::parse)
88    }
89
90    /// Checks if the input is a valid kinetics_macro definition and returns its role
91    /// Known definitions:
92    /// #[kinetics_macro::<role> or <role>]
93    fn parse_attr_role(&self, input: &Attribute) -> String {
94        let path = input.path();
95
96        if path.segments.len() == 1 {
97            let ident = &path.segments[0].ident;
98            return ident.to_string();
99        }
100
101        if path.segments.len() == 2 && &path.segments[0].ident == "kinetics_macro" {
102            let ident = &path.segments[1].ident;
103            return ident.to_string();
104        }
105
106        "".to_string()
107    }
108}
109
110impl<'a> Visit<'_> for Parser<'a> {
111    /// Visits function definitions
112    fn visit_item_fn(&mut self, item: &ItemFn) {
113        for attr in &item.attrs {
114            // Skip non-endpoint or non-worker attributes
115            let (role, params) = match self.parse_attr_role(attr).as_str() {
116                "endpoint" => {
117                    let params = self.parse_endpoint(attr).unwrap();
118                    (Role::Endpoint, Params::Endpoint(params))
119                }
120                "worker" => {
121                    let params = self.parse_worker(attr).unwrap();
122                    (Role::Worker, Params::Worker(params))
123                }
124                "cron" => {
125                    let params = self.parse_cron(attr).unwrap();
126                    (Role::Cron, Params::Cron(params))
127                }
128                _ => continue,
129            };
130
131            if let Some(pkg) = self.pkg {
132                self.functions.push(ParsedFunction {
133                    role,
134                    params,
135                    rust_function_name: item.sig.ident.to_string(),
136                    relative_path: self.fn_rel_path.clone(),
137                    pkg_rel_path: pkg.relative_path.clone(),
138                    pkg_name: pkg.name.clone(),
139                });
140            }
141        }
142
143        // We don't need to parse the function body (in case nested functions), so just exit here
144    }
145}