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