Skip to main content

nautilus_schema/
lib.rs

1//! Nautilus Schema Parser and Validator
2//!
3//! This crate provides end-to-end processing of `.nautilus` schema files.
4//!
5//! # Pipeline
6//!
7//! Processing a schema runs in four stages:
8//! - **Lexer** — converts source text into typed tokens with span tracking.
9//! - **Parser** — builds a syntax [`ast::Schema`] via recursive descent.
10//! - **Validator** — performs multi-pass semantic validation and emits a fully
11//!   resolved [`ir::SchemaIr`].
12//! - **Formatter** — renders an AST back to canonical source text.
13//!
14//! # Quick Start
15//!
16//! The [`analyze`] function runs the full pipeline in one call and collects all
17//! diagnostics:
18//!
19//! ```ignore
20//! use nautilus_schema::analyze;
21//!
22//! let result = analyze(source);
23//! for diag in &result.diagnostics {
24//!     eprintln!("{:?} — {}", diag.severity, diag.message);
25//! }
26//! if let Some(ir) = &result.ir {
27//!     println!("{} models validated", ir.models.len());
28//! }
29//! ```
30//!
31//! # Visitor Pattern
32//!
33//! The [`visitor`] module provides a trait-based visitor for flexible AST traversal:
34//!
35//! ```ignore
36//! use nautilus_schema::{visitor::{Visitor, walk_model}, ast::*, Result};
37//!
38//! struct ModelCounter { count: usize }
39//!
40//! impl Visitor for ModelCounter {
41//!     fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
42//!         self.count += 1;
43//!         walk_model(self, model)
44//!     }
45//! }
46//! ```
47
48#![warn(missing_docs)]
49#![forbid(unsafe_code)]
50
51use ast::Schema;
52use ir::SchemaIr;
53
54pub mod analysis;
55pub mod ast;
56pub mod bool_expr;
57pub mod diagnostic;
58mod error;
59pub mod formatter;
60pub mod ir;
61mod lexer;
62pub mod parser;
63mod span;
64pub mod sql_expr;
65mod token;
66mod validator;
67pub mod visitor;
68
69pub use analysis::{
70    analyze, completion, completion_with_analysis, goto_definition, goto_definition_with_analysis,
71    hover, hover_with_analysis, semantic_tokens, AnalysisResult, CompletionItem, CompletionKind,
72    HoverInfo, SemanticKind, SemanticToken,
73};
74pub use ast::ComputedKind;
75pub use diagnostic::{Diagnostic, Severity};
76pub use error::{Result, SchemaError};
77pub use formatter::format_schema;
78pub use lexer::Lexer;
79pub use parser::Parser;
80pub use span::{Position, Span};
81pub use token::{Token, TokenKind};
82pub use validator::validate_schema;
83
84/// Parsed schema plus any non-fatal parse errors recovered during parsing.
85#[derive(Debug, Clone)]
86pub struct ParsedSchema {
87    /// Parsed AST.
88    pub ast: Schema,
89    /// Errors recovered by the parser while still producing an AST.
90    pub recovered_errors: Vec<SchemaError>,
91}
92
93/// Parsed AST together with the validated IR.
94#[derive(Debug, Clone)]
95pub struct ValidatedSchema {
96    /// Parsed AST.
97    pub ast: Schema,
98    /// Fully validated schema IR.
99    pub ir: SchemaIr,
100}
101
102fn lex_source(source: &str) -> Result<Vec<Token>> {
103    let mut lexer = Lexer::new(source);
104    let mut tokens = Vec::new();
105    loop {
106        let token = lexer.next_token()?;
107        let is_eof = matches!(token.kind, TokenKind::Eof);
108        tokens.push(token);
109        if is_eof {
110            break;
111        }
112    }
113    Ok(tokens)
114}
115
116/// Parse a schema source string and return the AST plus parser recovery errors.
117pub fn parse_schema_source_with_recovery(source: &str) -> Result<ParsedSchema> {
118    let tokens = lex_source(source)?;
119    let mut parser = Parser::new(&tokens, source);
120    let ast = parser.parse_schema()?;
121    let recovered_errors = parser.take_errors();
122    Ok(ParsedSchema {
123        ast,
124        recovered_errors,
125    })
126}
127
128/// Parse a schema source string strictly, failing if the parser had to recover.
129pub fn parse_schema_source(source: &str) -> Result<Schema> {
130    let parsed = parse_schema_source_with_recovery(source)?;
131    if let Some(error) = parsed.recovered_errors.into_iter().next() {
132        return Err(error);
133    }
134    Ok(parsed.ast)
135}
136
137/// Parse and validate a schema source string.
138pub fn validate_schema_source(source: &str) -> Result<ValidatedSchema> {
139    let ast = parse_schema_source(source)?;
140    let ir = validate_schema(ast.clone())?;
141    Ok(ValidatedSchema { ast, ir })
142}
143
144/// Resolve `env(VAR_NAME)` syntax in a connection URL.
145///
146/// If `raw` matches the pattern `env(...)`, the value of the named
147/// environment variable is returned.  Otherwise `raw` is returned as-is.
148pub fn resolve_env_url(raw: &str) -> std::result::Result<String, String> {
149    if raw.starts_with("env(") && raw.ends_with(')') {
150        let var = &raw[4..raw.len() - 1];
151        std::env::var(var).map_err(|_| format!("environment variable '{}' is not set", var))
152    } else {
153        Ok(raw.to_string())
154    }
155}