kinetics_parser/
parser.rs

1use std::path::PathBuf;
2
3use crate::{environment::Environment, Cron, Endpoint, Worker};
4use color_eyre::eyre;
5use syn::{parse::Parse, visit::Visit, Attribute, ItemFn};
6use walkdir::WalkDir;
7
8/// Represents a function in the source code
9#[derive(Debug, Clone)]
10pub struct ParsedFunction {
11    /// Name of the function, parsed from the function definition
12    pub rust_function_name: String,
13
14    /// Path to the file where function is defined
15    pub relative_path: String,
16
17    /// Parsed from kinetics_macro macro definition
18    pub role: Role,
19}
20
21impl ParsedFunction {
22    /// Generate lambda function name out of Rust function name or macro attribute
23    ///
24    /// By default use the Rust function plus crate path as the function name. Convert
25    /// some-name to SomeName, and do other transformations in order to comply with Lambda
26    /// function name requirements.
27    pub fn func_name(&self, is_local: bool) -> String {
28        let rust_name = &self.rust_function_name;
29        let full_path = format!("{}/{rust_name}", self.relative_path);
30
31        let default_func_name = full_path
32            .as_str()
33            .replace("_", "Undrscr")
34            .replace("_", "Dash")
35            .split(&['.', '/'])
36            .filter(|s| !s.eq(&"rs"))
37            .map(|s| match s.chars().next() {
38                Some(first) => first.to_uppercase().collect::<String>() + &s[1..],
39                None => String::new(),
40            })
41            .collect::<String>()
42            .replacen("Src", "", 1);
43
44        // TODO Check the name for uniqueness
45        format!(
46            "{}{}",
47            self.role.name().unwrap_or(&default_func_name),
48            if is_local { "Local" } else { "" }
49        )
50    }
51}
52
53#[derive(Debug, Clone)]
54pub enum Role {
55    Endpoint(Endpoint),
56    Cron(Cron),
57    Worker(Worker),
58}
59
60impl Role {
61    pub fn name(&self) -> Option<&String> {
62        match self {
63            Role::Endpoint(params) => params.name.as_ref(),
64            Role::Cron(params) => params.name.as_ref(),
65            Role::Worker(params) => params.name.as_ref(),
66        }
67    }
68
69    pub fn environment(&self) -> &Environment {
70        match self {
71            Role::Endpoint(params) => &params.environment,
72            Role::Cron(params) => &params.environment,
73            Role::Worker(params) => &params.environment,
74        }
75    }
76}
77
78#[derive(Debug, Default)]
79pub struct Parser {
80    /// All found functions in the source code
81    pub functions: Vec<ParsedFunction>,
82
83    /// Relative path to currently processing file
84    pub relative_path: String,
85}
86
87impl Parser {
88    /// Init new Parser
89    ///
90    /// And optionally parse the requested dir
91    pub fn new(path: Option<&PathBuf>) -> eyre::Result<Self> {
92        let mut parser: Parser = Default::default();
93
94        if path.is_some() {
95            parser.walk_dir(path.unwrap())?;
96        }
97
98        Ok(parser)
99    }
100
101    pub fn walk_dir(&mut self, path: &PathBuf) -> eyre::Result<()> {
102        for entry in WalkDir::new(path)
103            .into_iter()
104            .filter_map(|e| e.ok())
105            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
106        {
107            let content = std::fs::read_to_string(entry.path())?;
108            let syntax = syn::parse_file(&content)?;
109
110            // Set current file relative path for further imports resolution
111            // WARN It prevents to implement parallel parsing of files and requires rework in the future
112            self.set_relative_path(entry.path().strip_prefix(path)?.to_str());
113
114            self.visit_file(&syntax);
115        }
116
117        Ok(())
118    }
119
120    pub fn set_relative_path(&mut self, file_path: Option<&str>) {
121        self.relative_path = file_path.map_or_else(|| "".to_string(), |s| s.to_string());
122    }
123
124    fn parse_endpoint(&mut self, attr: &Attribute) -> syn::Result<Endpoint> {
125        attr.parse_args_with(Endpoint::parse)
126    }
127
128    fn parse_worker(&mut self, attr: &Attribute) -> syn::Result<Worker> {
129        attr.parse_args_with(Worker::parse)
130    }
131
132    fn parse_cron(&mut self, attr: &Attribute) -> syn::Result<Cron> {
133        attr.parse_args_with(Cron::parse)
134    }
135
136    /// Checks if the input is a valid kinetics_macro definition and returns its role
137    /// Checks if the input is a valid kinetics_macro definition
138    /// Known definitions:
139    /// kinetics_macro::<role> or <role>
140    fn parse_attr_role(&self, input: &Attribute) -> String {
141        let path = input.path();
142
143        if path.segments.len() == 1 {
144            let ident = &path.segments[0].ident;
145            return ident.to_string();
146        }
147
148        if path.segments.len() == 2 && &path.segments[0].ident == "kinetics_macro" {
149            let ident = &path.segments[1].ident;
150            return ident.to_string();
151        }
152
153        "".to_string()
154    }
155}
156
157impl Visit<'_> for Parser {
158    /// Visits function definitions
159    fn visit_item_fn(&mut self, item: &ItemFn) {
160        for attr in &item.attrs {
161            // Skip non-endpoint or non-worker attributes
162            let role = match self.parse_attr_role(attr).as_str() {
163                "endpoint" => {
164                    let params = self.parse_endpoint(attr).unwrap();
165                    Role::Endpoint(params)
166                }
167                "worker" => {
168                    let params = self.parse_worker(attr).unwrap();
169                    Role::Worker(params)
170                }
171                "cron" => {
172                    let params = self.parse_cron(attr).unwrap();
173                    Role::Cron(params)
174                }
175                _ => continue,
176            };
177
178            self.functions.push(ParsedFunction {
179                role,
180                rust_function_name: item.sig.ident.to_string(),
181                relative_path: self.relative_path.clone(),
182            });
183        }
184
185        // We don't need to parse the function body (in case nested functions), so just exit here
186    }
187}