1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::DbError;
8
9#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
15pub enum PropertyType {
16 Boolean,
18 Integer,
20 Text,
22}
23
24#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
30pub enum PropertyValue {
31 Boolean(bool),
33 Integer(i64),
35 Text(String),
37}
38
39impl PropertyValue {
40 #[must_use]
46 pub const fn value_type(&self) -> PropertyType {
47 match self {
48 Self::Boolean(_value) => PropertyType::Boolean,
49 Self::Integer(_value) => PropertyType::Integer,
50 Self::Text(_value) => PropertyType::Text,
51 }
52 }
53
54 #[must_use]
60 pub fn as_text(&self) -> Option<&str> {
61 match self {
62 Self::Text(value) => Some(value),
63 Self::Boolean(_) | Self::Integer(_) => None,
64 }
65 }
66
67 #[must_use]
73 pub const fn as_int(&self) -> Option<i64> {
74 match self {
75 Self::Integer(value) => Some(*value),
76 Self::Boolean(_) | Self::Text(_) => None,
77 }
78 }
79
80 #[must_use]
87 pub fn as_count(&self) -> Option<usize> {
88 match self {
89 Self::Integer(value) => usize::try_from(*value).ok(),
90 Self::Boolean(_) | Self::Text(_) => None,
91 }
92 }
93
94 #[must_use]
100 pub const fn as_bool(&self) -> Option<bool> {
101 match self {
102 Self::Boolean(value) => Some(*value),
103 Self::Integer(_) | Self::Text(_) => None,
104 }
105 }
106}
107
108impl From<bool> for PropertyValue {
109 fn from(value: bool) -> Self {
115 Self::Boolean(value)
116 }
117}
118
119impl From<i64> for PropertyValue {
120 fn from(value: i64) -> Self {
126 Self::Integer(value)
127 }
128}
129
130impl From<&str> for PropertyValue {
131 fn from(value: &str) -> Self {
137 Self::Text(value.to_owned())
138 }
139}
140
141impl From<String> for PropertyValue {
142 fn from(value: String) -> Self {
148 Self::Text(value)
149 }
150}
151
152impl TryFrom<u64> for PropertyValue {
153 type Error = DbError;
154
155 fn try_from(value: u64) -> Result<Self, Self::Error> {
165 i64::try_from(value)
166 .map(Self::Integer)
167 .map_err(|_overflow| DbError::ValueOutOfRange)
168 }
169}
170
171impl TryFrom<usize> for PropertyValue {
172 type Error = DbError;
173
174 fn try_from(value: usize) -> Result<Self, Self::Error> {
185 i64::try_from(value)
186 .map(Self::Integer)
187 .map_err(|_overflow| DbError::ValueOutOfRange)
188 }
189}
190
191impl fmt::Display for PropertyValue {
192 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::Boolean(value) => write!(formatter, "{value}"),
195 Self::Integer(value) => write!(formatter, "{value}"),
196 Self::Text(value) => formatter.write_str(value),
197 }
198 }
199}
200
201pub(crate) fn parse_value_token(token: &str) -> Result<PropertyValue, String> {
211 let trimmed = token.trim();
212 if trimmed == "true" {
213 return Ok(PropertyValue::Boolean(true));
214 }
215 if trimmed == "false" {
216 return Ok(PropertyValue::Boolean(false));
217 }
218 if let Ok(value) = trimmed.parse::<i64>() {
219 return Ok(PropertyValue::Integer(value));
220 }
221 parse_quoted(trimmed).map(PropertyValue::Text)
222}
223
224fn parse_quoted(token: &str) -> Result<String, String> {
226 let single = token
227 .strip_prefix('\'')
228 .and_then(|text| text.strip_suffix('\''));
229 let double = token
230 .strip_prefix('"')
231 .and_then(|text| text.strip_suffix('"'));
232 single
233 .or(double)
234 .map_or_else(|| Err(token.to_owned()), |value| Ok(value.to_owned()))
235}
236
237#[cfg(test)]
238mod tests {
239 use proptest::prelude::*;
240
241 use super::*;
242
243 #[test]
244 fn from_scalars_roundtrip_through_accessors() {
245 assert_eq!(PropertyValue::from(true).as_bool(), Some(true));
246 assert_eq!(PropertyValue::from(7_i64).as_int(), Some(7));
247 assert_eq!(PropertyValue::from("hi").as_text(), Some("hi"));
248 assert_eq!(
249 PropertyValue::from(String::from("hi")).as_text(),
250 Some("hi")
251 );
252 }
253
254 #[test]
255 fn accessors_reject_mismatched_types() {
256 let text = PropertyValue::from("x");
257 assert_eq!(text.as_int(), None);
258 assert_eq!(text.as_bool(), None);
259 assert_eq!(text.as_count(), None);
260 }
261
262 proptest! {
263 #[test]
264 fn integer_roundtrips(value in any::<i64>()) {
265 prop_assert_eq!(PropertyValue::from(value).as_int(), Some(value));
266 }
267
268 #[test]
269 fn boolean_roundtrips(value in any::<bool>()) {
270 prop_assert_eq!(PropertyValue::from(value).as_bool(), Some(value));
271 }
272
273 #[test]
274 fn text_roundtrips(value in ".*") {
275 let parsed = PropertyValue::from(value.as_str());
276 prop_assert_eq!(parsed.as_text(), Some(value.as_str()));
277 }
278
279 #[test]
280 fn try_from_u64_matches_checked_narrowing(value in any::<u64>()) {
281 match i64::try_from(value) {
282 Ok(expected) => prop_assert_eq!(
283 PropertyValue::try_from(value).ok().and_then(|parsed| parsed.as_int()),
284 Some(expected)
285 ),
286 Err(_overflow) => prop_assert!(PropertyValue::try_from(value).is_err()),
287 }
288 }
289
290 #[test]
291 fn as_count_matches_checked_conversion(value in any::<i64>()) {
292 prop_assert_eq!(PropertyValue::Integer(value).as_count(), usize::try_from(value).ok());
293 }
294 }
295}