Skip to main content

use_yarn/
lib.rs

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            /// Creates non-empty Yarn text metadata.
14            ///
15            /// # Errors
16            ///
17            /// Returns [`YarnTextError::Empty`] when `input` is empty after trimming.
18            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            /// Returns the stored text.
28            #[must_use]
29            pub fn as_str(&self) -> &str {
30                &self.0
31            }
32        }
33    };
34}
35
36/// Common Yarn command labels.
37#[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/// Yarn release-line family.
64#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub enum YarnVersionFamily {
66    Classic,
67    Berry,
68}
69
70impl YarnVersionFamily {
71    /// Returns the family label.
72    #[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/// Common Yarn lockfile label.
84#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub enum YarnLockfile {
86    YarnLock,
87}
88
89impl YarnLockfile {
90    /// Returns the lockfile label.
91    #[must_use]
92    pub const fn as_str(self) -> &'static str {
93        "yarn.lock"
94    }
95}
96
97/// Error returned when Yarn text metadata is invalid.
98#[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}