1use anyhow::{bail, Context, Result};
12use log::debug;
13use std::{fs::File, io::Write, path::PathBuf};
14
15use crate::tera::get_tera;
16use crate::utils::{get_current_working_dir, get_full_path_str, is_dir, pretty_print};
17
18const INDEX_SQL_FILENAME: &str = "index.sql";
20const SQL_FILE_EXTENSION: &str = "sql";
21
22#[derive(clap::Args, Debug, Clone)]
23pub struct Build {
24 pub file: PathBuf,
27
28 #[arg(long, short)]
30 pub out: Option<PathBuf>,
31
32 #[arg(long, short)]
34 pub context: Option<PathBuf>,
35
36 #[arg(long, short)]
38 pub no_pretty: Option<bool>,
39}
40
41pub async fn call(args: Build) -> Result<()> {
42 let sql = build(args.clone())?;
44
45 match args.out {
47 Some(path) => {
48 let mut file = File::create(&path)
49 .with_context(|| format!("could not create output file {}", path.display()))?;
50
51 match file.write_all(sql.as_bytes()) {
52 Ok(_) => println!("Write to {}", path.display()),
53 Err(e) => println!("Failed write to {}: {}", path.display(), e),
54 }
55 }
56 None => {
57 if sql.is_empty() {
58 } else if args.no_pretty.unwrap_or_default() {
60 print!("{}", sql);
61 } else {
62 pretty_print(sql.as_bytes());
63 }
64 }
65 }
66
67 Ok(())
68}
69
70pub fn build(args: Build) -> Result<String> {
71 let path = &args.file;
72
73 let is_dir = is_dir(path);
74
75 if is_dir && path.read_dir()?.next().is_none() {
77 return Ok("".to_string());
78 }
79
80 let (working_dir, path_str) = get_dirs(args.clone())?;
81
82 if is_dir
84 && !path
85 .read_dir()?
86 .filter_map(Result::ok)
87 .any(|f| f.path().extension().and_then(|s| s.to_str()) == Some(SQL_FILE_EXTENSION))
88 {
89 let files = path
90 .read_dir()?
91 .map(Result::ok)
92 .into_iter()
93 .flatten()
94 .map(|f| f.path())
95 .collect::<Vec<_>>();
96
97 bail!("top-level doesn't contains any .sql file: {:?}", files);
98 }
99
100 let tera = get_tera(args.file.clone(), working_dir)?;
102
103 let loaded_template: Vec<_> = tera.get_template_names().collect();
105 debug!("loaded templates: {:?}", loaded_template);
106
107 let context = tera::Context::new();
109
110 let endpoint = if is_dir {
112 format!("{}/{}", path_str, INDEX_SQL_FILENAME)
113 .trim_start_matches('/')
114 .to_string()
115 } else {
116 path_str.to_string()
117 };
118
119 let out = tera
120 .render(&endpoint, &context)
121 .with_context(|| format!("failed to render from {}", path_str))?;
122
123 Ok(out.trim().to_string())
124}
125
126fn get_dirs(args: Build) -> Result<(PathBuf, String)> {
127 let path = &args.file;
128
129 let working_dir = get_current_working_dir(args.context)?;
131 debug!("Working dir: {}", &working_dir.display());
132
133 let path_str = get_full_path_str(path)?;
135 let working_dir_str = working_dir
136 .to_str()
137 .ok_or_else(|| anyhow::anyhow!("working directory path is not valid UTF-8"))?;
138 let path_str = path_str
139 .trim_start_matches(working_dir_str)
140 .trim_start_matches('/')
141 .to_string();
142
143 Ok((working_dir, path_str))
144}