Commit a0aac702 authored by Andrew Jones's avatar Andrew Jones Committed by Hero Bird

[cli] handle error when contract package already exists, friendly error messages (#177)

* [cli] handle directory already exists, display friendly errors

* [cli] rustfmt

* [cli] don't overwrite existing files

* [cli] add test

* [cli] atomic check for file exists on create

* [cli] derive From/Display impls, remove CommandErrorKind
parent 9cf5a571
......@@ -15,6 +15,7 @@ categories = ["cli", "tool"]
include = ["/Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"]
[dependencies]
derive_more = "0.14.0"
structopt = "0.2.15"
itertools = "0.8"
heck = "0.3"
......
......@@ -20,50 +20,21 @@ use std::{
};
use zip::result::ZipError;
/// The kinds of command errors.
#[derive(Debug)]
pub enum CommandErrorKind {
/// An error that can be encountered while executing commands.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum CommandError {
Io(IoError),
#[display(fmt="Command unimplemented")]
UnimplementedCommand,
#[display(fmt="Abstraction layer unimplemented")]
UnimplementedAbstractionLayer,
ZipError(ZipError),
Other(String),
}
/// An error that can be encountered while executing commands.
#[derive(Debug)]
pub struct CommandError {
kind: CommandErrorKind,
}
impl From<IoError> for CommandError {
fn from(error: IoError) -> Self {
Self {
kind: CommandErrorKind::Io(error),
}
}
}
impl From<ZipError> for CommandError {
fn from(error: ZipError) -> Self {
Self {
kind: CommandErrorKind::ZipError(error),
}
}
}
impl From<&str> for CommandError {
fn from(error: &str) -> Self {
Self {
kind: CommandErrorKind::Other(error.to_string()),
}
}
}
impl CommandError {
/// Creates a new command error from the given kind.
pub fn new(kind: CommandErrorKind) -> Self {
Self { kind }
CommandError::Other(error.into())
}
}
......
......@@ -20,7 +20,6 @@ mod new;
pub(crate) use self::{
error::{
CommandError,
CommandErrorKind,
Result,
},
new::execute_new,
......
......@@ -17,7 +17,6 @@
use crate::{
cmd::{
CommandError,
CommandErrorKind,
Result,
},
AbstractionLayer,
......@@ -36,12 +35,18 @@ use std::{
};
/// Initializes a project structure for the `lang` abstraction layer.
fn initialize_for_lang(name: &str) -> Result<()> {
fn initialize_for_lang(name: &str) -> Result<String> {
if name.contains("-") {
return Err("Contract names cannot contain hyphens".into())
}
fs::create_dir(name)?;
let out_dir = path::Path::new(name);
if out_dir.join("Cargo.toml").exists() {
return Err(format!("A Cargo package already exists in {}", name).into())
}
if !out_dir.exists() {
fs::create_dir(out_dir)?;
}
let template = include_bytes!(concat!(env!("OUT_DIR"), "/template.zip"));
let mut cursor = Cursor::new(Vec::new());
......@@ -69,7 +74,21 @@ fn initialize_for_lang(name: &str) -> Result<()> {
fs::create_dir_all(&p)?;
}
}
let mut outfile = fs::File::create(&outpath)?;
let mut outfile = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(outpath.clone())
.map_err(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
CommandError::from(format!(
"New contract file {} already exists",
outpath.display()
))
} else {
CommandError::from(e)
}
})?;
outfile.write_all(contents.as_bytes())?;
}
......@@ -84,20 +103,17 @@ fn initialize_for_lang(name: &str) -> Result<()> {
}
}
Ok(())
Ok(format!("Created contract {}", name))
}
pub(crate) fn execute_new(layer: AbstractionLayer, name: &str) -> Result<()> {
pub(crate) fn execute_new(layer: AbstractionLayer, name: &str) -> Result<String> {
match layer {
AbstractionLayer::Core => {
Err(CommandError::new(
CommandErrorKind::UnimplementedAbstractionLayer,
))
Err(CommandError::UnimplementedAbstractionLayer)
}
AbstractionLayer::Model => {
Err(CommandError::new(
CommandErrorKind::UnimplementedAbstractionLayer,
))
Err(CommandError::UnimplementedAbstractionLayer)
}
AbstractionLayer::Lang => initialize_for_lang(name),
}
......@@ -105,12 +121,42 @@ pub(crate) fn execute_new(layer: AbstractionLayer, name: &str) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_hyphenated_name() {
let result = super::initialize_for_lang("should-fail");
assert_eq!(
format!("{:?}", result),
r#"Err(CommandError { kind: Other("Contract names cannot contain hyphens") })"#
r#"Err(Other("Contract names cannot contain hyphens"))"#
)
}
#[test]
fn contract_cargo_project_already_exists() {
let name = "test_contract_cargo_project_already_exists";
let _ = super::initialize_for_lang(name);
let result = super::initialize_for_lang(name);
// clean up created files
std::fs::remove_dir_all(name).unwrap();
assert_eq!(
format!("{:?}", result),
r#"Err(Other("A Cargo package already exists in test_contract_cargo_project_already_exists"))"#
)
}
#[test]
fn dont_overwrite_existing_files_not_in_cargo_project() {
let name = "dont_overwrite_existing_files";
let dir = path::Path::new(name);
fs::create_dir_all(dir).unwrap();
fs::File::create(dir.join("build.sh")).unwrap();
let result = super::initialize_for_lang(name);
// clean up created files
std::fs::remove_dir_all(dir).unwrap();
assert_eq!(
format!("{:?}", result),
r#"Err(Other("New contract file dont_overwrite_existing_files/build.sh already exists"))"#
)
}
}
......@@ -99,22 +99,28 @@ enum Command {
},
}
fn main() -> cmd::Result<()> {
fn main() {
let Opts::Contract(args) = Opts::from_args();
match exec(args.cmd) {
Ok(msg) => println!("\t{}", msg),
Err(err) => eprintln!("error: {}", err),
}
}
fn exec(cmd: Command) -> cmd::Result<String> {
use crate::cmd::{
CommandError,
CommandErrorKind,
};
match &args.cmd {
match &cmd {
Command::New { layer, name } => cmd::execute_new(*layer, name),
Command::Build {} => {
Err(CommandError::new(CommandErrorKind::UnimplementedCommand))
Err(CommandError::UnimplementedCommand)
}
Command::Test {} => {
Err(CommandError::new(CommandErrorKind::UnimplementedCommand))
Err(CommandError::UnimplementedCommand)
}
Command::Deploy { .. } => {
Err(CommandError::new(CommandErrorKind::UnimplementedCommand))
Err(CommandError::UnimplementedCommand)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment