Skip to main content

rohas_parser/
parser.rs

1use crate::ast::*;
2use crate::error::{ParseError, Result};
3use crate::grammar::{RohasParser, Rule};
4use pest::Parser as PestParser;
5use std::fs;
6use std::path::Path;
7use tracing::{debug, info};
8
9pub struct Parser;
10
11impl Parser {
12    pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Schema> {
13        let path = path.as_ref();
14        info!("Parsing schema file: {}", path.display());
15
16        let content = fs::read_to_string(path)
17            .map_err(|e| ParseError::FileNotFound(format!("{}: {}", path.display(), e)))?;
18
19        Self::parse_string(&content)
20    }
21
22    pub fn parse_string(input: &str) -> Result<Schema> {
23        let pairs = RohasParser::parse(Rule::schema, input)?;
24        let mut schema = Schema::new();
25
26        for pair in pairs {
27            if pair.as_rule() == Rule::schema {
28                for inner_pair in pair.into_inner() {
29                    match inner_pair.as_rule() {
30                        Rule::model => {
31                            let model = Self::parse_model(inner_pair)?;
32                            schema.models.push(model);
33                        }
34                        Rule::api => {
35                            let api = Self::parse_api(inner_pair)?;
36                            schema.apis.push(api);
37                        }
38                        Rule::event => {
39                            let event = Self::parse_event(inner_pair)?;
40                            schema.events.push(event);
41                        }
42                        Rule::cron => {
43                            let cron = Self::parse_cron(inner_pair)?;
44                            schema.crons.push(cron);
45                        }
46                        Rule::input => {
47                            let input = Self::parse_input(inner_pair)?;
48                            schema.inputs.push(input);
49                        }
50                        Rule::ws => {
51                            let ws = Self::parse_websocket(inner_pair)?;
52                            schema.websockets.push(ws);
53                        }
54                        Rule::EOI => {}
55                        _ => {
56                            debug!("Unexpected rule: {:?}", inner_pair.as_rule());
57                        }
58                    }
59                }
60            }
61        }
62
63        schema.validate()?;
64        Ok(schema)
65    }
66
67    fn parse_model(pair: pest::iterators::Pair<Rule>) -> Result<Model> {
68        let mut inner = pair.into_inner();
69        let name = inner
70            .next()
71            .ok_or_else(|| ParseError::InvalidModel("Missing model name".into()))?
72            .as_str()
73            .to_string();
74
75        let mut fields = Vec::new();
76
77        for field_pair in inner {
78            if field_pair.as_rule() == Rule::field {
79                fields.push(Self::parse_field(field_pair)?);
80            }
81        }
82
83        Ok(Model {
84            name,
85            fields,
86            attributes: Vec::new(),
87        })
88    }
89
90    fn parse_field(pair: pest::iterators::Pair<Rule>) -> Result<Field> {
91        let mut inner = pair.into_inner();
92
93        let name = inner
94            .next()
95            .ok_or_else(|| ParseError::InvalidModel("Missing field name".into()))?
96            .as_str()
97            .to_string();
98
99        let field_type_pair = inner
100            .next()
101            .ok_or_else(|| ParseError::InvalidModel("Missing field type".into()))?;
102
103        let field_type = Self::parse_field_type(field_type_pair)?;
104
105        let mut optional = false;
106        let mut attributes = Vec::new();
107
108        for item in inner {
109            match item.as_rule() {
110                Rule::optional => optional = true,
111                Rule::attribute => attributes.push(Self::parse_attribute(item)?),
112                _ => {}
113            }
114        }
115
116        Ok(Field {
117            name,
118            field_type,
119            optional,
120            attributes,
121        })
122    }
123
124    fn parse_field_type(pair: pest::iterators::Pair<Rule>) -> Result<FieldType> {
125        let mut inner = pair.into_inner();
126        let type_name = inner
127            .next()
128            .ok_or_else(|| ParseError::InvalidType("Missing type name".into()))?
129            .as_str();
130
131        let mut field_type = FieldType::from_str(type_name);
132
133        // Check for array suffix
134        if let Some(array_pair) = inner.next() {
135            if array_pair.as_rule() == Rule::array_suffix {
136                field_type = FieldType::Array(Box::new(field_type));
137            }
138        }
139
140        Ok(field_type)
141    }
142
143    fn parse_attribute(pair: pest::iterators::Pair<Rule>) -> Result<Attribute> {
144        let mut inner = pair.into_inner();
145        let name = inner
146            .next()
147            .ok_or_else(|| ParseError::InvalidAttribute("Missing attribute name".into()))?
148            .as_str()
149            .to_string();
150
151        let mut args = Vec::new();
152
153        for arg_pair in inner {
154            if arg_pair.as_rule() == Rule::attr_args {
155                for arg in arg_pair.into_inner() {
156                    if arg.as_rule() == Rule::attr_arg_list {
157                        for item in arg.into_inner() {
158                            args.push(item.as_str().trim_matches('"').to_string());
159                        }
160                    }
161                }
162            }
163        }
164
165        Ok(Attribute { name, args })
166    }
167
168    fn parse_api(pair: pest::iterators::Pair<Rule>) -> Result<Api> {
169        let mut inner = pair.into_inner();
170        let name = inner
171            .next()
172            .ok_or_else(|| ParseError::InvalidApi("Missing API name".into()))?
173            .as_str()
174            .to_string();
175
176        let mut method = None;
177        let mut path = None;
178        let mut body = None;
179        let mut response = None;
180        let mut triggers = Vec::new();
181        let mut middlewares = Vec::new();
182
183        for prop in inner {
184            if prop.as_rule() == Rule::api_property {
185                let prop_text = prop.as_str();
186                let mut prop_inner = prop.into_inner();
187
188                if let Some(key) = prop_inner.next() {
189                    match key.as_rule() {
190                        Rule::http_method => method = HttpMethod::from_str(key.as_str()),
191                        Rule::string => path = Some(key.as_str().trim_matches('"').to_string()),
192                        Rule::ident => {
193                            if prop_text.starts_with("body:") {
194                                body = Some(key.as_str().to_string());
195                            } else if prop_text.starts_with("response:") {
196                                response = Some(key.as_str().to_string());
197                            }
198                        }
199                        Rule::trigger_list => {
200                            triggers = Self::parse_string_list(key)?;
201                        }
202                        Rule::string_list | Rule::middleware_list => {
203                            middlewares = Self::parse_string_list(key)?;
204                        }
205                        _ => {}
206                    }
207                }
208            }
209        }
210
211        Ok(Api {
212            name,
213            method: method.ok_or_else(|| ParseError::InvalidApi("Missing HTTP method".into()))?,
214            path: path.ok_or_else(|| ParseError::InvalidApi("Missing path".into()))?,
215            body,
216            response: response.ok_or_else(|| ParseError::InvalidApi("Missing response".into()))?,
217            triggers,
218            middlewares,
219        })
220    }
221
222    fn parse_event(pair: pest::iterators::Pair<Rule>) -> Result<Event> {
223        let mut inner = pair.into_inner();
224        let name = inner
225            .next()
226            .ok_or_else(|| ParseError::InvalidEvent("Missing event name".into()))?
227            .as_str()
228            .to_string();
229
230        let mut payload = String::new();
231        let mut handlers = Vec::new();
232        let mut triggers = Vec::new();
233        let mut adapter_type = None;
234
235        for prop in inner {
236            if prop.as_rule() == Rule::event_property {
237                let prop_text = prop.as_str();
238                let mut prop_inner = prop.into_inner();
239                if let Some(value) = prop_inner.next() {
240                    match value.as_rule() {
241                        Rule::ident => {
242                            if prop_text.starts_with("payload:") {
243                                payload = value.as_str().to_string();
244                            } else if prop_text.starts_with("type:") {
245                                let type_str = value.as_str().to_lowercase();
246                                if type_str == "sqs" || type_str == "eventbridge" {
247                                    adapter_type = Some(type_str);
248                                } else {
249                                    return Err(ParseError::InvalidEvent(format!(
250                                        "Invalid adapter type '{}' for event '{}'. Must be 'sqs' or 'eventbridge'",
251                                        value.as_str(), name
252                                    )));
253                                }
254                            }
255                        }
256                        Rule::handler_list | Rule::trigger_list => {
257                            let items = Self::parse_string_list(value)?;
258                            if prop_text.starts_with("handler:") {
259                                handlers = items;
260                            } else if prop_text.starts_with("triggers:") {
261                                triggers = items;
262                            }
263                        }
264                        _ => {}
265                    }
266                }
267            }
268        }
269
270        Ok(Event {
271            name,
272            payload,
273            handlers,
274            triggers,
275            adapter_type,
276        })
277    }
278
279    fn parse_cron(pair: pest::iterators::Pair<Rule>) -> Result<Cron> {
280        let mut inner = pair.into_inner();
281        let name = inner
282            .next()
283            .ok_or_else(|| ParseError::InvalidCron("Missing cron name".into()))?
284            .as_str()
285            .to_string();
286
287        let mut schedule = String::new();
288        let mut triggers = Vec::new();
289
290        for prop in inner {
291            if prop.as_rule() == Rule::cron_property {
292                let mut prop_inner = prop.into_inner();
293                if let Some(value) = prop_inner.next() {
294                    match value.as_rule() {
295                        Rule::string => schedule = value.as_str().trim_matches('"').to_string(),
296                        Rule::trigger_list => triggers = Self::parse_string_list(value)?,
297                        _ => {}
298                    }
299                }
300            }
301        }
302
303        Ok(Cron {
304            name,
305            schedule,
306            triggers,
307        })
308    }
309
310    fn parse_input(pair: pest::iterators::Pair<Rule>) -> Result<Input> {
311        let mut inner = pair.into_inner();
312        let name = inner
313            .next()
314            .ok_or_else(|| ParseError::InvalidModel("Missing input name".into()))?
315            .as_str()
316            .to_string();
317
318        let mut fields = Vec::new();
319
320        for field_pair in inner {
321            if field_pair.as_rule() == Rule::input_field {
322                let mut field_inner = field_pair.into_inner();
323
324                let field_name = field_inner
325                    .next()
326                    .ok_or_else(|| ParseError::InvalidModel("Missing field name".into()))?
327                    .as_str()
328                    .to_string();
329
330                let field_type_pair = field_inner
331                    .next()
332                    .ok_or_else(|| ParseError::InvalidModel("Missing field type".into()))?;
333
334                let field_type = Self::parse_field_type(field_type_pair)?;
335
336                let optional = field_inner.next().is_some();
337
338                fields.push(Field {
339                    name: field_name,
340                    field_type,
341                    optional,
342                    attributes: Vec::new(),
343                });
344            }
345        }
346
347        Ok(Input { name, fields })
348    }
349
350    fn parse_string_list(pair: pest::iterators::Pair<Rule>) -> Result<Vec<String>> {
351        let mut items = Vec::new();
352        for item in pair.into_inner() {
353            match item.as_rule() {
354                Rule::ident => items.push(item.as_str().to_string()),
355                Rule::string => items.push(item.as_str().trim_matches('"').to_string()),
356                _ => {}
357            }
358        }
359        Ok(items)
360    }
361
362    fn parse_websocket(pair: pest::iterators::Pair<Rule>) -> Result<WebSocket> {
363        let mut inner = pair.into_inner();
364        let name = inner
365            .next()
366            .ok_or_else(|| ParseError::InvalidApi("Missing websocket name".into()))?
367            .as_str()
368            .to_string();
369
370        let mut path = None;
371        let mut message = None;
372        let mut on_connect = Vec::new();
373        let mut on_message = Vec::new();
374        let mut on_disconnect = Vec::new();
375        let mut triggers = Vec::new();
376        let mut broadcast = false;
377        let mut middlewares = Vec::new();
378
379        for prop in inner {
380            if prop.as_rule() == Rule::ws_property {
381                let prop_text = prop.as_str();
382                let mut prop_inner = prop.into_inner();
383
384                if let Some(key) = prop_inner.next() {
385                    match key.as_rule() {
386                        Rule::string => {
387                            if prop_text.starts_with("path:") {
388                                path = Some(key.as_str().trim_matches('"').to_string());
389                            }
390                        }
391                        Rule::ident => {
392                            if prop_text.starts_with("message:") {
393                                message = Some(key.as_str().to_string());
394                            }
395                        }
396                        Rule::handler_list => {
397                            if prop_text.starts_with("onConnect:") {
398                                on_connect = Self::parse_string_list(key)?;
399                            } else if prop_text.starts_with("onMessage:") {
400                                on_message = Self::parse_string_list(key)?;
401                            } else if prop_text.starts_with("onDisconnect:") {
402                                on_disconnect = Self::parse_string_list(key)?;
403                            }
404                        }
405                        Rule::trigger_list => {
406                            triggers = Self::parse_string_list(key)?;
407                        }
408                        Rule::string_list | Rule::middleware_list => {
409                            if prop_text.starts_with("middlewares:") {
410                                middlewares = Self::parse_string_list(key)?;
411                            }
412                        }
413                        Rule::boolean => {
414                            if prop_text.starts_with("broadcast:") {
415                                broadcast = key.as_str() == "true";
416                            }
417                        }
418                        _ => {}
419                    }
420                }
421            }
422        }
423
424        Ok(WebSocket {
425            name,
426            path: path.ok_or_else(|| ParseError::InvalidApi("Missing path".into()))?,
427            message,
428            on_connect,
429            on_message,
430            on_disconnect,
431            triggers,
432            broadcast,
433            middlewares,
434        })
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_parse_model() {
444        let input = r#"
445            model User {
446                id Int @id @auto
447                name String
448                email String @unique
449            }
450        "#;
451
452        let schema = Parser::parse_string(input).expect("Failed to parse");
453        assert_eq!(schema.models.len(), 1);
454        assert_eq!(schema.models[0].name, "User");
455        assert_eq!(schema.models[0].fields.len(), 3);
456    }
457
458    #[test]
459    fn test_parse_api() {
460        let input = r#"
461            api CreateUser {
462                method: POST
463                path: "/users"
464                body: CreateUserInput
465                response: User
466                triggers: [UserCreated]
467            }
468        "#;
469
470        let schema = Parser::parse_string(input).expect("Failed to parse");
471        assert_eq!(schema.apis.len(), 1);
472        assert_eq!(schema.apis[0].name, "CreateUser");
473    }
474
475    #[test]
476    fn test_parse_event() {
477        let input = r#"
478            event UserCreated {
479                payload: User
480                handler: [send_welcome_email, update_analytics]
481                triggers: [NotifyAdmin]
482            }
483        "#;
484
485        let schema = Parser::parse_string(input).expect("Failed to parse");
486        assert_eq!(schema.events.len(), 1);
487        assert_eq!(schema.events[0].name, "UserCreated");
488        assert_eq!(schema.events[0].handlers.len(), 2);
489    }
490}