aiken_project/
package_name.rs

1use 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}