aiken_project/
package_name.rs1use owo_colors::{OwoColorize, Stream::Stdout};
2use serde::{Deserialize, Serialize, de::Visitor};
3use std::{
4 fmt::{self, Display},
5 str::FromStr,
6};
7use thiserror::Error;
8
9#[derive(PartialEq, Eq, Hash, Clone, Debug)]
10pub struct PackageName {
11 pub owner: String,
12 pub repo: String,
13}
14
15impl PackageName {
16 fn validate(&self) -> Result<(), Error> {
17 let r = regex::Regex::new("^[a-z0-9_-]+$").expect("regex could not be compiled");
18
19 if !(r.is_match(&self.owner) && r.is_match(&self.repo)) {
20 return Err(Error::InvalidProjectName {
21 reason: InvalidProjectNameReason::Format,
22 name: self.to_string(),
23 });
24 }
25
26 Ok(())
27 }
28
29 pub fn from_str_unchecked(name: &str) -> Result<Self, Error> {
30 let mut name_split = name.split('/');
31
32 let owner = name_split
33 .next()
34 .ok_or_else(|| Error::InvalidProjectName {
35 name: name.to_string(),
36 reason: InvalidProjectNameReason::Format,
37 })?
38 .to_string();
39
40 let repo = name_split
41 .next()
42 .ok_or_else(|| Error::InvalidProjectName {
43 name: name.to_string(),
44 reason: InvalidProjectNameReason::Format,
45 })?
46 .to_string();
47
48 Ok(PackageName { owner, repo })
49 }
50}
51
52impl FromStr for PackageName {
53 type Err = Error;
54
55 fn from_str(name: &str) -> Result<Self, Error> {
56 let package_name = Self::from_str_unchecked(name)?;
57
58 package_name.validate()?;
59
60 Ok(package_name)
61 }
62}
63
64impl Display for PackageName {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}/{}", self.owner, self.repo)
67 }
68}
69
70impl Serialize for PackageName {
71 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72 where
73 S: serde::Serializer,
74 {
75 serializer.serialize_str(&self.to_string())
76 }
77}
78
79impl<'de> Deserialize<'de> for PackageName {
80 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81 where
82 D: serde::Deserializer<'de>,
83 {
84 struct PackageNameVisitor;
85
86 impl Visitor<'_> for PackageNameVisitor {
87 type Value = PackageName;
88
89 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
90 formatter
91 .write_str("a string representing an owner and repo, ex: aiken-lang/stdlib")
92 }
93
94 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
95 where
96 E: serde::de::Error,
97 {
98 let mut name = v.split('/');
99
100 let owner = name.next().ok_or_else(|| {
101 serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
102 })?;
103
104 let repo = name.next().ok_or_else(|| {
105 serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
106 })?;
107
108 Ok(PackageName {
109 owner: owner.to_string(),
110 repo: repo.to_string(),
111 })
112 }
113 }
114
115 deserializer.deserialize_str(PackageNameVisitor)
116 }
117}
118
119#[derive(Debug, Error, miette::Diagnostic)]
120pub enum Error {
121 #[error(
122 "{} is not a valid project name: {}",
123 name.if_supports_color(Stdout, |s| s.red()),
124 reason.to_string()
125 )]
126 InvalidProjectName {
127 name: String,
128 reason: InvalidProjectNameReason,
129 },
130 #[error(
131 "A project named {} already exists.",
132 name.if_supports_color(Stdout, |s| s.red())
133 )]
134 ProjectExists { name: String },
135}
136
137#[derive(Debug, Clone, Copy)]
138pub enum InvalidProjectNameReason {
139 Reserved,
140 Format,
141}
142
143impl fmt::Display for InvalidProjectNameReason {
144 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145 match self {
146 InvalidProjectNameReason::Reserved => write!(f, "It's a reserved word in Aiken."),
147 InvalidProjectNameReason::Format => write!(
148 f,
149 "It is malformed.\n\nProjects must be named as:\n\n\t\
150 {}/{}\n\nEach part must start with a lowercase letter \
151 and may only contain lowercase letters, numbers, hyphens or underscores.\
152 \nFor example,\n\n\t{}",
153 "{owner}".if_supports_color(Stdout, |s| s.bright_blue()),
154 "{project}".if_supports_color(Stdout, |s| s.bright_blue()),
155 "aiken-lang/stdlib".if_supports_color(Stdout, |s| s.bright_blue()),
156 ),
157 }
158 }
159}