feat: redo the new command

This commit is contained in:
rvcas 2022-12-22 10:41:55 -05:00 committed by Lucas
parent a129a8a0d3
commit c723f4f796
2 changed files with 245 additions and 108 deletions

View File

@ -1,14 +1,17 @@
use std::fmt; use std::fmt;
use owo_colors::OwoColorize;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error, miette::Diagnostic)]
pub enum Error { pub enum Error {
#[error("'{}' is not a valid project name: {}", name, reason.to_string())] #[error("{} is not a valid project name: {}", name.red(), reason.to_string())]
InvalidProjectName { InvalidProjectName {
name: String, name: String,
reason: InvalidProjectNameReason, reason: InvalidProjectNameReason,
}, },
#[error("A project named {} already exists.", name.red())]
ProjectExists { name: String },
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -28,9 +31,12 @@ impl fmt::Display for InvalidProjectNameReason {
InvalidProjectNameReason::Format => write!( InvalidProjectNameReason::Format => write!(
f, f,
"it is malformed.\n\nProjects must be named as:\n\n\t\ "it is malformed.\n\nProjects must be named as:\n\n\t\
{{repository}}/{{project}}\n\nEach part must start with a lowercase letter \ {}/{}\n\nEach part must start with a lowercase letter \
and may only contain lowercase letters, numbers, hyphens or underscores.\ and may only contain lowercase letters, numbers, hyphens or underscores.\
\nFor example,\n\n\taiken-lang/stdlib" \nFor example,\n\n\t{}",
"{owner}".bright_blue(),
"{project}".bright_blue(),
"aiken-lang/stdlib".bright_blue(),
), ),
} }
} }

View File

@ -1,8 +1,9 @@
use indoc::formatdoc; use aiken_project::config::PackageName;
use indoc::{formatdoc, indoc};
use miette::IntoDiagnostic; use miette::IntoDiagnostic;
use std::fs; use owo_colors::OwoColorize;
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, path::Path};
use super::error::{Error, InvalidProjectNameReason}; use super::error::{Error, InvalidProjectNameReason};
@ -10,47 +11,173 @@ use super::error::{Error, InvalidProjectNameReason};
/// Create a new Aiken project /// Create a new Aiken project
pub struct Args { pub struct Args {
/// Project name /// Project name
name: PathBuf, name: String,
/// Library only
lib: bool,
} }
pub struct Creator { pub fn exec(args: Args) -> miette::Result<()> {
root: PathBuf, validate_name(&args.name)?;
lib: PathBuf,
validators: PathBuf,
project_lib: PathBuf,
project_name: String,
}
impl Creator { let package_name = package_name_from_str(&args.name)?;
fn new(args: Args, project_name: String) -> Self {
let root = PathBuf::from(args.name.file_name().unwrap()); create_project(args, &package_name)?;
let lib = root.join("lib");
let validators = root.join("validators"); print_success_message(&package_name);
let project_name = project_name;
let project_lib = lib.join(&project_name);
Self {
root,
lib,
validators,
project_lib,
project_name,
}
}
fn run(&self) -> miette::Result<()> {
fs::create_dir_all(&self.lib).into_diagnostic()?;
fs::create_dir_all(&self.project_lib).into_diagnostic()?;
fs::create_dir_all(&self.validators).into_diagnostic()?;
self.aiken_toml()?;
self.readme()?;
Ok(()) Ok(())
} }
fn readme(&self) -> miette::Result<()> { fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()> {
write( let root = PathBuf::from(&package_name.repo);
self.root.join("README.md"),
&format! { if root.exists() {
r#"# {name} Err(Error::ProjectExists {
name: package_name.repo.clone(),
})?;
}
create_lib(&root, package_name)?;
if !args.lib {
create_validators(&root, package_name)?;
}
aiken_toml(&root, package_name)?;
readme(&root, &package_name.repo)?;
gitignore(&root)?;
Ok(())
}
fn print_success_message(package_name: &PackageName) {
println!(
"{}",
formatdoc! {
r#"
Your Aiken project {name} has been {s} created.
The project can be compiled and tested by running these commands:
{cd} {name}
{aiken} check
"#,
s = "successfully".bold().bright_green(),
cd = "cd".bold().purple(),
name = package_name.repo.bright_blue(),
aiken = "aiken".bold().purple(),
}
)
}
fn create_lib(root: &Path, package_name: &PackageName) -> miette::Result<()> {
let lib = root.join("lib");
fs::create_dir_all(&lib).into_diagnostic()?;
let lib_module_path = lib.join(format!("{}.ak", package_name.repo));
fs::write(
lib_module_path,
indoc! {
r#"
pub fn hello(_name: String) -> Bool {
trace("hello")
True
}
test hello_1() {
hello("Human")
}
"#
},
)
.into_diagnostic()?;
let nested_path = lib.join(&package_name.repo);
fs::create_dir_all(&nested_path).into_diagnostic()?;
let nested_module_path = nested_path.join("util.ak");
fs::write(
nested_module_path,
indoc! {
r#"
pub fn is_one(a: Int) -> Bool {
trace("hi")
a == 1
}
test is_one_1() {
is_one(1)
}
"#
},
)
.into_diagnostic()?;
Ok(())
}
fn create_validators(root: &Path, package_name: &PackageName) -> miette::Result<()> {
let validators = root.join("validators");
fs::create_dir_all(&validators).into_diagnostic()?;
let always_true_path = validators.join("always_true.ak");
fs::write(
always_true_path,
formatdoc! {
r#"
use aiken/transaction.{{ScriptContext}}
use {name}
use {name}/util
pub fn spend(_datum: Data, _redeemer: Data, _context: ScriptContext) -> Bool {{
{name}.hello("World") && util.is_one(1)
}}
"#,
name = package_name.repo
},
)
.into_diagnostic()?;
Ok(())
}
fn aiken_toml(root: &Path, package_name: &PackageName) -> miette::Result<()> {
let aiken_toml_path = root.join("aiken.toml");
fs::write(
aiken_toml_path,
formatdoc! {
r#"
name = "{name}"
version = "0.0.0"
licences = ["Apache-2.0"]
description = "Aiken contracts for project '{name}'"
dependencies = [
{{ name = "aiken-lang/stdlib", version = "main", source = "github" }},
]
"#,
name = package_name.to_string(),
},
)
.into_diagnostic()
}
fn readme(root: &Path, project_name: &str) -> miette::Result<()> {
fs::write(
root.join("README.md"),
formatdoc! {
r#"
# {name}
Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension.
@ -71,7 +198,7 @@ Validators are named after their purpose, so one of:
## Building ## Building
```console ```sh
aiken build aiken build
``` ```
@ -87,13 +214,13 @@ test foo() {{
To run all tests, simply do: To run all tests, simply do:
```console ```sh
aiken check aiken check
``` ```
To run only tests matching the string `foo`, do: To run only tests matching the string `foo`, do:
```console ```sh
aiken check -m foo aiken check -m foo
``` ```
@ -103,7 +230,7 @@ If you're writing a library, you might want to generate an HTML documentation fo
Use: Use:
``` ```sh
aiken docs aiken docs
``` ```
@ -111,30 +238,24 @@ aiken docs
Find more on the [Aiken's user manual](https://aiken-lang.org). Find more on the [Aiken's user manual](https://aiken-lang.org).
"#, "#,
name = self.project_name name = project_name
}, },
) ).into_diagnostic()
} }
fn aiken_toml(&self) -> miette::Result<()> { fn gitignore(root: &Path) -> miette::Result<()> {
write( let gitignore_path = root.join(".gitignore");
self.root.join("aiken.toml"),
&formatdoc! { fs::write(
r#"name = "{name}" gitignore_path,
version = "0.0.0" indoc! {
licences = ["Apache-2.0"] r#"
description = "Aiken contracts for project '{name}'" build/
dependencies = [] "#
"#,
name = self.project_name,
}, },
) )
} .into_diagnostic()?;
}
fn write(path: PathBuf, contents: &str) -> miette::Result<()> {
let mut f = fs::File::create(path).into_diagnostic()?;
f.write_all(contents.as_bytes()).into_diagnostic()?;
Ok(()) Ok(())
} }
@ -162,14 +283,24 @@ fn validate_name(name: &str) -> Result<(), Error> {
} }
} }
pub fn exec(args: Args) -> miette::Result<()> { fn package_name_from_str(name: &str) -> Result<PackageName, Error> {
if !args.name.exists() { let mut name_split = name.split('/');
let project_name = args.name.clone().into_os_string().into_string().unwrap();
validate_name(&project_name).into_diagnostic()?; let owner = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
Creator::new(args, project_name).run()?; let repo = name_split
} .next()
.ok_or_else(|| Error::InvalidProjectName {
Ok(()) name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
Ok(PackageName { owner, repo })
} }