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;
53use std::path::{Path, PathBuf};
54
55pub mod analysis;
56pub mod ast;
57pub mod bool_expr;
58pub mod diagnostic;
59mod error;
60pub mod formatter;
61pub mod ir;
62mod lexer;
63pub mod parser;
64mod span;
65pub mod sql_expr;
66mod token;
67mod validator;
68pub mod visitor;
69
70pub use analysis::{
71    analyze, completion, completion_with_analysis, goto_definition, goto_definition_with_analysis,
72    hover, hover_with_analysis, semantic_tokens, AnalysisResult, CompletionItem, CompletionKind,
73    HoverInfo, SemanticKind, SemanticToken,
74};
75pub use ast::ComputedKind;
76pub use diagnostic::{Diagnostic, Severity};
77pub use error::{Result, SchemaError};
78pub use formatter::format_schema;
79pub use lexer::Lexer;
80pub use parser::Parser;
81pub use span::{Position, Span};
82pub use token::{Token, TokenKind};
83pub use validator::validate_schema;
84
85/// Parsed schema plus any non-fatal parse errors recovered during parsing.
86#[derive(Debug, Clone)]
87pub struct ParsedSchema {
88    /// Parsed AST.
89    pub ast: Schema,
90    /// Errors recovered by the parser while still producing an AST.
91    pub recovered_errors: Vec<SchemaError>,
92}
93
94/// Parsed AST together with the validated IR.
95#[derive(Debug, Clone)]
96pub struct ValidatedSchema {
97    /// Parsed AST.
98    pub ast: Schema,
99    /// Fully validated schema IR.
100    pub ir: SchemaIr,
101}
102
103fn lex_source(source: &str) -> Result<Vec<Token>> {
104    let mut lexer = Lexer::new(source);
105    let mut tokens = Vec::new();
106    loop {
107        let token = lexer.next_token()?;
108        let is_eof = matches!(token.kind, TokenKind::Eof);
109        tokens.push(token);
110        if is_eof {
111            break;
112        }
113    }
114    Ok(tokens)
115}
116
117/// Parse a schema source string and return the AST plus parser recovery errors.
118pub fn parse_schema_source_with_recovery(source: &str) -> Result<ParsedSchema> {
119    let tokens = lex_source(source)?;
120    let mut parser = Parser::new(&tokens, source);
121    let ast = parser.parse_schema()?;
122    let recovered_errors = parser.take_errors();
123    Ok(ParsedSchema {
124        ast,
125        recovered_errors,
126    })
127}
128
129/// Parse a schema source string strictly, failing if the parser had to recover.
130pub fn parse_schema_source(source: &str) -> Result<Schema> {
131    let parsed = parse_schema_source_with_recovery(source)?;
132    if let Some(error) = parsed.recovered_errors.into_iter().next() {
133        return Err(error);
134    }
135    Ok(parsed.ast)
136}
137
138/// Parse and validate a schema source string.
139pub fn validate_schema_source(source: &str) -> Result<ValidatedSchema> {
140    let ast = parse_schema_source(source)?;
141    let ir = validate_schema(ast.clone())?;
142    Ok(ValidatedSchema { ast, ir })
143}
144
145/// Return every `.nautilus` file directly inside `dir`, sorted lexicographically.
146pub fn discover_schema_paths(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
147    let mut paths: Vec<PathBuf> = std::fs::read_dir(dir)?
148        .filter_map(|entry| entry.ok())
149        .map(|entry| entry.path())
150        .filter(|path| {
151            path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("nautilus")
152        })
153        .collect();
154    paths.sort();
155    Ok(paths)
156}
157
158/// Return every `.nautilus` file in the current working directory, sorted lexicographically.
159pub fn discover_schema_paths_in_current_dir() -> std::io::Result<Vec<PathBuf>> {
160    let current_dir = std::env::current_dir()?;
161    discover_schema_paths(&current_dir)
162}
163
164/// Resolve `env(VAR_NAME)` syntax in a connection URL.
165///
166/// If `raw` matches the pattern `env(...)`, the value of the named
167/// environment variable is returned.  Otherwise `raw` is returned as-is.
168pub fn resolve_env_url(raw: &str) -> std::result::Result<String, String> {
169    if raw.starts_with("env(") && raw.ends_with(')') {
170        let var = &raw[4..raw.len() - 1];
171        std::env::var(var).map_err(|_| format!("environment variable '{}' is not set", var))
172    } else {
173        Ok(raw.to_string())
174    }
175}