diff --git a/Cargo.lock b/Cargo.lock index 2b2fc4d4..14ed619a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,7 @@ dependencies = [ "ignore", "itertools", "miette", + "owo-colors", "pallas", "pallas-traverse", "petgraph", diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index fcdb0d8f..e10ead1b 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -18,6 +18,7 @@ hex = "0.4.3" ignore = "0.4.18" itertools = "0.10.1" miette = { version = "5.3.0", features = ["fancy"] } +owo-colors = "3.5.0" pallas = "0.16.0" pallas-traverse = "0.16.0" petgraph = "0.6.2" diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index 415fe43b..7c3154ff 100644 --- a/crates/aiken-project/src/config.rs +++ b/crates/aiken-project/src/config.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::{package_name::PackageName, Error}; use aiken_lang::ast::Span; use miette::NamedSource; use serde::{de::Visitor, Deserialize, Serialize}; @@ -37,67 +37,6 @@ pub struct Dependency { pub source: Platform, } -#[derive(PartialEq, Eq, Hash, Clone)] -pub struct PackageName { - pub owner: String, - pub repo: String, -} - -impl Display for PackageName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}/{}", self.owner, self.repo) - } -} - -impl Serialize for PackageName { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for PackageName { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct PackageNameVisitor; - - impl<'de> Visitor<'de> for PackageNameVisitor { - type Value = PackageName; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter - .write_str("a string representing an owner and repo, ex: aiken-lang/stdlib") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - let mut name = v.split('/'); - - let owner = name.next().ok_or_else(|| { - serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self) - })?; - - let repo = name.next().ok_or_else(|| { - serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self) - })?; - - Ok(PackageName { - owner: owner.to_string(), - repo: repo.to_string(), - }) - } - } - - deserializer.deserialize_str(PackageNameVisitor) - } -} - impl Display for Platform { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> { match *self { diff --git a/crates/aiken-project/src/deps.rs b/crates/aiken-project/src/deps.rs index dfc82a45..3d28d20f 100644 --- a/crates/aiken-project/src/deps.rs +++ b/crates/aiken-project/src/deps.rs @@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize}; use tokio::time::Instant; use crate::{ - config::{Config, Dependency, PackageName}, + config::{Config, Dependency}, error::Error, + package_name::PackageName, paths, telemetry::{Event, EventListener}, }; diff --git a/crates/aiken-project/src/deps/downloader.rs b/crates/aiken-project/src/deps/downloader.rs index 5dd82d17..ad3868c9 100644 --- a/crates/aiken-project/src/deps/downloader.rs +++ b/crates/aiken-project/src/deps/downloader.rs @@ -4,8 +4,8 @@ use futures::future; use reqwest::Client; use crate::{ - config::PackageName, error::Error, + package_name::PackageName, paths::{self, CacheKey}, }; diff --git a/crates/aiken-project/src/deps/manifest.rs b/crates/aiken-project/src/deps/manifest.rs index 0b22b83f..7ed9140c 100644 --- a/crates/aiken-project/src/deps/manifest.rs +++ b/crates/aiken-project/src/deps/manifest.rs @@ -5,8 +5,9 @@ use miette::NamedSource; use serde::{Deserialize, Serialize}; use crate::{ - config::{Config, Dependency, PackageName, Platform}, + config::{Config, Dependency, Platform}, error::Error, + package_name::PackageName, paths, telemetry::{Event, EventListener}, }; diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index bb595375..89c572d0 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub mod format; pub mod module; pub mod options; +pub mod package_name; pub mod paths; pub mod pretty; pub mod script; @@ -18,10 +19,10 @@ use aiken_lang::{ uplc::CodeGenerator, IdGenerator, }; -use config::PackageName; use deps::UseManifest; use miette::NamedSource; use options::{CodeGenMode, Options}; +use package_name::PackageName; use pallas::{ codec::minicbor, ledger::{addresses::Address, primitives::babbage}, diff --git a/crates/aiken-project/src/package_name.rs b/crates/aiken-project/src/package_name.rs new file mode 100644 index 00000000..c468cc0d --- /dev/null +++ b/crates/aiken-project/src/package_name.rs @@ -0,0 +1,152 @@ +use owo_colors::OwoColorize; +use serde::{de::Visitor, Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; +use thiserror::Error; + +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct PackageName { + pub owner: String, + pub repo: String, +} + +impl PackageName { + fn validate(&self) -> Result<(), Error> { + let name = format!("{}/{}", self.owner, self.repo); + + if self.owner.starts_with("aiken") { + return Err(Error::InvalidProjectName { + reason: InvalidProjectNameReason::Reserved, + name, + }); + } + + let r = regex::Regex::new("^[a-z0-9_-]+$").expect("regex could not be compiled"); + + if !(r.is_match(&self.owner) && r.is_match(&self.repo)) { + return Err(Error::InvalidProjectName { + reason: InvalidProjectNameReason::Format, + name, + }); + } + + Ok(()) + } +} + +impl FromStr for PackageName { + type Err = Error; + + fn from_str(name: &str) -> Result { + let mut name_split = name.split('/'); + let owner = name_split + .next() + .ok_or_else(|| Error::InvalidProjectName { + name: name.to_string(), + reason: InvalidProjectNameReason::Format, + })? + .to_string(); + let repo = name_split + .next() + .ok_or_else(|| Error::InvalidProjectName { + name: name.to_string(), + reason: InvalidProjectNameReason::Format, + })? + .to_string(); + let package_name = PackageName { owner, repo }; + package_name.validate()?; + Ok(package_name) + } +} + +impl Display for PackageName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.owner, self.repo) + } +} + +impl Serialize for PackageName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for PackageName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct PackageNameVisitor; + + impl<'de> Visitor<'de> for PackageNameVisitor { + type Value = PackageName; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter + .write_str("a string representing an owner and repo, ex: aiken-lang/stdlib") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut name = v.split('/'); + + let owner = name.next().ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self) + })?; + + let repo = name.next().ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self) + })?; + + Ok(PackageName { + owner: owner.to_string(), + repo: repo.to_string(), + }) + } + } + + deserializer.deserialize_str(PackageNameVisitor) + } +} + +#[derive(Debug, Error, miette::Diagnostic)] +pub enum Error { + #[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)] +pub enum InvalidProjectNameReason { + Reserved, + Format, +} + +impl fmt::Display for InvalidProjectNameReason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InvalidProjectNameReason::Reserved => write!(f, "It's a reserved word in Aiken."), + InvalidProjectNameReason::Format => write!( + f, + "It is malformed.\n\nProjects must be named as:\n\n\t\ + {}/{}\n\nEach part must start with a lowercase letter \ + and may only contain lowercase letters, numbers, hyphens or underscores.\ + \nFor example,\n\n\t{}", + "{owner}".bright_blue(), + "{project}".bright_blue(), + "aiken-lang/stdlib".bright_blue(), + ), + } + } +} diff --git a/crates/aiken-project/src/paths.rs b/crates/aiken-project/src/paths.rs index 4f514e90..def9946e 100644 --- a/crates/aiken-project/src/paths.rs +++ b/crates/aiken-project/src/paths.rs @@ -1,5 +1,5 @@ use crate::deps::manifest::Package; -use crate::{config::PackageName, error::Error}; +use crate::{error::Error, package_name::PackageName}; use reqwest::Client; use std::path::PathBuf; diff --git a/crates/aiken/src/cmd/error.rs b/crates/aiken/src/cmd/error.rs deleted file mode 100644 index 5f72cc9f..00000000 --- a/crates/aiken/src/cmd/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::fmt; - -use owo_colors::OwoColorize; -use thiserror::Error; - -#[derive(Debug, Error, miette::Diagnostic)] -pub enum Error { - #[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)] -pub enum InvalidProjectNameReason { - AikenPrefix, - AikenReservedModule, - Format, -} - -impl fmt::Display for InvalidProjectNameReason { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - InvalidProjectNameReason::AikenPrefix => write!(f, "it's a reserved word in Aiken."), - InvalidProjectNameReason::AikenReservedModule => { - write!(f, "it's a reserved module name in Aiken.") - } - InvalidProjectNameReason::Format => write!( - f, - "it is malformed.\n\nProjects must be named as:\n\n\t\ - {}/{}\n\nEach part must start with a lowercase letter \ - and may only contain lowercase letters, numbers, hyphens or underscores.\ - \nFor example,\n\n\t{}", - "{owner}".bright_blue(), - "{project}".bright_blue(), - "aiken-lang/stdlib".bright_blue(), - ), - } - } -} diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 9bfd480b..ef5a6344 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -2,7 +2,6 @@ pub mod build; pub mod check; pub mod deps; pub mod docs; -pub mod error; pub mod fmt; pub mod lsp; pub mod new; diff --git a/crates/aiken/src/cmd/new.rs b/crates/aiken/src/cmd/new.rs index d84e4312..c2665365 100644 --- a/crates/aiken/src/cmd/new.rs +++ b/crates/aiken/src/cmd/new.rs @@ -1,11 +1,15 @@ -use aiken_project::config::PackageName; +use aiken_project::{ + config::Config, + package_name::{self, PackageName}, +}; use indoc::{formatdoc, indoc}; use miette::IntoDiagnostic; use owo_colors::OwoColorize; -use std::path::PathBuf; -use std::{fs, path::Path}; - -use super::error::{Error, InvalidProjectNameReason}; +use std::{ + fs, + path::{Path, PathBuf}, + str::FromStr, +}; #[derive(clap::Args)] /// Create a new Aiken project @@ -18,14 +22,9 @@ pub struct Args { } pub fn exec(args: Args) -> miette::Result<()> { - validate_name(&args.name)?; - - let package_name = package_name_from_str(&args.name)?; - + let package_name = PackageName::from_str(&args.name).into_diagnostic()?; create_project(args, &package_name)?; - print_success_message(&package_name); - Ok(()) } @@ -33,7 +32,7 @@ fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()> let root = PathBuf::from(&package_name.repo); if root.exists() { - Err(Error::ProjectExists { + Err(package_name::Error::ProjectExists { name: package_name.repo.clone(), })?; } @@ -198,49 +197,3 @@ fn gitignore(root: &Path) -> miette::Result<()> { Ok(()) } - -fn validate_name(name: &str) -> Result<(), Error> { - if name.starts_with("aiken_") { - Err(Error::InvalidProjectName { - name: name.to_string(), - reason: InvalidProjectNameReason::AikenPrefix, - }) - } else if name == "aiken" { - Err(Error::InvalidProjectName { - name: name.to_string(), - reason: InvalidProjectNameReason::AikenReservedModule, - }) - } else if !regex::Regex::new("^[a-z0-9_-]+/[a-z0-9_-]+$") - .expect("regex could not be compiled") - .is_match(name) - { - Err(Error::InvalidProjectName { - name: name.to_string(), - reason: InvalidProjectNameReason::Format, - }) - } else { - Ok(()) - } -} - -fn package_name_from_str(name: &str) -> Result { - let mut name_split = name.split('/'); - - let owner = name_split - .next() - .ok_or_else(|| Error::InvalidProjectName { - name: name.to_string(), - reason: InvalidProjectNameReason::Format, - })? - .to_string(); - - let repo = name_split - .next() - .ok_or_else(|| Error::InvalidProjectName { - name: name.to_string(), - reason: InvalidProjectNameReason::Format, - })? - .to_string(); - - Ok(PackageName { owner, repo }) -}