1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! autoload_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, PhpAutoloadError> {
14 let trimmed = input.trim();
15 if trimmed.is_empty() {
16 Err(PhpAutoloadError::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
35autoload_text_newtype!(AutoloadPath);
36
37#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
39pub struct Psr4Prefix(String);
40
41impl Psr4Prefix {
42 pub fn new(input: &str) -> Result<Self, PhpAutoloadError> {
43 let trimmed = input.trim();
44 if trimmed.is_empty() {
45 return Err(PhpAutoloadError::Empty);
46 }
47 if !trimmed.ends_with('\\') {
48 return Err(PhpAutoloadError::InvalidPrefix);
49 }
50 Ok(Self(trimmed.to_string()))
51 }
52
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl fmt::Display for Psr4Prefix {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str(self.as_str())
61 }
62}
63
64impl FromStr for Psr4Prefix {
65 type Err = PhpAutoloadError;
66
67 fn from_str(input: &str) -> Result<Self, Self::Err> {
68 Self::new(input)
69 }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct Psr4Mapping {
75 prefix: Psr4Prefix,
76 paths: Vec<AutoloadPath>,
77}
78
79impl Psr4Mapping {
80 pub fn new(prefix: Psr4Prefix) -> Self {
81 Self {
82 prefix,
83 paths: Vec::new(),
84 }
85 }
86
87 pub fn with_path(mut self, path: AutoloadPath) -> Self {
88 self.paths.push(path);
89 self
90 }
91
92 pub const fn prefix(&self) -> &Psr4Prefix {
93 &self.prefix
94 }
95
96 pub fn paths(&self) -> &[AutoloadPath] {
97 &self.paths
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct ClassmapEntry {
104 class_name: String,
105 path: AutoloadPath,
106}
107
108impl ClassmapEntry {
109 pub fn new(class_name: &str, path: AutoloadPath) -> Self {
110 Self {
111 class_name: class_name.trim().to_string(),
112 path,
113 }
114 }
115
116 pub fn class_name(&self) -> &str {
117 &self.class_name
118 }
119
120 pub const fn path(&self) -> &AutoloadPath {
121 &self.path
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct FilesAutoloadEntry {
128 path: AutoloadPath,
129}
130
131impl FilesAutoloadEntry {
132 pub const fn new(path: AutoloadPath) -> Self {
133 Self { path }
134 }
135
136 pub const fn path(&self) -> &AutoloadPath {
137 &self.path
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
143pub enum AutoloadStrategy {
144 Psr4,
145 Classmap,
146 Files,
147 IncludePath,
148}
149
150impl AutoloadStrategy {
151 pub const fn as_str(self) -> &'static str {
152 match self {
153 Self::Psr4 => "psr-4",
154 Self::Classmap => "classmap",
155 Self::Files => "files",
156 Self::IncludePath => "include-path",
157 }
158 }
159}
160
161impl fmt::Display for AutoloadStrategy {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter.write_str(self.as_str())
164 }
165}
166
167#[derive(Clone, Debug, Default, Eq, PartialEq)]
169pub struct AutoloadConfig {
170 psr4: Vec<Psr4Mapping>,
171 classmap: Vec<ClassmapEntry>,
172 files: Vec<FilesAutoloadEntry>,
173}
174
175impl AutoloadConfig {
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn with_psr4(mut self, mapping: Psr4Mapping) -> Self {
181 self.psr4.push(mapping);
182 self
183 }
184
185 pub fn with_classmap(mut self, entry: ClassmapEntry) -> Self {
186 self.classmap.push(entry);
187 self
188 }
189
190 pub fn with_file(mut self, entry: FilesAutoloadEntry) -> Self {
191 self.files.push(entry);
192 self
193 }
194
195 pub fn psr4(&self) -> &[Psr4Mapping] {
196 &self.psr4
197 }
198
199 pub fn classmap(&self) -> &[ClassmapEntry] {
200 &self.classmap
201 }
202
203 pub fn files(&self) -> &[FilesAutoloadEntry] {
204 &self.files
205 }
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub enum PhpAutoloadError {
211 Empty,
212 InvalidPrefix,
213}
214
215impl fmt::Display for PhpAutoloadError {
216 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match self {
218 Self::Empty => formatter.write_str("PHP autoload metadata cannot be empty"),
219 Self::InvalidPrefix => {
220 formatter.write_str("PSR-4 prefixes must end with a namespace separator")
221 },
222 }
223 }
224}
225
226impl Error for PhpAutoloadError {}
227
228#[cfg(test)]
229mod tests {
230 use super::{
231 AutoloadConfig, AutoloadPath, ClassmapEntry, FilesAutoloadEntry, PhpAutoloadError,
232 Psr4Mapping, Psr4Prefix,
233 };
234
235 #[test]
236 fn builds_autoload_metadata() -> Result<(), PhpAutoloadError> {
237 let mapping =
238 Psr4Mapping::new(Psr4Prefix::new("App\\")?).with_path(AutoloadPath::new("src/")?);
239 let classmap = ClassmapEntry::new(
240 "Legacy_Class",
241 AutoloadPath::new("legacy/Legacy_Class.php")?,
242 );
243 let config = AutoloadConfig::new()
244 .with_psr4(mapping)
245 .with_classmap(classmap)
246 .with_file(FilesAutoloadEntry::new(AutoloadPath::new("bootstrap.php")?));
247
248 assert_eq!(config.psr4()[0].prefix().as_str(), "App\\");
249 assert_eq!(config.classmap()[0].class_name(), "Legacy_Class");
250 assert_eq!(config.files()[0].path().as_str(), "bootstrap.php");
251 Ok(())
252 }
253}