1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! text_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, YarnTextError> {
19 let trimmed = input.trim();
20 if trimmed.is_empty() {
21 Err(YarnTextError::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}
35
36#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub enum YarnCommand {
39 Install,
40 Add,
41 Run,
42 Test,
43 Workspaces,
44 Publish,
45}
46
47impl FromStr for YarnCommand {
48 type Err = YarnTextError;
49
50 fn from_str(input: &str) -> Result<Self, Self::Err> {
51 match normalized(input)?.as_str() {
52 "install" => Ok(Self::Install),
53 "add" => Ok(Self::Add),
54 "run" => Ok(Self::Run),
55 "test" => Ok(Self::Test),
56 "workspaces" | "workspace" => Ok(Self::Workspaces),
57 "publish" => Ok(Self::Publish),
58 _ => Err(YarnTextError::Unknown),
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub enum YarnVersionFamily {
66 Classic,
67 Berry,
68}
69
70impl YarnVersionFamily {
71 #[must_use]
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Classic => "classic",
76 Self::Berry => "berry",
77 }
78 }
79}
80
81text_newtype!(YarnWorkspace);
82
83#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub enum YarnLockfile {
86 YarnLock,
87}
88
89impl YarnLockfile {
90 #[must_use]
92 pub const fn as_str(self) -> &'static str {
93 "yarn.lock"
94 }
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum YarnTextError {
100 Empty,
101 Unknown,
102}
103
104impl fmt::Display for YarnTextError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Empty => formatter.write_str("Yarn metadata text cannot be empty"),
108 Self::Unknown => formatter.write_str("unknown Yarn command"),
109 }
110 }
111}
112
113impl Error for YarnTextError {}
114
115fn normalized(input: &str) -> Result<String, YarnTextError> {
116 let trimmed = input.trim();
117 if trimmed.is_empty() {
118 Err(YarnTextError::Empty)
119 } else {
120 Ok(trimmed.to_ascii_lowercase())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::{YarnCommand, YarnLockfile, YarnVersionFamily, YarnWorkspace};
127
128 #[test]
129 fn models_yarn_metadata() -> Result<(), Box<dyn std::error::Error>> {
130 assert_eq!("install".parse::<YarnCommand>()?, YarnCommand::Install);
131 assert_eq!(YarnVersionFamily::Classic.as_str(), "classic");
132 assert_eq!(YarnWorkspace::new("packages/*")?.as_str(), "packages/*");
133 assert_eq!(YarnLockfile::YarnLock.as_str(), "yarn.lock");
134 Ok(())
135 }
136}