Skip to main content

use_php_autoload/
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! 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/// PSR-4 namespace prefix metadata.
38#[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/// PSR-4 prefix-to-path mapping metadata.
73#[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/// Classmap entry metadata.
102#[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/// Composer-style files autoload entry metadata.
126#[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/// PHP autoload strategy metadata.
142#[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/// Lightweight autoload configuration metadata.
168#[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/// Error returned when PHP autoload metadata is invalid.
209#[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}