Skip to main content

teaql_core/
value.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3
4use chrono::{DateTime, NaiveDate, Utc};
5pub use rust_decimal::Decimal;
6use rust_decimal::prelude::ToPrimitive;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum DataType {
10    Bool,
11    I64,
12    U64,
13    F64,
14    Decimal,
15    Text,
16    LargeText,
17    Json,
18    Date,
19    Timestamp,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum Value {
24    Null,
25    Bool(bool),
26    I64(i64),
27    U64(u64),
28    F64(f64),
29    Decimal(Decimal),
30    Text(String),
31    Json(serde_json::Value),
32    Date(NaiveDate),
33    Timestamp(DateTime<Utc>),
34    Object(BTreeMap<String, Value>),
35    List(Vec<Value>),
36    TypedNull(DataType),
37}
38
39impl From<&str> for Value {
40    fn from(value: &str) -> Self {
41        Self::Text(value.to_owned())
42    }
43}
44
45impl From<String> for Value {
46    fn from(value: String) -> Self {
47        Self::Text(value)
48    }
49}
50
51impl From<i64> for Value {
52    fn from(value: i64) -> Self {
53        Self::I64(value)
54    }
55}
56
57impl From<i32> for Value {
58    fn from(value: i32) -> Self {
59        Self::I64(i64::from(value))
60    }
61}
62
63impl From<i16> for Value {
64    fn from(value: i16) -> Self {
65        Self::I64(i64::from(value))
66    }
67}
68
69impl From<u64> for Value {
70    fn from(value: u64) -> Self {
71        Self::U64(value)
72    }
73}
74
75impl From<u32> for Value {
76    fn from(value: u32) -> Self {
77        Self::U64(u64::from(value))
78    }
79}
80
81impl From<u16> for Value {
82    fn from(value: u16) -> Self {
83        Self::U64(u64::from(value))
84    }
85}
86
87impl From<f64> for Value {
88    fn from(value: f64) -> Self {
89        Self::F64(value)
90    }
91}
92
93impl From<f32> for Value {
94    fn from(value: f32) -> Self {
95        Self::F64(f64::from(value))
96    }
97}
98
99impl From<bool> for Value {
100    fn from(value: bool) -> Self {
101        Self::Bool(value)
102    }
103}
104
105impl From<Decimal> for Value {
106    fn from(value: Decimal) -> Self {
107        Self::Decimal(value)
108    }
109}
110
111impl From<serde_json::Value> for Value {
112    fn from(value: serde_json::Value) -> Self {
113        Self::Json(value)
114    }
115}
116
117impl From<NaiveDate> for Value {
118    fn from(value: NaiveDate) -> Self {
119        Self::Date(value)
120    }
121}
122
123impl From<DateTime<Utc>> for Value {
124    fn from(value: DateTime<Utc>) -> Self {
125        Self::Timestamp(value)
126    }
127}
128
129impl Value {
130    pub fn object(record: crate::Record) -> Self {
131        Self::Object(record)
132    }
133
134    pub fn try_i64(&self) -> Option<i64> {
135        match self {
136            Self::I64(value) => Some(*value),
137            Self::U64(value) => i64::try_from(*value).ok(),
138            Self::Decimal(value) => value.to_i64(),
139            _ => None,
140        }
141    }
142
143    pub fn try_u64(&self) -> Option<u64> {
144        match self {
145            Self::U64(value) => Some(*value),
146            Self::I64(value) => u64::try_from(*value).ok(),
147            Self::Decimal(value) => value.to_u64(),
148            _ => None,
149        }
150    }
151
152    pub fn try_decimal(&self) -> Option<Decimal> {
153        match self {
154            Self::Decimal(value) => Some(*value),
155            Self::I64(value) => Some(Decimal::from(*value)),
156            Self::U64(value) => Some(Decimal::from(*value)),
157            Self::Text(value) => Decimal::from_str(value).ok(),
158            _ => None,
159        }
160    }
161
162    pub fn try_f64(&self) -> Option<f64> {
163        match self {
164            Self::F64(value) => Some(*value),
165            Self::I64(value) => Some(*value as f64),
166            Self::U64(value) => Some(*value as f64),
167            Self::Decimal(value) => value.to_f64(),
168            _ => None,
169        }
170    }
171
172    pub fn try_text(&self) -> Option<&str> {
173        match self {
174            Self::Text(value) => Some(value),
175            _ => None,
176        }
177    }
178
179    pub fn try_bool(&self) -> Option<bool> {
180        match self {
181            Self::Bool(value) => Some(*value),
182            _ => None,
183        }
184    }
185
186    pub fn try_date(&self) -> Option<NaiveDate> {
187        match self {
188            Self::Date(value) => Some(*value),
189            Self::Text(value) => {
190                if let Ok(nd) = chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d") {
191                    return Some(nd);
192                }
193                None
194            }
195            _ => None,
196        }
197    }
198
199    pub fn try_timestamp(&self) -> Option<DateTime<Utc>> {
200        match self {
201            Self::Timestamp(value) => Some(*value),
202            Self::Text(value) => {
203                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(value) {
204                    return Some(dt.with_timezone(&chrono::Utc));
205                }
206                if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
207                    return Some(chrono::DateTime::from_naive_utc_and_offset(
208                        ndt,
209                        chrono::Utc,
210                    ));
211                }
212                if let Ok(nd) = chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d") {
213                    let ndt = nd.and_hms_opt(0, 0, 0)?;
214                    return Some(chrono::DateTime::from_naive_utc_and_offset(
215                        ndt,
216                        chrono::Utc,
217                    ));
218                }
219                None
220            }
221            _ => None,
222        }
223    }
224
225    pub fn to_json_value(&self) -> serde_json::Value {
226        match self {
227            Self::Null => serde_json::Value::Null,
228            Self::Bool(value) => serde_json::Value::Bool(*value),
229            Self::I64(value) => serde_json::Value::from(*value),
230            Self::U64(value) => serde_json::Value::from(*value),
231            Self::F64(value) => serde_json::Number::from_f64(*value)
232                .map(serde_json::Value::Number)
233                .unwrap_or(serde_json::Value::Null),
234            Self::Decimal(value) => serde_json::Value::String(value.to_string()),
235            Self::Text(value) => serde_json::Value::String(value.clone()),
236            Self::Json(value) => value.clone(),
237            Self::Date(value) => serde_json::Value::String(value.to_string()),
238            Self::Timestamp(value) => serde_json::Value::String(value.to_rfc3339()),
239            Self::Object(record) => crate::record_to_json_value(record),
240            Self::List(values) => {
241                serde_json::Value::Array(values.iter().map(Value::to_json_value).collect())
242            }
243            Self::TypedNull(_) => serde_json::Value::Null,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn value_try_i64_accepts_representable_numeric_variants() {
254        assert_eq!(Value::I64(i64::MIN).try_i64(), Some(i64::MIN));
255        assert_eq!(Value::I64(i64::MAX).try_i64(), Some(i64::MAX));
256        assert_eq!(Value::U64(i64::MAX as u64).try_i64(), Some(i64::MAX));
257        assert_eq!(Value::Decimal(Decimal::from(-42)).try_i64(), Some(-42));
258    }
259
260    #[test]
261    fn value_try_i64_rejects_unsigned_overflow_and_unrelated_variants() {
262        assert_eq!(Value::U64(i64::MAX as u64 + 1).try_i64(), None);
263        assert_eq!(Value::U64(u64::MAX).try_i64(), None);
264        assert_eq!(Value::F64(42.0).try_i64(), None);
265        assert_eq!(Value::Text("42".to_owned()).try_i64(), None);
266        assert_eq!(Value::Null.try_i64(), None);
267    }
268
269    #[test]
270    fn value_try_u64_accepts_representable_numeric_variants() {
271        assert_eq!(Value::U64(0).try_u64(), Some(0));
272        assert_eq!(Value::U64(u64::MAX).try_u64(), Some(u64::MAX));
273        assert_eq!(Value::I64(i64::MAX).try_u64(), Some(i64::MAX as u64));
274        assert_eq!(Value::Decimal(Decimal::from(42)).try_u64(), Some(42));
275    }
276
277    #[test]
278    fn value_try_u64_rejects_negative_and_unrelated_variants() {
279        assert_eq!(Value::I64(-1).try_u64(), None);
280        assert_eq!(Value::Decimal(Decimal::from(-1)).try_u64(), None);
281        assert_eq!(Value::F64(42.0).try_u64(), None);
282        assert_eq!(Value::Text("42".to_owned()).try_u64(), None);
283        assert_eq!(Value::Null.try_u64(), None);
284    }
285
286    #[test]
287    fn value_try_decimal_accepts_decimal_integer_and_text_variants() {
288        let decimal = Decimal::from_str("123.450").expect("valid decimal");
289
290        assert_eq!(Value::Decimal(decimal).try_decimal(), Some(decimal));
291        assert_eq!(
292            Value::I64(i64::MIN).try_decimal(),
293            Some(Decimal::from(i64::MIN))
294        );
295        assert_eq!(
296            Value::U64(u64::MAX).try_decimal(),
297            Some(Decimal::from(u64::MAX))
298        );
299        assert_eq!(
300            Value::Text("123.450".to_owned()).try_decimal(),
301            Some(decimal)
302        );
303    }
304
305    #[test]
306    fn value_try_decimal_rejects_invalid_text_and_unrelated_variants() {
307        assert_eq!(Value::Text("not-a-decimal".to_owned()).try_decimal(), None);
308        assert_eq!(Value::Bool(true).try_decimal(), None);
309        assert_eq!(Value::F64(1.5).try_decimal(), None);
310        assert_eq!(Value::Null.try_decimal(), None);
311    }
312
313    #[test]
314    fn value_try_f64_accepts_supported_numeric_variants() {
315        assert_eq!(Value::F64(1.25).try_f64(), Some(1.25));
316        assert_eq!(Value::I64(-2).try_f64(), Some(-2.0));
317        assert_eq!(Value::U64(2).try_f64(), Some(2.0));
318        assert_eq!(
319            Value::Decimal(Decimal::from_str("1.5").expect("valid decimal")).try_f64(),
320            Some(1.5)
321        );
322    }
323
324    #[test]
325    fn value_try_f64_rejects_unrelated_variants() {
326        assert_eq!(Value::Text("1.5".to_owned()).try_f64(), None);
327        assert_eq!(Value::Bool(true).try_f64(), None);
328        assert_eq!(Value::Null.try_f64(), None);
329    }
330
331    #[test]
332    fn value_try_date_accepts_date_and_iso_date_text() {
333        let leap_day = NaiveDate::from_ymd_opt(2024, 2, 29).expect("valid leap day");
334
335        assert_eq!(Value::Date(leap_day).try_date(), Some(leap_day));
336        assert_eq!(
337            Value::Text("2024-02-29".to_owned()).try_date(),
338            Some(leap_day)
339        );
340    }
341
342    #[test]
343    fn value_try_date_rejects_invalid_dates_and_unrelated_variants() {
344        assert_eq!(Value::Text("2023-02-29".to_owned()).try_date(), None);
345        assert_eq!(
346            Value::Text("2024-02-29T00:00:00Z".to_owned()).try_date(),
347            None
348        );
349        assert_eq!(Value::I64(20240229).try_date(), None);
350        assert_eq!(Value::Null.try_date(), None);
351    }
352
353    #[test]
354    fn value_try_timestamp_accepts_timestamp_and_supported_text_formats() {
355        let utc_timestamp = DateTime::parse_from_rfc3339("2024-01-02T03:04:05Z")
356            .expect("valid RFC 3339 timestamp")
357            .with_timezone(&Utc);
358        let offset_timestamp = DateTime::parse_from_rfc3339("2024-01-02T03:04:05+08:00")
359            .expect("valid RFC 3339 timestamp")
360            .with_timezone(&Utc);
361        let naive_timestamp = NaiveDate::from_ymd_opt(2024, 1, 2)
362            .expect("valid date")
363            .and_hms_opt(3, 4, 5)
364            .expect("valid time");
365        let midnight = NaiveDate::from_ymd_opt(2024, 1, 2)
366            .expect("valid date")
367            .and_hms_opt(0, 0, 0)
368            .expect("valid time");
369
370        assert_eq!(
371            Value::Timestamp(utc_timestamp).try_timestamp(),
372            Some(utc_timestamp)
373        );
374        assert_eq!(
375            Value::Text("2024-01-02T03:04:05+08:00".to_owned()).try_timestamp(),
376            Some(offset_timestamp)
377        );
378        assert_eq!(
379            Value::Text("2024-01-02 03:04:05".to_owned()).try_timestamp(),
380            Some(DateTime::from_naive_utc_and_offset(naive_timestamp, Utc))
381        );
382        assert_eq!(
383            Value::Text("2024-01-02".to_owned()).try_timestamp(),
384            Some(DateTime::from_naive_utc_and_offset(midnight, Utc))
385        );
386    }
387
388    #[test]
389    fn value_try_timestamp_normalizes_offsets_and_rejects_invalid_input() {
390        let expected_utc = DateTime::parse_from_rfc3339("2024-01-01T19:04:05Z")
391            .expect("valid RFC 3339 timestamp")
392            .with_timezone(&Utc);
393
394        assert_eq!(
395            Value::Text("2024-01-02T03:04:05+08:00".to_owned()).try_timestamp(),
396            Some(expected_utc)
397        );
398        assert_eq!(
399            Value::Text("2024-13-40 25:61:61".to_owned()).try_timestamp(),
400            None
401        );
402        assert_eq!(Value::Bool(true).try_timestamp(), None);
403        assert_eq!(Value::Null.try_timestamp(), None);
404    }
405}