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 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(),
),
}
}

View File

@ -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,47 +11,173 @@ 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)?;
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,
}
}
let package_name = package_name_from_str(&args.name)?;
create_project(args, &package_name)?;
print_success_message(&package_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(())
}
fn readme(&self) -> miette::Result<()> {
write(
self.root.join("README.md"),
&format! {
r#"# {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 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.
@ -71,7 +198,7 @@ Validators are named after their purpose, so one of:
## Building
```console
```sh
aiken build
```
@ -87,13 +214,13 @@ test foo() {{
To run all tests, simply do:
```console
```sh
aiken check
```
To run only tests matching the string `foo`, do:
```console
```sh
aiken check -m foo
```
@ -103,7 +230,7 @@ If you're writing a library, you might want to generate an HTML documentation fo
Use:
```
```sh
aiken docs
```
@ -111,30 +238,24 @@ aiken docs
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<()> {
write(
self.root.join("aiken.toml"),
&formatdoc! {
r#"name = "{name}"
version = "0.0.0"
licences = ["Apache-2.0"]
description = "Aiken contracts for project '{name}'"
dependencies = []
"#,
name = self.project_name,
fn gitignore(root: &Path) -> miette::Result<()> {
let gitignore_path = root.join(".gitignore");
fs::write(
gitignore_path,
indoc! {
r#"
build/
"#
},
)
}
}
.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(())
}
@ -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<PackageName, Error> {
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()?;
}
Ok(())
let repo = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
Ok(PackageName { owner, repo })
}