Skip to main content

ruest_db_parser/
parse.rs

1use ruest_db_schema::{
2    Attribute, DefaultValue, Field, FieldKind, Model, RelationAttr, ScalarType, Schema,
3};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum ParseError {
8    #[error("parse error at line {line}: {message}")]
9    Syntax { line: usize, message: String },
10}
11
12pub fn parse_schema(source: &str) -> Result<Schema, ParseError> {
13    let mut models = Vec::new();
14    let mut line_no = 0usize;
15
16    let lines: Vec<&str> = source.lines().map(str::trim).collect();
17    let mut i = 0;
18
19    while i < lines.len() {
20        line_no = i + 1;
21        let line = lines[i];
22        i += 1;
23
24        if line.is_empty() || line.starts_with("//") {
25            continue;
26        }
27
28        if let Some(name) = line.strip_prefix("model ").and_then(|s| s.strip_suffix('{')) {
29            let name = name.trim().to_string();
30            let (body, consumed) = read_block(&lines[i..], line_no)?;
31            i += consumed;
32            models.push(parse_model(&name, &body)?);
33            continue;
34        }
35
36        return Err(ParseError::Syntax {
37            line: line_no,
38            message: format!("expected `model Name {{`, got `{line}`"),
39        });
40    }
41
42    validate_relations(&models)?;
43    Ok(Schema { models })
44}
45
46fn read_block(lines: &[&str], start_line: usize) -> Result<(Vec<String>, usize), ParseError> {
47    let mut body = Vec::new();
48    let mut depth = 1usize;
49    let mut i = 0usize;
50
51    while i < lines.len() {
52        let line = lines[i];
53        i += 1;
54        for ch in line.chars() {
55            if ch == '{' {
56                depth += 1;
57            } else if ch == '}' {
58                depth -= 1;
59            }
60        }
61        if depth == 0 {
62            let trimmed = line.trim_end_matches('}').trim();
63            if !trimmed.is_empty() {
64                body.push(trimmed.to_string());
65            }
66            return Ok((body, i));
67        }
68        body.push(line.to_string());
69    }
70
71    Err(ParseError::Syntax {
72        line: start_line,
73        message: "unclosed model block".into(),
74    })
75}
76
77fn parse_model(name: &str, body: &[String]) -> Result<Model, ParseError> {
78    let mut fields = Vec::new();
79
80    for (idx, line) in body.iter().enumerate() {
81        let line = line.trim();
82        if line.is_empty() || line.starts_with("//") {
83            continue;
84        }
85        fields.push(parse_field(line).map_err(|message| ParseError::Syntax {
86            line: idx + 1,
87            message,
88        })?);
89    }
90
91    if fields.is_empty() {
92        return Err(ParseError::Syntax {
93            line: 0,
94            message: format!("model `{name}` has no fields"),
95        });
96    }
97
98    Ok(Model {
99        name: name.to_string(),
100        fields,
101    })
102}
103
104fn parse_field(line: &str) -> Result<Field, String> {
105    let (head, attrs) = split_attributes(line);
106    let parts: Vec<&str> = head.split_whitespace().collect();
107    if parts.len() < 2 {
108        return Err(format!("invalid field line: `{line}`"));
109    }
110
111    let name = parts[0].to_string();
112    let mut type_part = parts[1];
113    let optional = type_part.ends_with('?');
114    if optional {
115        type_part = &type_part[..type_part.len() - 1];
116    }
117    let list = type_part.ends_with("[]");
118    let type_name = if list {
119        &type_part[..type_part.len() - 2]
120    } else {
121        type_part
122    };
123
124    let kind = parse_type_name(type_name)?;
125    let attributes = parse_attributes(&attrs)?;
126
127    Ok(Field {
128        name,
129        kind,
130        optional,
131        list,
132        attributes,
133    })
134}
135
136fn split_attributes(line: &str) -> (&str, &str) {
137    if let Some(pos) = line.find('@') {
138        (&line[..pos], &line[pos..])
139    } else {
140        (line, "")
141    }
142}
143
144fn parse_type_name(name: &str) -> Result<FieldKind, String> {
145    match name {
146        "String" => Ok(FieldKind::Scalar(ScalarType::String)),
147        "Int" => Ok(FieldKind::Scalar(ScalarType::Int)),
148        "Float" => Ok(FieldKind::Scalar(ScalarType::Float)),
149        "Boolean" => Ok(FieldKind::Scalar(ScalarType::Boolean)),
150        "DateTime" => Ok(FieldKind::Scalar(ScalarType::DateTime)),
151        "UUID" | "Uuid" => Ok(FieldKind::Scalar(ScalarType::Uuid)),
152        other if other.chars().next().is_some_and(|c| c.is_ascii_uppercase()) => {
153            Ok(FieldKind::Model(other.to_string()))
154        }
155        other => Err(format!("unknown type `{other}`")),
156    }
157}
158
159fn parse_attributes(src: &str) -> Result<Vec<Attribute>, String> {
160    let mut attrs = Vec::new();
161    let mut rest = src.trim();
162
163    while let Some(stripped) = rest.strip_prefix('@') {
164        rest = stripped;
165        let (name, rem) = take_ident(rest);
166        rest = rem.trim();
167
168        match name {
169            "id" => attrs.push(Attribute::Id),
170            "unique" => attrs.push(Attribute::Unique),
171            "default" => {
172                let (value, rem) = parse_paren_value(rest)?;
173                rest = rem.trim();
174                attrs.push(Attribute::Default(parse_default(&value)?));
175            }
176            "relation" => {
177                let (inner, rem) = parse_paren_value(rest)?;
178                rest = rem.trim();
179                attrs.push(Attribute::Relation(parse_relation(&inner)?));
180            }
181            other => return Err(format!("unknown attribute `@{other}`")),
182        }
183    }
184
185    Ok(attrs)
186}
187
188fn take_ident(s: &str) -> (&str, &str) {
189    let end = s
190        .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
191        .unwrap_or(s.len());
192    (&s[..end], &s[end..])
193}
194
195fn parse_paren_value(s: &str) -> Result<(String, &str), String> {
196    let s = s.trim();
197    if !s.starts_with('(') {
198        return Err("expected `(` after attribute".into());
199    }
200    let mut depth = 0i32;
201    let mut end = 0usize;
202    for (i, ch) in s.char_indices() {
203        if ch == '(' {
204            depth += 1;
205        } else if ch == ')' {
206            depth -= 1;
207            if depth == 0 {
208                end = i;
209                break;
210            }
211        }
212    }
213    if depth != 0 {
214        return Err("unclosed parentheses".into());
215    }
216    Ok((s[1..end].to_string(), &s[end + 1..]))
217}
218
219fn parse_default(value: &str) -> Result<DefaultValue, String> {
220    let value = value.trim();
221    if value == "uuid()" {
222        return Ok(DefaultValue::Uuid);
223    }
224    if value == "now()" {
225        return Ok(DefaultValue::Now);
226    }
227    if (value.starts_with('"') && value.ends_with('"'))
228        || (value.starts_with('\'') && value.ends_with('\''))
229    {
230        return Ok(DefaultValue::Literal(
231            value[1..value.len() - 1].to_string(),
232        ));
233    }
234    Ok(DefaultValue::Literal(value.to_string()))
235}
236
237fn parse_relation(inner: &str) -> Result<RelationAttr, String> {
238    let mut fields = Vec::new();
239    let mut references = Vec::new();
240    let mut current: Option<&str> = None;
241
242    for part in inner.split(',') {
243        let part = part.trim();
244        if let Some(key) = part.strip_prefix("fields:") {
245            current = Some("fields");
246            let key = key.trim();
247            if !key.is_empty() {
248                fields.extend(parse_bracket_list(key)?);
249            }
250            continue;
251        }
252        if let Some(key) = part.strip_prefix("references:") {
253            current = Some("references");
254            let key = key.trim();
255            if !key.is_empty() {
256                references.extend(parse_bracket_list(key)?);
257            }
258            continue;
259        }
260        if part.starts_with('[') {
261            let list = parse_bracket_list(part)?;
262            match current {
263                Some("fields") => fields.extend(list),
264                Some("references") => references.extend(list),
265                _ => return Err(format!("unexpected list in relation: `{part}`")),
266            }
267        }
268    }
269
270    if fields.is_empty() || references.is_empty() {
271        return Err("relation requires fields and references".into());
272    }
273
274    Ok(RelationAttr { fields, references })
275}
276
277fn parse_bracket_list(s: &str) -> Result<Vec<String>, String> {
278    let s = s.trim();
279    if !s.starts_with('[') || !s.ends_with(']') {
280        return Err(format!("expected bracket list, got `{s}`"));
281    }
282    let inner = &s[1..s.len() - 1];
283    Ok(inner
284        .split(',')
285        .map(|p| p.trim().to_string())
286        .filter(|p| !p.is_empty())
287        .collect())
288}
289
290fn validate_relations(models: &[Model]) -> Result<(), ParseError> {
291    let names: std::collections::HashSet<_> = models.iter().map(|m| m.name.as_str()).collect();
292
293    for model in models {
294        for field in &model.fields {
295            if let FieldKind::Model(ref target) = field.kind {
296                if !names.contains(target.as_str()) {
297                    return Err(ParseError::Syntax {
298                        line: 0,
299                        message: format!(
300                            "model `{}` references unknown model `{target}`",
301                            model.name
302                        ),
303                    });
304                }
305            }
306            if field.is_relation_scalar() {
307                if let Some(Attribute::Relation(rel)) = field
308                    .attributes
309                    .iter()
310                    .find(|a| matches!(a, Attribute::Relation(_)))
311                {
312                    for fk in &rel.fields {
313                        if !model.fields.iter().any(|f| f.name == *fk) {
314                            return Err(ParseError::Syntax {
315                                line: 0,
316                                message: format!(
317                                    "relation on `{}` references missing field `{fk}`",
318                                    model.name
319                                ),
320                            });
321                        }
322                    }
323                }
324            }
325        }
326    }
327    Ok(())
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    const SAMPLE: &str = r#"
335model User {
336  id        String   @id @default(uuid())
337  email     String   @unique
338  name      String
339  posts     Post[]
340}
341
342model Post {
343  id        String @id @default(uuid())
344  title     String
345  userId    String
346  user      User @relation(fields: [userId], references: [id])
347}
348"#;
349
350    #[test]
351    fn parses_prisma_like_schema() {
352        let schema = parse_schema(SAMPLE).expect("parse");
353        assert_eq!(schema.models.len(), 2);
354        assert!(schema.model("User").unwrap().id_field().is_some());
355    }
356}