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