1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! ini_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, PhpIniError> {
14 let trimmed = input.trim();
15 if trimmed.is_empty() {
16 Err(PhpIniError::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 = PhpIniError;
35
36 fn from_str(input: &str) -> Result<Self, Self::Err> {
37 Self::new(input)
38 }
39 }
40 };
41}
42
43ini_text_newtype!(PhpIniSectionName);
44ini_text_newtype!(PhpIniDirectiveName);
45
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub enum PhpIniEnvironment {
49 Production,
50 Development,
51 Testing,
52 Cli,
53 Unknown,
54}
55
56impl PhpIniEnvironment {
57 pub const fn as_str(self) -> &'static str {
58 match self {
59 Self::Production => "production",
60 Self::Development => "development",
61 Self::Testing => "testing",
62 Self::Cli => "cli",
63 Self::Unknown => "unknown",
64 }
65 }
66}
67
68impl fmt::Display for PhpIniEnvironment {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74#[derive(Clone, Debug, PartialEq)]
76pub enum PhpIniValue {
77 String(String),
78 Integer(i64),
79 Float(f64),
80 Boolean(bool),
81 Null,
82}
83
84impl fmt::Display for PhpIniValue {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::String(value) => formatter.write_str(value),
88 Self::Integer(value) => write!(formatter, "{value}"),
89 Self::Float(value) => write!(formatter, "{value}"),
90 Self::Boolean(value) => formatter.write_str(if *value { "true" } else { "false" }),
91 Self::Null => formatter.write_str("null"),
92 }
93 }
94}
95
96impl FromStr for PhpIniValue {
97 type Err = PhpIniError;
98
99 fn from_str(input: &str) -> Result<Self, Self::Err> {
100 let trimmed = input.trim().trim_matches('"');
101 if trimmed.is_empty() {
102 return Ok(Self::String(String::new()));
103 }
104 match trimmed.to_ascii_lowercase().as_str() {
105 "true" | "on" | "yes" => Ok(Self::Boolean(true)),
106 "false" | "off" | "no" => Ok(Self::Boolean(false)),
107 "null" | "none" => Ok(Self::Null),
108 _ => trimmed
109 .parse::<i64>()
110 .map(Self::Integer)
111 .or_else(|_| trimmed.parse::<f64>().map(Self::Float))
112 .or_else(|_| Ok(Self::String(trimmed.to_string()))),
113 }
114 }
115}
116
117#[derive(Clone, Debug, PartialEq)]
119pub struct PhpIniDirective {
120 section: Option<PhpIniSectionName>,
121 name: PhpIniDirectiveName,
122 value: PhpIniValue,
123}
124
125impl PhpIniDirective {
126 pub const fn new(name: PhpIniDirectiveName, value: PhpIniValue) -> Self {
127 Self {
128 section: None,
129 name,
130 value,
131 }
132 }
133
134 pub fn with_section(mut self, section: PhpIniSectionName) -> Self {
135 self.section = Some(section);
136 self
137 }
138
139 pub const fn section(&self) -> Option<&PhpIniSectionName> {
140 self.section.as_ref()
141 }
142
143 pub const fn name(&self) -> &PhpIniDirectiveName {
144 &self.name
145 }
146
147 pub const fn value(&self) -> &PhpIniValue {
148 &self.value
149 }
150}
151
152impl fmt::Display for PhpIniDirective {
153 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(formatter, "{} = {}", self.name, self.value)
155 }
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum PhpIniError {
161 Empty,
162 MissingEquals,
163}
164
165impl fmt::Display for PhpIniError {
166 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 match self {
168 Self::Empty => formatter.write_str("PHP INI metadata cannot be empty"),
169 Self::MissingEquals => formatter.write_str("PHP INI directive line must contain '='"),
170 }
171 }
172}
173
174impl Error for PhpIniError {}
175
176pub fn parse_ini_directive_line(input: &str) -> Result<PhpIniDirective, PhpIniError> {
177 let trimmed = input.trim();
178 if trimmed.is_empty() {
179 return Err(PhpIniError::Empty);
180 }
181 let Some((name, value)) = trimmed.split_once('=') else {
182 return Err(PhpIniError::MissingEquals);
183 };
184 Ok(PhpIniDirective::new(
185 PhpIniDirectiveName::new(name)?,
186 value.parse()?,
187 ))
188}
189
190#[cfg(test)]
191mod tests {
192 use super::{
193 PhpIniDirective, PhpIniDirectiveName, PhpIniEnvironment, PhpIniError, PhpIniValue,
194 parse_ini_directive_line,
195 };
196
197 #[test]
198 fn formats_and_parses_directives() -> Result<(), PhpIniError> {
199 let directive = PhpIniDirective::new(
200 PhpIniDirectiveName::new("memory_limit")?,
201 PhpIniValue::String("128M".to_string()),
202 );
203 let parsed = parse_ini_directive_line("display_errors = On")?;
204
205 assert_eq!(directive.to_string(), "memory_limit = 128M");
206 assert_eq!(parsed.value(), &PhpIniValue::Boolean(true));
207 assert_eq!(PhpIniEnvironment::Development.to_string(), "development");
208 Ok(())
209 }
210}