use std::{
fs,
io::Read,
path::{Path, PathBuf},
};
use codas::{langs, parse, types::Coda};
use super::{open_file_or_stdin, CompileCommand, Lang};
pub fn execute_compile_command(command: CompileCommand) {
match command.lang {
Some(lang) => pipe_mode(command.source, lang),
None => batch_mode(command),
}
}
fn pipe_mode(source: Option<PathBuf>, lang: Lang) {
let mut input = open_file_or_stdin(source).expect("source doesn't exist");
let mut markdown = String::new();
input
.read_to_string(&mut markdown)
.expect("failed to read source");
let coda = parse::parse(&markdown).expect("failed to parse coda");
let mut stdout = std::io::stdout().lock();
generate(&coda, lang, &mut stdout);
}
fn batch_mode(command: CompileCommand) {
let source = command
.source
.unwrap_or_else(|| std::env::current_dir().unwrap());
if !source.is_dir() {
eprintln!(
"error: --source must be a directory in batch mode (got {})",
source.display()
);
std::process::exit(1);
}
let codas = discover_codas(&source);
if codas.is_empty() {
eprintln!("no codas found in {}", source.display());
return;
}
let langs = [
Lang::Rust,
Lang::Python,
Lang::Typescript,
Lang::OpenApi,
Lang::Sql,
];
for lang in langs {
let lang_dir = command.target.join(lang.dir_name());
fs::create_dir_all(&lang_dir).expect("failed to create output directory");
for (path, coda) in &codas {
let file_name = lang.file_name(&coda.local_name);
let out_path = lang_dir.join(&file_name);
let mut file = fs::File::create(&out_path).expect("failed to create output file");
generate(coda, lang, &mut file);
eprintln!(" {} -> {}", path.display(), out_path.display());
}
}
eprintln!(
"compiled {} coda(s) to {} language(s)",
codas.len(),
langs.len()
);
}
fn discover_codas(dir: &Path) -> Vec<(PathBuf, Coda)> {
let mut codas = Vec::new();
collect_md_files(dir, &mut codas);
codas.sort_by(|(a, _), (b, _)| a.cmp(b));
codas
}
fn collect_md_files(dir: &Path, codas: &mut Vec<(PathBuf, Coda)>) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_md_files(&path, codas);
} else if path.extension().is_some_and(|ext| ext == "md") {
let markdown = match fs::read_to_string(&path) {
Ok(m) => m,
Err(_) => continue,
};
if let Ok(coda) = parse::parse(&markdown) {
codas.push((path, coda));
}
}
}
}
fn generate(coda: &Coda, lang: Lang, out: &mut impl std::io::Write) {
match lang {
Lang::Rust => langs::rust::generate_types(coda, out, true),
Lang::Python => langs::python::generate_types(coda, out),
Lang::Typescript => langs::typescript::generate_types(coda, out),
Lang::OpenApi => langs::open_api::generate_spec(coda, out),
Lang::Sql => langs::sql::generate_types(coda, out),
}
.expect("failed to write output");
}
impl Lang {
fn dir_name(self) -> &'static str {
match self {
Lang::Rust => "rust",
Lang::Python => "python",
Lang::Typescript => "typescript",
Lang::OpenApi => "open-api",
Lang::Sql => "sql",
}
}
fn file_name(self, local_name: &str) -> String {
let snake = to_snake_case(local_name);
match self {
Lang::Rust => format!("{snake}.rs"),
Lang::Python => format!("{snake}.py"),
Lang::Typescript => format!("{snake}.ts"),
Lang::OpenApi => format!("{snake}.yaml"),
Lang::Sql => format!("{snake}.sql"),
}
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
}
result
}