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#[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#[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#[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#[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#[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}