use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use crate::ast::{ProtoFileAst, ProtoSchema};
mod ast_parser;
mod db_parser;
pub mod lexer;
mod naming;
mod options;
mod paths;
mod selection;
mod structure;
use ast_parser::ProtoAstParser;
use db_parser::ProtoParser;
use lexer::{LexError, Lexer};
use selection::dedupe_canonical_table_schemas;
#[cfg(test)]
use naming::{infer_sql_type, to_plural, to_snake_case};
pub const UDB_ANNOTATION_VERSION: &str = "1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnotationParserMode {
Compat,
Warn,
Strict,
}
#[derive(Debug, Clone)]
pub struct ParserConfig {
pub proto_namespace: String,
pub annotation_mode: AnnotationParserMode,
pub expected_annotation_version: String,
}
impl ParserConfig {
pub fn new(proto_namespace: impl Into<String>) -> Self {
Self {
proto_namespace: proto_namespace.into(),
annotation_mode: AnnotationParserMode::Compat,
expected_annotation_version: UDB_ANNOTATION_VERSION.to_string(),
}
}
pub fn with_annotation_mode(mut self, mode: AnnotationParserMode) -> Self {
self.annotation_mode = mode;
self
}
pub fn with_expected_annotation_version(mut self, version: impl Into<String>) -> Self {
self.expected_annotation_version = version.into();
self
}
}
impl Default for ParserConfig {
fn default() -> Self {
Self::new("")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserDiagnostic {
pub file: String,
pub line: usize,
pub column: usize,
pub code: String,
pub message: String,
}
impl fmt::Display for ParserDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{} [{}]: {}",
self.file, self.line, self.column, self.code, self.message
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ParseReport {
pub schemas: Vec<ProtoSchema>,
pub diagnostics: Vec<ParserDiagnostic>,
}
impl ParseReport {
pub fn passed(&self) -> bool {
self.diagnostics.is_empty()
}
}
#[derive(Debug)]
pub enum ParseError {
Io {
path: PathBuf,
source: std::io::Error,
},
Lex(LexError),
Syntax {
file: String,
line: usize,
column: usize,
message: String,
},
Directory {
path: PathBuf,
errors: Vec<String>,
},
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io { path, source } => write!(f, "cannot read {}: {source}", path.display()),
Self::Lex(source) => write!(f, "{source}"),
Self::Syntax {
file,
line,
column,
message,
} => write!(f, "{file}:{line}:{column}: {message}"),
Self::Directory { path, errors } => write!(
f,
"parse_directory {}: {} file(s) failed:\n {}",
path.display(),
errors.len(),
errors.join("\n ")
),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse_directory(
dir_path: impl AsRef<Path>,
config: &ParserConfig,
) -> Result<Vec<ProtoSchema>, ParseError> {
parse_directory_report(dir_path, config).map(|report| report.schemas)
}
pub fn parse_directory_report(
dir_path: impl AsRef<Path>,
config: &ParserConfig,
) -> Result<ParseReport, ParseError> {
let dir_path = dir_path.as_ref();
let mut paths = Vec::new();
collect_proto_files(dir_path, &mut paths).map_err(|source| ParseError::Io {
path: dir_path.to_path_buf(),
source,
})?;
paths.sort();
let mut schemas = Vec::new();
let mut diagnostics = Vec::new();
let mut errors = Vec::new();
for path in paths {
match parse_file_report(&path, config) {
Ok(mut report) => {
schemas.append(&mut report.schemas);
diagnostics.append(&mut report.diagnostics);
}
Err(err) => errors.push(format!("{}: {err}", path.display())),
}
}
let schemas = dedupe_canonical_table_schemas(schemas);
if errors.is_empty() {
Ok(ParseReport {
schemas,
diagnostics,
})
} else {
Err(ParseError::Directory {
path: dir_path.to_path_buf(),
errors,
})
}
}
pub fn parse_file(
file_path: impl AsRef<Path>,
config: &ParserConfig,
) -> Result<Vec<ProtoSchema>, ParseError> {
parse_file_report(file_path, config).map(|report| report.schemas)
}
pub fn parse_proto_source(
source: &[u8],
name: impl Into<String>,
config: &ParserConfig,
) -> Result<ParseReport, ParseError> {
let file = name.into();
let tokens = Lexer::new(source, file.clone())
.tokenize()
.map_err(ParseError::Lex)?;
ProtoParser::new(tokens, file, config).parse_report()
}
pub fn parse_file_report(
file_path: impl AsRef<Path>,
config: &ParserConfig,
) -> Result<ParseReport, ParseError> {
let file_path = file_path.as_ref();
let source = fs::read(file_path).map_err(|source| ParseError::Io {
path: file_path.to_path_buf(),
source,
})?;
let file = file_path.to_string_lossy().to_string();
let tokens = Lexer::new(&source, file.clone())
.tokenize()
.map_err(ParseError::Lex)?;
ProtoParser::new(tokens, file, config).parse_report()
}
pub fn parse_ast_file(file_path: impl AsRef<Path>) -> Result<ProtoFileAst, ParseError> {
let file_path = file_path.as_ref();
let source = fs::read(file_path).map_err(|source| ParseError::Io {
path: file_path.to_path_buf(),
source,
})?;
let file = file_path.to_string_lossy().to_string();
parse_ast_source(&source, file)
}
pub fn parse_ast_source(
source: &[u8],
file: impl Into<String>,
) -> Result<ProtoFileAst, ParseError> {
let file = file.into();
let tokens = Lexer::new(source, file.clone())
.tokenize()
.map_err(ParseError::Lex)?;
ProtoAstParser::new(tokens, file).parse()
}
fn collect_proto_files(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), std::io::Error> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_proto_files(&path, paths)?;
} else if path.extension().and_then(|ext| ext.to_str()) == Some("proto") {
paths.push(path);
}
}
Ok(())
}
#[cfg(test)]
#[path = "tests.rs"]
mod parser_tests;