Skip to main content

kinetics_parser/
parser.rs

1use crate::{environment::Environment, Cron, Endpoint, Worker};
2use color_eyre::eyre;
3use serde::{Deserialize, Serialize};
4use std::fmt::Display;
5use std::path::PathBuf;
6use syn::{parse::Parse, visit::Visit, Attribute, ItemFn};
7use walkdir::WalkDir;
8
9/// Represents a function in the source code
10#[derive(Debug, Clone)]
11pub struct ParsedFunction {
12    /// Name of the function, parsed from the function definition
13    pub rust_function_name: String,
14
15    /// Path to the file where function is defined
16    pub relative_path: String,
17
18    /// Parsed from kinetics_macro macro definition
19    pub role: Role,
20}
21
22impl ParsedFunction {
23    /// Convert a path to CamelCase name
24    pub fn path_to_name(path: &str) -> String {
25        path.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, Serialize, Deserialize)]
59pub enum Role {
60    Endpoint(Endpoint),
61    Cron(Cron),
62    Worker(Worker),
63}
64
65impl Display for Role {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        let str = match self {
68            Role::Endpoint(_) => "endpoint",
69            Role::Cron(_) => "cron",
70            Role::Worker(_) => "worker",
71        };
72
73        write!(f, "{}", str)
74    }
75}
76
77impl Role {
78    pub fn name(&self) -> Option<&String> {
79        match self {
80            Role::Endpoint(params) => params.name.as_ref(),
81            Role::Cron(params) => params.name.as_ref(),
82            Role::Worker(params) => params.name.as_ref(),
83        }
84    }
85
86    pub fn environment(&self) -> &Environment {
87        match self {
88            Role::Endpoint(params) => &params.environment,
89            Role::Cron(params) => &params.environment,
90            Role::Worker(params) => &params.environment,
91        }
92    }
93}
94
95#[derive(Debug, Default)]
96pub struct Parser {
97    /// All found functions in the source code
98    pub functions: Vec<ParsedFunction>,
99
100    /// Relative path to currently processing file
101    pub relative_path: String,
102}
103
104impl Parser {
105    /// Init new Parser
106    ///
107    /// And optionally parse the requested dir
108    pub fn new(path: Option<&PathBuf>) -> eyre::Result<Self> {
109        let mut parser: Parser = Default::default();
110
111        if let Some(path) = path {
112            parser.walk_dir(path)?;
113        }
114
115        Ok(parser)
116    }
117
118    pub fn walk_dir(&mut self, path: &PathBuf) -> eyre::Result<()> {
119        for entry in WalkDir::new(path)
120            .into_iter()
121            .filter_map(|e| e.ok())
122            .filter(|e| {
123                e.path().strip_prefix(path).is_ok_and(|p| p.starts_with("src/")) // only src folder
124                && e.path().extension().is_some_and(|ext| ext == "rs") // only rust files
125            })
126        {
127            let content = std::fs::read_to_string(entry.path())?;
128            let syntax = syn::parse_file(&content)?;
129
130            // Set current file relative path for further imports resolution
131            // WARN It prevents to implement parallel parsing of files and requires rework in the future
132            self.set_relative_path(entry.path().strip_prefix(path)?.to_str());
133
134            self.visit_file(&syntax);
135        }
136
137        Ok(())
138    }
139
140    pub fn set_relative_path(&mut self, file_path: Option<&str>) {
141        self.relative_path = file_path.map_or_else(|| "".to_string(), |s| s.to_string());
142    }
143
144    fn parse_endpoint(&mut self, attr: &Attribute) -> syn::Result<Endpoint> {
145        attr.parse_args_with(Endpoint::parse)
146    }
147
148    fn parse_worker(&mut self, attr: &Attribute) -> syn::Result<Worker> {
149        attr.parse_args_with(Worker::parse)
150    }
151
152    fn parse_cron(&mut self, attr: &Attribute) -> syn::Result<Cron> {
153        attr.parse_args_with(Cron::parse)
154    }
155
156    /// Checks if the input is a valid kinetics_macro definition and returns its role
157    /// Checks if the input is a valid kinetics_macro definition
158    /// Known definitions:
159    /// kinetics_macro::<role> or <role>
160    fn parse_attr_role(&self, input: &Attribute) -> String {
161        let path = input.path();
162
163        if path.segments.len() == 1 {
164            let ident = &path.segments[0].ident;
165            return ident.to_string();
166        }
167
168        if path.segments.len() == 2 && &path.segments[0].ident == "kinetics_macro" {
169            let ident = &path.segments[1].ident;
170            return ident.to_string();
171        }
172
173        "".to_string()
174    }
175}
176
177impl Visit<'_> for Parser {
178    /// Visits function definitions
179    fn visit_item_fn(&mut self, item: &ItemFn) {
180        for attr in &item.attrs {
181            // Skip non-endpoint or non-worker attributes
182            let role = match self.parse_attr_role(attr).as_str() {
183                "endpoint" => {
184                    let params = self.parse_endpoint(attr).unwrap();
185                    Role::Endpoint(params)
186                }
187                "worker" => {
188                    let params = self.parse_worker(attr).unwrap();
189                    Role::Worker(params)
190                }
191                "cron" => {
192                    let params = self.parse_cron(attr).unwrap();
193                    Role::Cron(params)
194                }
195                _ => continue,
196            };
197
198            self.functions.push(ParsedFunction {
199                role,
200                rust_function_name: item.sig.ident.to_string(),
201                relative_path: self.relative_path.clone(),
202            });
203        }
204
205        // We don't need to parse the function body (in case nested functions), so just exit here
206    }
207}