1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use anyhow::{bail, Context, Result};
use log::debug;
use std::{fs::File, io::Write, path::PathBuf};

use crate::tera::get_tera;
use crate::utils::{get_current_working_dir, get_full_path_str, is_dir, pretty_print};

#[derive(clap::Args, Debug, Clone)]
pub struct Build {
    /// Target path to render. If the target path is a directory,
    /// the root folder must contains the index.sql file
    pub file: PathBuf,

    /// Output path. The file will be overwritten if is already exists
    #[arg(long, short)]
    pub out: Option<PathBuf>,

    /// Change the context current working dir
    #[arg(long, short)]
    pub context: Option<PathBuf>,

    /// No pretty print for SQL
    #[arg(long, short)]
    pub no_pretty: Option<bool>,
}

pub async fn call(args: Build) -> Result<()> {
    // Render SQL
    let sql = build(args.clone())?;

    // Print to stdout or write to file?
    match args.out {
        Some(path) => {
            let mut file = File::create(&path)
                .with_context(|| format!("could not create output file {}", path.display()))?;

            match file.write_all(sql.as_bytes()) {
                Ok(_) => println!("Write to {}", path.display()),
                Err(e) => println!("Failed write to {}: {}", path.display(), e),
            }
        }
        None => {
            if args.no_pretty.unwrap_or_default() {
                print!("{}", sql);
            } else {
                pretty_print(sql.as_bytes());
            }
        }
    }

    Ok(())
}

pub fn build(args: Build) -> Result<String> {
    let path = &args.file;

    let is_dir = is_dir(path);

    // If input path is empty folder, just return empty
    if is_dir && path.read_dir()?.next().is_none() {
        return Ok("".to_string());
    }

    let (working_dir, path_str) = get_dirs(args.clone())?;

    // If input path contains no *.sql files, error
    if is_dir
        && !path
            .read_dir()?
            .filter_map(Result::ok)
            .any(|f| f.path().extension().unwrap_or_default() == "sql")
    {
        let files = path
            .read_dir()?
            .map(Result::ok)
            .into_iter()
            .flatten()
            .map(|f| f.path())
            .collect::<Vec<_>>();

        bail!("top-level doesn't contains any .sql file: {:?}", files);
    }

    // Init Tera template
    let tera = get_tera(args.file.clone(), working_dir)?;

    // For debug
    let loaded_template: Vec<_> = tera.get_template_names().collect();
    debug!("loaded templates: {:?}", loaded_template);

    // TODO: Tera context
    let context = tera::Context::new();

    // Render the index.sql file if the target path is a folder
    let endpoint = if is_dir {
        format!("{}/index.sql", path_str)
            .trim_start_matches('/')
            .to_string()
    } else {
        path_str.to_string()
    };

    let out = tera
        .render(&endpoint, &context)
        .with_context(|| format!("failed to render from {}", path_str))?;

    Ok(out.trim().to_string())
}

fn get_dirs(args: Build) -> Result<(PathBuf, String)> {
    let path = &args.file;

    // Working directory (context directory)
    let working_dir = get_current_working_dir(args.context)?;
    debug!("Working dir: {}", &working_dir.display());

    // Get path_str (without context directory prefix)
    let path_str = get_full_path_str(path)?;
    let working_dir_str = working_dir.to_str().expect("could not get working dir str");
    let path_str = path_str
        .trim_start_matches(working_dir_str)
        .trim_start_matches('/')
        .to_string();

    Ok((working_dir, path_str))
}