Skip to main content

ruprizzle_parser/
lib.rs

1//! `schema.ruprizzle` parser.
2//!
3//! The public surface is deliberately one function, [`parse`]. Everything else —
4//! the Pest grammar, the loose AST, the five lowering passes, the validation rule
5//! table — is an implementation detail behind it. That narrowness is what makes
6//! replacing the parser a contained change rather than a schedule risk (see the
7//! fallback note in `ImplPlan02SchemaDslParser.md`).
8//!
9//! ```
10//! let schema = ruprizzle_parser::parse(
11//!     "schema.ruprizzle",
12//!     r#"
13//!     datasource db {
14//!       provider = "postgres"
15//!       url      = env("DATABASE_URL")
16//!     }
17//!
18//!     model User {
19//!       id    Uuid   @id @default(uuid7())
20//!       email String @unique
21//!     }
22//!     "#,
23//! )
24//! .expect("valid schema");
25//!
26//! assert_eq!(schema.model("User").unwrap().table, "users");
27//! ```
28//!
29//! Errors accumulate: one call reports every problem it can find, each with a
30//! span and a suggested fix.
31
32#![forbid(unsafe_code)]
33#![warn(missing_docs, clippy::pedantic)]
34
35pub mod ast;
36mod errors;
37mod grammar;
38mod lower;
39pub mod naming;
40mod validate;
41
42use ruprizzle_core::diagnostic::{Diagnostics, SchemaErrors};
43use ruprizzle_core::ir::Schema;
44
45pub use ast::Ast;
46
47/// Parses and validates a schema.
48///
49/// `file_name` is used only to label diagnostics; nothing is read from disk.
50///
51/// # Errors
52///
53/// Returns every problem found in one bundle — syntax errors, unresolved types,
54/// and broken validation rules alike — with the source attached so each can
55/// render its own span. Warnings (V17, and the dialect notes added in P2) never
56/// fail: they are returned to the caller through [`parse_with_warnings`].
57pub fn parse(file_name: &str, source: &str) -> Result<Schema, Box<SchemaErrors>> {
58    parse_with_warnings(file_name, source).map(|(schema, _)| schema)
59}
60
61/// Like [`parse`], but also returns the advisory diagnostics.
62///
63/// The CLI prints these on the success path; tests assert on them.
64///
65/// # Errors
66///
67/// As [`parse`].
68pub fn parse_with_warnings(
69    file_name: &str,
70    source: &str,
71) -> Result<(Schema, Vec<ruprizzle_core::SchemaError>), Box<SchemaErrors>> {
72    let mut diags = Diagnostics::new();
73
74    let ast = match grammar::parse_ast(source) {
75        Ok(ast) => ast,
76        Err(err) => {
77            // A syntax error means there is no tree to lower, so this is the one
78            // place the parser cannot keep going.
79            diags.push(errors::from_pest(&err, source));
80            diags.into_result(file_name, source)?;
81            unreachable!("a syntax error is always fatal");
82        }
83    };
84
85    let schema = lower::lower(&ast, &mut diags);
86    let warnings = diags.take_warnings();
87    diags.into_result(file_name, source)?;
88    Ok((schema, warnings))
89}
90
91/// Parses a schema without validating it, for tests and tooling that need the
92/// syntax tree rather than the IR.
93///
94/// # Errors
95///
96/// Returns the syntax error, phrased for humans.
97pub fn parse_ast(file_name: &str, source: &str) -> Result<Ast, Box<SchemaErrors>> {
98    grammar::parse_ast(source).map_err(|err| {
99        let mut diags = Diagnostics::new();
100        diags.push(errors::from_pest(&err, source));
101        diags
102            .into_result(file_name, source)
103            .expect_err("a syntax error is fatal")
104    })
105}