mod to_schema;
use anyhow::{Result, bail};
use runique_dsl::ast::ModelInput;
use syn::visit::Visit;
pub(crate) use to_schema::decl_to_column;
use to_schema::{list_tables, model_to_parsed_schema, pascal_to_snake};
use crate::migration::utils::types::ParsedSchema;
pub(crate) fn located(err: &syn::Error) -> String {
let start = err.span().start();
format!("{}:{}: {}", start.line, start.column + 1, err)
}
pub(crate) struct MacroCollector<T> {
name: &'static str,
pub found: Vec<syn::Result<T>>,
}
impl<T> MacroCollector<T> {
pub(crate) fn new(name: &'static str) -> Self {
Self {
name,
found: Vec::new(),
}
}
}
impl<'ast, T: syn::parse::Parse> Visit<'ast> for MacroCollector<T> {
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
if mac
.path
.segments
.last()
.is_some_and(|s| s.ident == self.name)
{
self.found.push(syn::parse2::<T>(mac.tokens.clone()));
}
syn::visit::visit_macro(self, mac);
}
}
pub struct ParsedModel {
pub name: String,
pub schema: ParsedSchema,
pub lists: Vec<ParsedSchema>,
}
pub fn parse_schema_from_source(source: &str) -> Result<Option<(String, ParsedSchema)>> {
Ok(parse_model_from_source(source)?.map(|m| (m.name, m.schema)))
}
pub fn parse_model_from_source(source: &str) -> Result<Option<ParsedModel>> {
let file = syn::parse_str::<syn::File>(source).map_err(|e| anyhow::anyhow!(located(&e)))?;
let mut collector = MacroCollector::<ModelInput>::new("model");
collector.visit_file(&file);
let mut models = Vec::new();
for parsed in collector.found {
models.push(parsed.map_err(|e| anyhow::anyhow!(located(&e)))?);
}
if models.len() > 1 {
bail!(
"{} `model!{{}}` declarations in one file — one model per file",
models.len()
);
}
Ok(models.pop().map(|model| {
let schema = model_to_parsed_schema(&model);
ParsedModel {
name: pascal_to_snake(&model.name.to_string()),
lists: list_tables(&model, &schema),
schema,
}
}))
}