Skip to main content

oxgraph_db/
value.rs

1//! Typed property and query values.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::DbError;
8
9/// Supported scalar property types.
10///
11/// # Performance
12///
13/// Copying and comparing a type tag are `O(1)`.
14#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
15pub enum PropertyType {
16    /// Boolean property.
17    Boolean,
18    /// Signed 64-bit integer property.
19    Integer,
20    /// UTF-8 text property.
21    Text,
22}
23
24/// One typed property value.
25///
26/// # Performance
27///
28/// Copying is `O(value length)` for text and `O(1)` otherwise.
29#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
30pub enum PropertyValue {
31    /// Boolean value.
32    Boolean(bool),
33    /// Signed integer value.
34    Integer(i64),
35    /// UTF-8 text value.
36    Text(String),
37}
38
39impl PropertyValue {
40    /// Returns this value's type tag.
41    ///
42    /// # Performance
43    ///
44    /// This function is `O(1)`.
45    #[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    /// Returns the text payload, or `None` for a non-text value.
55    ///
56    /// # Performance
57    ///
58    /// This function is `O(1)`.
59    #[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    /// Returns the integer payload, or `None` for a non-integer value.
68    ///
69    /// # Performance
70    ///
71    /// This function is `O(1)`.
72    #[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    /// Returns the integer payload narrowed to `usize`, or `None` when the value
81    /// is not an integer or falls outside `usize` range.
82    ///
83    /// # Performance
84    ///
85    /// This function is `O(1)`.
86    #[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    /// Returns the boolean payload, or `None` for a non-boolean value.
95    ///
96    /// # Performance
97    ///
98    /// This function is `O(1)`.
99    #[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    /// Wraps a boolean value.
110    ///
111    /// # Performance
112    ///
113    /// This function is `O(1)`.
114    fn from(value: bool) -> Self {
115        Self::Boolean(value)
116    }
117}
118
119impl From<i64> for PropertyValue {
120    /// Wraps a signed 64-bit integer value.
121    ///
122    /// # Performance
123    ///
124    /// This function is `O(1)`.
125    fn from(value: i64) -> Self {
126        Self::Integer(value)
127    }
128}
129
130impl From<&str> for PropertyValue {
131    /// Copies a string slice into an owned text value.
132    ///
133    /// # Performance
134    ///
135    /// This function is `O(value length)`.
136    fn from(value: &str) -> Self {
137        Self::Text(value.to_owned())
138    }
139}
140
141impl From<String> for PropertyValue {
142    /// Takes ownership of a string as a text value.
143    ///
144    /// # Performance
145    ///
146    /// This function is `O(1)`.
147    fn from(value: String) -> Self {
148        Self::Text(value)
149    }
150}
151
152impl TryFrom<u64> for PropertyValue {
153    type Error = DbError;
154
155    /// Narrows an unsigned 64-bit value into a signed [`PropertyValue::Integer`].
156    ///
157    /// # Errors
158    ///
159    /// Returns [`DbError::ValueOutOfRange`] when the value exceeds `i64::MAX`.
160    ///
161    /// # Performance
162    ///
163    /// This function is `O(1)`.
164    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    /// Narrows a pointer-width unsigned value into a signed
175    /// [`PropertyValue::Integer`].
176    ///
177    /// # Errors
178    ///
179    /// Returns [`DbError::ValueOutOfRange`] when the value exceeds `i64::MAX`.
180    ///
181    /// # Performance
182    ///
183    /// This function is `O(1)`.
184    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
201/// Property value parser used by CLI, HTTP, `OxQL`.
202///
203/// # Errors
204///
205/// Returns the original token when it cannot be parsed as a supported value.
206///
207/// # Performance
208///
209/// This function is `O(token.len())`.
210pub(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
224/// Parses one single- or double-quoted token.
225fn 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}