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}