Skip to main content

use_php_extension/
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! extension_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, PhpExtensionError> {
14                let trimmed = input.trim();
15                if trimmed.is_empty() {
16                    Err(PhpExtensionError::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        impl FromStr for $name {
34            type Err = PhpExtensionError;
35
36            fn from_str(input: &str) -> Result<Self, Self::Err> {
37                Self::new(input)
38            }
39        }
40    };
41}
42
43extension_text_newtype!(PhpExtensionName);
44extension_text_newtype!(PhpVersionConstraint);
45
46/// PHP extension kind metadata.
47#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub enum PhpExtensionKind {
49    Core,
50    Bundled,
51    Pecl,
52    Zend,
53    Userland,
54    Unknown,
55}
56
57impl PhpExtensionKind {
58    pub const fn as_str(self) -> &'static str {
59        match self {
60            Self::Core => "core",
61            Self::Bundled => "bundled",
62            Self::Pecl => "pecl",
63            Self::Zend => "zend",
64            Self::Userland => "userland",
65            Self::Unknown => "unknown",
66        }
67    }
68}
69
70/// PHP extension requirement kind metadata.
71#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub enum PhpExtensionRequirementKind {
73    Required,
74    Optional,
75    Conflict,
76}
77
78impl PhpExtensionRequirementKind {
79    pub const fn as_str(self) -> &'static str {
80        match self {
81            Self::Required => "required",
82            Self::Optional => "optional",
83            Self::Conflict => "conflict",
84        }
85    }
86}
87
88/// PHP extension requirement metadata.
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct PhpExtensionRequirement {
91    name: PhpExtensionName,
92    requirement_kind: PhpExtensionRequirementKind,
93    extension_kind: PhpExtensionKind,
94    version: Option<PhpVersionConstraint>,
95}
96
97impl PhpExtensionRequirement {
98    pub const fn required(name: PhpExtensionName) -> Self {
99        Self {
100            name,
101            requirement_kind: PhpExtensionRequirementKind::Required,
102            extension_kind: PhpExtensionKind::Unknown,
103            version: None,
104        }
105    }
106
107    pub const fn optional(name: PhpExtensionName) -> Self {
108        Self {
109            name,
110            requirement_kind: PhpExtensionRequirementKind::Optional,
111            extension_kind: PhpExtensionKind::Unknown,
112            version: None,
113        }
114    }
115
116    pub const fn with_kind(mut self, extension_kind: PhpExtensionKind) -> Self {
117        self.extension_kind = extension_kind;
118        self
119    }
120
121    pub fn with_version(mut self, version: PhpVersionConstraint) -> Self {
122        self.version = Some(version);
123        self
124    }
125
126    pub const fn name(&self) -> &PhpExtensionName {
127        &self.name
128    }
129
130    pub const fn requirement_kind(&self) -> PhpExtensionRequirementKind {
131        self.requirement_kind
132    }
133
134    pub const fn extension_kind(&self) -> PhpExtensionKind {
135        self.extension_kind
136    }
137
138    pub const fn version(&self) -> Option<&PhpVersionConstraint> {
139        self.version.as_ref()
140    }
141
142    pub const fn is_required(&self) -> bool {
143        matches!(self.requirement_kind, PhpExtensionRequirementKind::Required)
144    }
145}
146
147/// Error returned when PHP extension metadata is invalid.
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub enum PhpExtensionError {
150    Empty,
151}
152
153impl fmt::Display for PhpExtensionError {
154    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155        formatter.write_str("PHP extension metadata cannot be empty")
156    }
157}
158
159impl Error for PhpExtensionError {}
160
161#[cfg(test)]
162mod tests {
163    use super::{
164        PhpExtensionError, PhpExtensionKind, PhpExtensionName, PhpExtensionRequirement,
165        PhpVersionConstraint,
166    };
167
168    #[test]
169    fn builds_extension_requirement() -> Result<(), PhpExtensionError> {
170        let requirement = PhpExtensionRequirement::required(PhpExtensionName::new("mbstring")?)
171            .with_kind(PhpExtensionKind::Bundled)
172            .with_version(PhpVersionConstraint::new("*")?);
173
174        assert_eq!(requirement.name().as_str(), "mbstring");
175        assert!(requirement.is_required());
176        assert_eq!(requirement.extension_kind(), PhpExtensionKind::Bundled);
177        Ok(())
178    }
179}