1use std::{fmt, sync::Arc};
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)]
37pub enum PropertyValue {
38 Boolean(bool),
40 Integer(i64),
42 Text(Arc<str>),
44}
45
46impl PropertyValue {
47 #[must_use]
53 pub const fn value_type(&self) -> PropertyType {
54 match self {
55 Self::Boolean(_value) => PropertyType::Boolean,
56 Self::Integer(_value) => PropertyType::Integer,
57 Self::Text(_value) => PropertyType::Text,
58 }
59 }
60
61 #[must_use]
67 pub fn as_text(&self) -> Option<&str> {
68 match self {
69 Self::Text(value) => Some(value.as_ref()),
70 Self::Boolean(_) | Self::Integer(_) => None,
71 }
72 }
73
74 #[must_use]
80 pub const fn as_int(&self) -> Option<i64> {
81 match self {
82 Self::Integer(value) => Some(*value),
83 Self::Boolean(_) | Self::Text(_) => None,
84 }
85 }
86
87 #[must_use]
94 pub fn as_count(&self) -> Option<usize> {
95 match self {
96 Self::Integer(value) => usize::try_from(*value).ok(),
97 Self::Boolean(_) | Self::Text(_) => None,
98 }
99 }
100
101 #[must_use]
107 pub const fn as_bool(&self) -> Option<bool> {
108 match self {
109 Self::Boolean(value) => Some(*value),
110 Self::Integer(_) | Self::Text(_) => None,
111 }
112 }
113}
114
115impl From<bool> for PropertyValue {
116 fn from(value: bool) -> Self {
122 Self::Boolean(value)
123 }
124}
125
126impl From<i64> for PropertyValue {
127 fn from(value: i64) -> Self {
133 Self::Integer(value)
134 }
135}
136
137impl From<&str> for PropertyValue {
138 fn from(value: &str) -> Self {
144 Self::Text(Arc::from(value))
145 }
146}
147
148impl From<String> for PropertyValue {
149 fn from(value: String) -> Self {
157 Self::Text(Arc::from(value))
158 }
159}
160
161impl From<Arc<str>> for PropertyValue {
162 fn from(value: Arc<str>) -> Self {
168 Self::Text(value)
169 }
170}
171
172impl TryFrom<u64> for PropertyValue {
173 type Error = DbError;
174
175 fn try_from(value: u64) -> Result<Self, Self::Error> {
186 i64::try_from(value)
187 .map(Self::Integer)
188 .map_err(|_overflow| DbError::Query(crate::error::QueryError::ValueOutOfRange))
189 }
190}
191
192impl TryFrom<usize> for PropertyValue {
193 type Error = DbError;
194
195 fn try_from(value: usize) -> Result<Self, Self::Error> {
207 i64::try_from(value)
208 .map(Self::Integer)
209 .map_err(|_overflow| DbError::Query(crate::error::QueryError::ValueOutOfRange))
210 }
211}
212
213impl fmt::Display for PropertyValue {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match self {
216 Self::Boolean(value) => write!(formatter, "{value}"),
217 Self::Integer(value) => write!(formatter, "{value}"),
218 Self::Text(value) => formatter.write_str(value),
219 }
220 }
221}
222
223pub(crate) fn parse_value_token(token: &str) -> Result<PropertyValue, String> {
233 let trimmed = token.trim();
234 if trimmed == "true" {
235 return Ok(PropertyValue::Boolean(true));
236 }
237 if trimmed == "false" {
238 return Ok(PropertyValue::Boolean(false));
239 }
240 if let Ok(value) = trimmed.parse::<i64>() {
241 return Ok(PropertyValue::Integer(value));
242 }
243 parse_quoted(trimmed).map(PropertyValue::from)
244}
245
246fn parse_quoted(token: &str) -> Result<String, String> {
248 let single = token
249 .strip_prefix('\'')
250 .and_then(|text| text.strip_suffix('\''));
251 let double = token
252 .strip_prefix('"')
253 .and_then(|text| text.strip_suffix('"'));
254 single
255 .or(double)
256 .map_or_else(|| Err(token.to_owned()), |value| Ok(value.to_owned()))
257}
258
259#[cfg(test)]
260mod tests {
261 use proptest::prelude::*;
262
263 use super::*;
264
265 #[test]
266 fn from_scalars_roundtrip_through_accessors() {
267 assert_eq!(PropertyValue::from(true).as_bool(), Some(true));
268 assert_eq!(PropertyValue::from(7_i64).as_int(), Some(7));
269 assert_eq!(PropertyValue::from("hi").as_text(), Some("hi"));
270 assert_eq!(
271 PropertyValue::from(String::from("hi")).as_text(),
272 Some("hi")
273 );
274 }
275
276 #[test]
277 fn accessors_reject_mismatched_types() {
278 let text = PropertyValue::from("x");
279 assert_eq!(text.as_int(), None);
280 assert_eq!(text.as_bool(), None);
281 assert_eq!(text.as_count(), None);
282 }
283
284 proptest! {
285 #[test]
286 fn integer_roundtrips(value in any::<i64>()) {
287 prop_assert_eq!(PropertyValue::from(value).as_int(), Some(value));
288 }
289
290 #[test]
291 fn boolean_roundtrips(value in any::<bool>()) {
292 prop_assert_eq!(PropertyValue::from(value).as_bool(), Some(value));
293 }
294
295 #[test]
296 fn text_roundtrips(value in ".*") {
297 let parsed = PropertyValue::from(value.as_str());
298 prop_assert_eq!(parsed.as_text(), Some(value.as_str()));
299 }
300
301 #[test]
302 fn try_from_u64_matches_checked_narrowing(value in any::<u64>()) {
303 match i64::try_from(value) {
304 Ok(expected) => prop_assert_eq!(
305 PropertyValue::try_from(value).ok().and_then(|parsed| parsed.as_int()),
306 Some(expected)
307 ),
308 Err(_overflow) => prop_assert!(PropertyValue::try_from(value).is_err()),
309 }
310 }
311
312 #[test]
313 fn as_count_matches_checked_conversion(value in any::<i64>()) {
314 prop_assert_eq!(PropertyValue::Integer(value).as_count(), usize::try_from(value).ok());
315 }
316 }
317}