#![allow(clippy::never_loop)]
use crate::{
error::{Error, ErrorKind},
prelude::*,
properties::{self, Properties},
template::{Collection, Template},
};
use abscissa_core::{
clap::Parser,
fs::{self, File},
status_err, status_info, status_ok, status_warn, Command, Runnable,
};
use ident_case::RenameRule;
use std::{
io,
path::{Path, PathBuf},
process,
time::Instant,
};
#[derive(Command, Debug, Default, Parser)]
pub struct NewCommand {
app_path: Option<PathBuf>,
#[arg(short, long)]
force: bool,
#[arg(short, long)]
patch_crates_io: Option<String>,
}
impl Runnable for NewCommand {
fn run(&self) {
let started_at = Instant::now();
#[allow(clippy::redundant_closure)]
let app_properties = self.parse_options().unwrap_or_else(|e| fatal_error(e));
let app_template = Collection::default();
let branch_name = "main";
#[allow(clippy::redundant_closure)]
self.create_parent_directory()
.unwrap_or_else(|e| fatal_error(e));
let mut template_files = app_template.iter().collect::<Vec<_>>();
template_files.sort_by(|a, b| a.name().cmp(b.name()));
for template_file in &template_files {
#[allow(clippy::redundant_closure)]
self.render_template_file(&app_template, template_file, &app_properties)
.unwrap_or_else(|e| fatal_error(e));
}
#[allow(clippy::redundant_closure)]
self.run_git_init(branch_name)
.unwrap_or_else(|e| fatal_error(e));
#[allow(clippy::redundant_closure)]
self.generate_lockfile().unwrap_or_else(|e| fatal_error(e));
let duration = started_at.elapsed();
status_ok!(
"Finished",
"`{}` generated in {:.2}s",
&app_properties.name,
duration.as_secs() as f64 + f64::from(duration.subsec_nanos()) * 1e-9
);
}
}
impl NewCommand {
fn app_path(&self) -> Result<&Path, Error> {
match &self.app_path {
Some(path) => Ok(path.as_ref()),
None => fail!(ErrorKind::Path, "no app_path given"),
}
}
fn create_parent_directory(&self) -> Result<(), Error> {
let app_path = self.app_path()?;
if app_path.exists() {
if self.force {
status_info!("Exists", "`{}` (application directory)", app_path.display());
return Ok(());
} else {
fatal_error(
format_err!(
ErrorKind::Path,
"destination `{}` already exists",
app_path.display()
)
.into(),
);
}
}
fs::create_dir(app_path).map_err(|e| {
format_err!(
ErrorKind::Path,
"couldn't create {}: {}",
app_path.display(),
e
)
})?;
status_ok!(
"Created",
"`{}` (application directory)",
app_path.display()
);
Ok(())
}
fn render_template_file(
&self,
app_template: &Collection<'_>,
template_file: &Template<'_>,
app_properties: &Properties,
) -> Result<(), Error> {
let output_path_rel = template_file.output_path(app_properties);
let output_path = self.app_path()?.join(&output_path_rel);
if output_path.exists() {
if self.force {
status_warn!("overwriting: {}", output_path.display())
} else {
fatal_error(
format_err!(
ErrorKind::Path,
"file already exists: {}",
output_path.display()
)
.into(),
);
}
}
let output_dir = output_path.parent().unwrap();
fs::create_dir_all(output_dir).map_err(|e| {
format_err!(
ErrorKind::Path,
"error creating {}: {}",
output_dir.display(),
e
)
})?;
let mut output_file = File::create(&output_path).map_err(|e| {
format_err!(
ErrorKind::Path,
"couldn't create {}: {}",
output_path.display(),
e
)
})?;
app_template.render(template_file, app_properties, &mut output_file)?;
status_ok!("Created", "new file: {}", output_path_rel.display());
Ok(())
}
fn run_git_init(&self, branch_name: &str) -> Result<(), Error> {
let path = self.app_path()?;
if path.join(".git").exists() {
status_warn!("'.git' directory already exists");
return Ok(());
}
status_ok!("Running", "git init {}", path.display());
let status = process::Command::new("git")
.stdout(process::Stdio::null())
.arg("init")
.arg("-b")
.arg(branch_name)
.arg(path)
.status();
match status {
Ok(status) => {
if !status.success() {
status_warn!(
"`git init` exited with error code: {}",
status
.code()
.map(|n| n.to_string())
.unwrap_or_else(|| "unknown".to_owned())
);
}
}
Err(e) => {
if e.kind() != io::ErrorKind::NotFound {
fatal_error(
format_err!(ErrorKind::Git, "error running `git init`: {}", e).into(),
);
}
}
}
Ok(())
}
fn generate_lockfile(&self) -> Result<(), Error> {
if self.app_path()?.join("Cargo.lock").exists() {
status_warn!("'Cargo.lock already exists");
return Ok(());
}
status_ok!("Running", "cargo generate-lockfile");
let status = process::Command::new("cargo")
.stdout(process::Stdio::null())
.args(&["generate-lockfile", "--offline", "--manifest-path"])
.arg(self.app_path()?.join("Cargo.toml"))
.status();
match status {
Ok(status) => {
if !status.success() {
status_warn!(
"`cargo generate-lockfile` exited with error code: {}",
status
.code()
.map(|n| n.to_string())
.unwrap_or_else(|| "unknown".to_owned())
);
}
}
Err(e) => fatal_error(
format_err!(
ErrorKind::Cargo,
"error running `cargo generate-lockfile`: {}",
e
)
.into(),
),
}
Ok(())
}
fn parse_options(&self) -> Result<Properties, Error> {
let abscissa = properties::framework::Properties::new(abscissa_core::VERSION);
let app_path = self.app_path()?;
let app_name = app_path
.file_name()
.expect("no filename?")
.to_string_lossy()
.replace('-', "_");
let name: properties::name::App = app_name.parse().expect("no app name");
let title = RenameRule::PascalCase.apply_to_field(&name);
let description = title.clone();
let edition = properties::rust::Edition::Rust2018;
let patch_crates_io = self.patch_crates_io.clone();
let application_type = properties::name::Type::from_snake_case(app_name.clone() + "_app");
let command_type = properties::name::Type::from_snake_case(app_name.clone() + "_cmd");
let config_type = properties::name::Type::from_snake_case(app_name + "_config");
let properties = Properties {
abscissa,
name,
title,
description,
authors: vec![],
version: "0.1.0".parse().unwrap(),
edition,
patch_crates_io,
application_type,
command_type,
config_type,
};
Ok(properties)
}
}
pub fn fatal_error(err: Error) -> ! {
status_err!("{}", err);
process::exit(1)
}