From c723f4f796955a5fd328aff63323b5a98e48e2a9 Mon Sep 17 00:00:00 2001 From: rvcas Date: Thu, 22 Dec 2022 10:41:55 -0500 Subject: [PATCH] feat: redo the new command --- crates/aiken/src/cmd/error.rs | 14 +- crates/aiken/src/cmd/new.rs | 339 +++++++++++++++++++++++----------- 2 files changed, 245 insertions(+), 108 deletions(-) diff --git a/crates/aiken/src/cmd/error.rs b/crates/aiken/src/cmd/error.rs index 98c5ef44..5f72cc9f 100644 --- a/crates/aiken/src/cmd/error.rs +++ b/crates/aiken/src/cmd/error.rs @@ -1,14 +1,17 @@ use std::fmt; +use owo_colors::OwoColorize; use thiserror::Error; -#[derive(Debug, Error)] +#[derive(Debug, Error, miette::Diagnostic)] 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 { name: String, reason: InvalidProjectNameReason, }, + #[error("A project named {} already exists.", name.red())] + ProjectExists { name: String }, } #[derive(Debug, Clone, Copy)] @@ -28,9 +31,12 @@ impl fmt::Display for InvalidProjectNameReason { InvalidProjectNameReason::Format => write!( f, "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.\ - \nFor example,\n\n\taiken-lang/stdlib" + \nFor example,\n\n\t{}", + "{owner}".bright_blue(), + "{project}".bright_blue(), + "aiken-lang/stdlib".bright_blue(), ), } } diff --git a/crates/aiken/src/cmd/new.rs b/crates/aiken/src/cmd/new.rs index 0d186a2a..7a677140 100644 --- a/crates/aiken/src/cmd/new.rs +++ b/crates/aiken/src/cmd/new.rs @@ -1,8 +1,9 @@ -use indoc::formatdoc; +use aiken_project::config::PackageName; +use indoc::{formatdoc, indoc}; use miette::IntoDiagnostic; -use std::fs; -use std::io::Write; +use owo_colors::OwoColorize; use std::path::PathBuf; +use std::{fs, path::Path}; use super::error::{Error, InvalidProjectNameReason}; @@ -10,131 +11,251 @@ use super::error::{Error, InvalidProjectNameReason}; /// Create a new Aiken project pub struct Args { /// Project name - name: PathBuf, + name: String, + /// Library only + lib: bool, } -pub struct Creator { - root: PathBuf, - lib: PathBuf, - validators: PathBuf, - project_lib: PathBuf, - project_name: String, +pub fn exec(args: Args) -> miette::Result<()> { + validate_name(&args.name)?; + + let package_name = package_name_from_str(&args.name)?; + + create_project(args, &package_name)?; + + print_success_message(&package_name); + + Ok(()) } -impl Creator { - fn new(args: Args, project_name: String) -> Self { - let root = PathBuf::from(args.name.file_name().unwrap()); - let lib = root.join("lib"); - let validators = root.join("validators"); - let project_name = project_name; - let project_lib = lib.join(&project_name); - Self { - root, - lib, - validators, - project_lib, - project_name, +fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()> { + let root = PathBuf::from(&package_name.repo); + + if root.exists() { + 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 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(()) - } +fn create_lib(root: &Path, package_name: &PackageName) -> miette::Result<()> { + let lib = root.join("lib"); - fn readme(&self) -> miette::Result<()> { - write( - self.root.join("README.md"), - &format! { - r#"# {name} + fs::create_dir_all(&lib).into_diagnostic()?; -Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. + let lib_module_path = lib.join(format!("{}.ak", package_name.repo)); -For example, as `validators/always_true.ak` + fs::write( + lib_module_path, + indoc! { + r#" + pub fn hello(_name: String) -> Bool { + trace("hello") -```gleam -pub fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool {{ - True -}} -``` + True + } -Validators are named after their purpose, so one of: + test hello_1() { + hello("Human") + } + "# + }, + ) + .into_diagnostic()?; -- `script` -- `mint` -- `withdraw` -- `certify` + let nested_path = lib.join(&package_name.repo); -## Building + fs::create_dir_all(&nested_path).into_diagnostic()?; -```console -aiken build -``` + let nested_module_path = nested_path.join("util.ak"); -## Testing + fs::write( + nested_module_path, + indoc! { + r#" + pub fn is_one(a: Int) -> Bool { + trace("hi") -You can write tests in any module using the `test` keyword. For example: + a == 1 + } -```gleam -test foo() {{ - 1 + 1 == 2 -}} -``` + test is_one_1() { + is_one(1) + } + "# + }, + ) + .into_diagnostic()?; -To run all tests, simply do: + Ok(()) +} -```console -aiken check -``` +fn create_validators(root: &Path, package_name: &PackageName) -> miette::Result<()> { + let validators = root.join("validators"); -To run only tests matching the string `foo`, do: + fs::create_dir_all(&validators).into_diagnostic()?; -```console -aiken check -m foo -``` + let always_true_path = validators.join("always_true.ak"); -## Documentation + fs::write( + always_true_path, + formatdoc! { + r#" + use aiken/transaction.{{ScriptContext}} + use {name} + use {name}/util -If you're writing a library, you might want to generate an HTML documentation for it. + pub fn spend(_datum: Data, _redeemer: Data, _context: ScriptContext) -> Bool {{ + {name}.hello("World") && util.is_one(1) + }} + "#, + name = package_name.repo + }, + ) + .into_diagnostic()?; -Use: + Ok(()) +} -``` -aiken docs -``` +fn aiken_toml(root: &Path, package_name: &PackageName) -> miette::Result<()> { + let aiken_toml_path = root.join("aiken.toml"); -## Resources - -Find more on the [Aiken's user manual](https://aiken-lang.org). -"#, - name = self.project_name - }, - ) - } - - fn aiken_toml(&self) -> miette::Result<()> { - write( - self.root.join("aiken.toml"), - &formatdoc! { - r#"name = "{name}" + 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 = self.project_name, - }, - ) - } + + dependencies = [ + {{ name = "aiken-lang/stdlib", version = "main", source = "github" }}, + ] + "#, + name = package_name.to_string(), + }, + ) + .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()?; +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. + + For example, as `validators/always_true.ak` + + ```gleam + pub fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool {{ + True + }} + ``` + + Validators are named after their purpose, so one of: + + - `script` + - `mint` + - `withdraw` + - `certify` + + ## Building + + ```sh + aiken build + ``` + + ## Testing + + You can write tests in any module using the `test` keyword. For example: + + ```gleam + test foo() {{ + 1 + 1 == 2 + }} + ``` + + To run all tests, simply do: + + ```sh + aiken check + ``` + + To run only tests matching the string `foo`, do: + + ```sh + aiken check -m foo + ``` + + ## Documentation + + If you're writing a library, you might want to generate an HTML documentation for it. + + Use: + + ```sh + aiken docs + ``` + + ## Resources + + Find more on the [Aiken's user manual](https://aiken-lang.org). + "#, + name = project_name + }, + ).into_diagnostic() +} + +fn gitignore(root: &Path) -> miette::Result<()> { + let gitignore_path = root.join(".gitignore"); + + fs::write( + gitignore_path, + indoc! { + r#" + build/ + "# + }, + ) + .into_diagnostic()?; + Ok(()) } @@ -162,14 +283,24 @@ fn validate_name(name: &str) -> Result<(), Error> { } } -pub fn exec(args: Args) -> miette::Result<()> { - if !args.name.exists() { - let project_name = args.name.clone().into_os_string().into_string().unwrap(); +fn package_name_from_str(name: &str) -> Result { + let mut name_split = name.split('/'); - 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 { + name: name.to_string(), + reason: InvalidProjectNameReason::Format, + })? + .to_string(); - Ok(()) + Ok(PackageName { owner, repo }) }