1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! string_newtype {
8 ($name:ident) => {
9 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10 pub struct $name(String);
11
12 impl $name {
13 pub fn new(input: &str) -> Result<Self, NpmTextError> {
19 let trimmed = input.trim();
20 if trimmed.is_empty() {
21 Err(NpmTextError::Empty)
22 } else {
23 Ok(Self(trimmed.to_string()))
24 }
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32 }
33
34 impl fmt::Display for $name {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter.write_str(self.as_str())
37 }
38 }
39 };
40}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub enum NpmCommand {
45 Install,
46 Ci,
47 Run,
48 Test,
49 Publish,
50 Pack,
51 Version,
52 Audit,
53}
54
55impl NpmCommand {
56 #[must_use]
58 pub const fn as_str(self) -> &'static str {
59 match self {
60 Self::Install => "install",
61 Self::Ci => "ci",
62 Self::Run => "run",
63 Self::Test => "test",
64 Self::Publish => "publish",
65 Self::Pack => "pack",
66 Self::Version => "version",
67 Self::Audit => "audit",
68 }
69 }
70}
71
72impl FromStr for NpmCommand {
73 type Err = NpmCommandParseError;
74
75 fn from_str(input: &str) -> Result<Self, Self::Err> {
76 parse_command(input, |label| match label {
77 "install" | "i" => Some(Self::Install),
78 "ci" => Some(Self::Ci),
79 "run" => Some(Self::Run),
80 "test" | "t" => Some(Self::Test),
81 "publish" => Some(Self::Publish),
82 "pack" => Some(Self::Pack),
83 "version" => Some(Self::Version),
84 "audit" => Some(Self::Audit),
85 _ => None,
86 })
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum NpmCommandParseError {
93 Empty,
94 Unknown,
95}
96
97impl fmt::Display for NpmCommandParseError {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::Empty => formatter.write_str("npm command cannot be empty"),
101 Self::Unknown => formatter.write_str("unknown npm command"),
102 }
103 }
104}
105
106impl Error for NpmCommandParseError {}
107
108string_newtype!(NpmScriptCommand);
109string_newtype!(NpmPackageSpec);
110
111#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113pub struct NpmRegistryUrl(String);
114
115impl NpmRegistryUrl {
116 pub fn new(input: &str) -> Result<Self, NpmTextError> {
122 let trimmed = input.trim();
123 if trimmed.is_empty() {
124 return Err(NpmTextError::Empty);
125 }
126 if trimmed.chars().any(char::is_whitespace) {
127 return Err(NpmTextError::ContainsWhitespace);
128 }
129 if !(trimmed.starts_with("https://") || trimmed.starts_with("http://")) {
130 return Err(NpmTextError::InvalidUrl);
131 }
132 Ok(Self(trimmed.to_string()))
133 }
134
135 #[must_use]
137 pub fn as_str(&self) -> &str {
138 &self.0
139 }
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum NpmTextError {
145 Empty,
146 ContainsWhitespace,
147 InvalidUrl,
148}
149
150impl fmt::Display for NpmTextError {
151 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
152 match self {
153 Self::Empty => formatter.write_str("npm metadata text cannot be empty"),
154 Self::ContainsWhitespace => {
155 formatter.write_str("npm metadata text cannot contain whitespace")
156 }
157 Self::InvalidUrl => {
158 formatter.write_str("npm registry URL must start with http:// or https://")
159 }
160 }
161 }
162}
163
164impl Error for NpmTextError {}
165
166fn parse_command<F>(input: &str, parse: F) -> Result<NpmCommand, NpmCommandParseError>
167where
168 F: FnOnce(&str) -> Option<NpmCommand>,
169{
170 let trimmed = input.trim();
171 if trimmed.is_empty() {
172 return Err(NpmCommandParseError::Empty);
173 }
174 parse(trimmed.to_ascii_lowercase().as_str()).ok_or(NpmCommandParseError::Unknown)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::{NpmCommand, NpmPackageSpec, NpmRegistryUrl, NpmTextError};
180
181 #[test]
182 fn models_npm_metadata() -> Result<(), Box<dyn std::error::Error>> {
183 assert_eq!("ci".parse::<NpmCommand>()?, NpmCommand::Ci);
184 assert_eq!(
185 NpmRegistryUrl::new("https://registry.npmjs.org/")?.as_str(),
186 "https://registry.npmjs.org/"
187 );
188 assert_eq!(
189 NpmPackageSpec::new("react@latest")?.as_str(),
190 "react@latest"
191 );
192 assert_eq!(
193 NpmRegistryUrl::new("registry.npmjs.org"),
194 Err(NpmTextError::InvalidUrl)
195 );
196 Ok(())
197 }
198}