use crate::generator::{GeneratedFile, Generator};
use doido_core::{anyhow, Result};
use include_dir::{include_dir, Dir, DirEntry};
static APP_TEMPLATE_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/new");
struct TemplateContext<'a> {
name: &'a str,
db_url: String,
db_url_test: String,
db_url_production: String,
sqlx_feature: &'a str,
}
fn substitute_template(template: &str, ctx: &TemplateContext<'_>) -> String {
template
.replace("{doido_name}", ctx.name)
.replace("{doido_db_url_test}", &ctx.db_url_test)
.replace("{doido_db_url_production}", &ctx.db_url_production)
.replace("{doido_db_url}", &ctx.db_url)
.replace("{doido_sqlx_feature}", ctx.sqlx_feature)
.replace("{doido_path}", crate::TEMPLATE_WORKSPACE_PATH)
}
fn collect_from_dir(
dir: &Dir<'_>,
ctx: &TemplateContext<'_>,
app_name: &str,
out: &mut Vec<GeneratedFile>,
) -> Result<()> {
for entry in dir.entries() {
match entry {
DirEntry::Dir(sub) => collect_from_dir(sub, ctx, app_name, out)?,
DirEntry::File(f) => {
let relative = f.path();
let raw = f.contents_utf8().ok_or_else(|| {
anyhow::anyhow!("template file '{}' is not valid UTF-8", relative.display())
})?;
let rendered = substitute_template(raw, ctx);
let relative = relative.to_string_lossy().replace('\\', "/");
let relative = relative.strip_suffix(".template").unwrap_or(&relative);
let disk_path = format!("{app_name}/{relative}");
out.push(GeneratedFile {
path: disk_path,
content: rendered,
});
}
}
}
Ok(())
}
struct DbDefaults {
scheme: &'static str,
user: &'static str,
password: &'static str,
port: u16,
}
fn db_defaults(backend: &str) -> Option<DbDefaults> {
match backend {
"postgres" => Some(DbDefaults {
scheme: "postgres",
user: "postgres",
password: "postgres",
port: 5432,
}),
"mysql" => Some(DbDefaults {
scheme: "mysql",
user: "root",
password: "password",
port: 3306,
}),
_ => None,
}
}
fn default_database_url(backend: &str, name: &str, env: &str) -> String {
match db_defaults(backend) {
Some(d) => {
let password = if env == "production" {
"CHANGE_ME"
} else {
d.password
};
format!(
"{}://{}:{}@localhost:{}/{}_{}",
d.scheme, d.user, password, d.port, name, env
)
}
None => format!("sqlite://db/{env}.db"),
}
}
pub struct ProjectGenerator;
impl Generator for ProjectGenerator {
fn name(&self) -> &str {
"new"
}
fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
let name = args
.first()
.copied()
.ok_or_else(|| anyhow::anyhow!("new generator requires a name argument"))?;
let database = args
.iter()
.find(|a| a.starts_with("--database="))
.and_then(|a| a.split_once('=').map(|(_, v)| v))
.unwrap_or("sqlite");
match database {
"sqlite" | "postgres" | "mysql" => {}
other => {
return Err(anyhow::anyhow!(
"Unknown database: {}. Use sqlite, postgres, or mysql.",
other
));
}
}
let db_url = default_database_url(database, name, "development");
let db_url_test = default_database_url(database, name, "test");
let db_url_production = default_database_url(database, name, "production");
let sqlx_feature = match database {
"postgres" => "postgres",
"mysql" => "mysql",
_ => "sqlite",
};
let ctx = TemplateContext {
name,
db_url,
db_url_test,
db_url_production,
sqlx_feature,
};
let mut files = Vec::new();
collect_from_dir(&APP_TEMPLATE_DIR, &ctx, name, &mut files)?;
files.sort_by(|a, b| a.path.cmp(&b.path));
Ok(files)
}
}
#[cfg(test)]
mod tests {
use super::default_database_url;
#[test]
fn postgres_url_has_default_user_password_and_port() {
assert_eq!(
default_database_url("postgres", "blog", "development"),
"postgres://postgres:postgres@localhost:5432/blog_development"
);
}
#[test]
fn mysql_url_has_default_user_password_and_port() {
assert_eq!(
default_database_url("mysql", "store", "test"),
"mysql://root:password@localhost:3306/store_test"
);
}
#[test]
fn production_password_is_a_placeholder() {
assert_eq!(
default_database_url("postgres", "blog", "production"),
"postgres://postgres:CHANGE_ME@localhost:5432/blog_production"
);
assert_eq!(
default_database_url("mysql", "store", "production"),
"mysql://root:CHANGE_ME@localhost:3306/store_production"
);
}
#[test]
fn sqlite_stays_a_bare_file_path() {
assert_eq!(
default_database_url("sqlite", "blog", "development"),
"sqlite://db/development.db"
);
}
}