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}