Skip to main content

use_packagist/
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! packagist_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, PackagistError> {
14                let trimmed = input.trim();
15                if trimmed.is_empty() {
16                    Err(PackagistError::Empty)
17                } else {
18                    Ok(Self(trimmed.to_string()))
19                }
20            }
21
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25        }
26
27        impl fmt::Display for $name {
28            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29                formatter.write_str(self.as_str())
30            }
31        }
32    };
33}
34
35packagist_text_newtype!(PackagistVendorName);
36packagist_text_newtype!(PackagistPackageShortName);
37packagist_text_newtype!(PackagistMetadataLabel);
38
39/// Packagist package name metadata in `vendor/package` form.
40#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
41pub struct PackagistPackageName {
42    vendor: String,
43    package: String,
44}
45
46impl PackagistPackageName {
47    pub fn new(input: &str) -> Result<Self, PackagistError> {
48        let trimmed = input.trim();
49        let Some((vendor, package)) = trimmed.split_once('/') else {
50            return Err(PackagistError::InvalidPackageName);
51        };
52        if vendor.is_empty() || package.is_empty() || package.contains('/') {
53            return Err(PackagistError::InvalidPackageName);
54        }
55        Ok(Self {
56            vendor: vendor.to_string(),
57            package: package.to_string(),
58        })
59    }
60
61    pub fn vendor(&self) -> &str {
62        &self.vendor
63    }
64
65    pub fn package(&self) -> &str {
66        &self.package
67    }
68}
69
70impl fmt::Display for PackagistPackageName {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(formatter, "{}/{}", self.vendor, self.package)
73    }
74}
75
76impl FromStr for PackagistPackageName {
77    type Err = PackagistError;
78
79    fn from_str(input: &str) -> Result<Self, Self::Err> {
80        Self::new(input)
81    }
82}
83
84/// Packagist package type metadata.
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub enum PackagistPackageType {
87    Library,
88    Project,
89    Metapackage,
90    ComposerPlugin,
91    SymfonyBundle,
92    Other,
93}
94
95impl PackagistPackageType {
96    pub const fn as_str(self) -> &'static str {
97        match self {
98            Self::Library => "library",
99            Self::Project => "project",
100            Self::Metapackage => "metapackage",
101            Self::ComposerPlugin => "composer-plugin",
102            Self::SymfonyBundle => "symfony-bundle",
103            Self::Other => "other",
104        }
105    }
106}
107
108/// Packagist stability metadata.
109#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum PackagistStability {
111    Dev,
112    Alpha,
113    Beta,
114    Rc,
115    Stable,
116}
117
118impl PackagistStability {
119    pub const fn as_str(self) -> &'static str {
120        match self {
121            Self::Dev => "dev",
122            Self::Alpha => "alpha",
123            Self::Beta => "beta",
124            Self::Rc => "RC",
125            Self::Stable => "stable",
126        }
127    }
128}
129
130/// Packagist download count metadata.
131#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct PackagistDownloadCount(u64);
133
134impl PackagistDownloadCount {
135    pub const fn new(value: u64) -> Self {
136        Self(value)
137    }
138
139    pub const fn get(self) -> u64 {
140        self.0
141    }
142}
143
144/// Error returned when Packagist metadata is invalid.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum PackagistError {
147    Empty,
148    InvalidPackageName,
149}
150
151impl fmt::Display for PackagistError {
152    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            Self::Empty => formatter.write_str("Packagist metadata cannot be empty"),
155            Self::InvalidPackageName => {
156                formatter.write_str("Packagist package name must look like vendor/package")
157            },
158        }
159    }
160}
161
162impl Error for PackagistError {}
163
164#[cfg(test)]
165mod tests {
166    use super::{
167        PackagistDownloadCount, PackagistError, PackagistPackageName, PackagistPackageType,
168        PackagistStability,
169    };
170
171    #[test]
172    fn validates_package_name_and_labels() -> Result<(), PackagistError> {
173        let package = PackagistPackageName::new("symfony/console")?;
174        let downloads = PackagistDownloadCount::new(42);
175
176        assert_eq!(package.vendor(), "symfony");
177        assert_eq!(downloads.get(), 42);
178        assert_eq!(PackagistPackageType::Library.as_str(), "library");
179        assert_eq!(PackagistStability::Stable.as_str(), "stable");
180        Ok(())
181    }
182}