Skip to main content

systemprompt_identifiers/db_value/
from_value.rs

1//! `FromDbValue`: fallible decoding of `DbValue`s into typed values.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use chrono::{DateTime, Utc};
7
8use super::{DbValue, parse_database_datetime};
9use crate::error::DbValueError;
10
11pub trait FromDbValue: Sized {
12    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError>;
13}
14
15impl FromDbValue for String {
16    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
17        match value {
18            DbValue::String(s) => Ok(s.clone()),
19            DbValue::Int(i) => Ok(i.to_string()),
20            DbValue::Float(f) => Ok(f.to_string()),
21            DbValue::Bool(b) => Ok(b.to_string()),
22            DbValue::Timestamp(dt) => Ok(dt.to_rfc3339()),
23            DbValue::StringArray(arr) => Ok(serde_json::Value::from(arr.clone()).to_string()),
24            DbValue::NullString
25            | DbValue::NullInt
26            | DbValue::NullFloat
27            | DbValue::NullBool
28            | DbValue::NullBytes
29            | DbValue::NullTimestamp
30            | DbValue::NullStringArray => Err(DbValueError::null_for("String")),
31            DbValue::Bytes(_) => Err(DbValueError::incompatible("Bytes", "String")),
32        }
33    }
34}
35
36impl FromDbValue for i64 {
37    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
38        match value {
39            DbValue::Int(i) => Ok(*i),
40            DbValue::Float(f) => f64_to_i64_checked(*f),
41            DbValue::Bool(b) => Ok(Self::from(*b)),
42            DbValue::String(s) => s
43                .parse()
44                .map_err(|_e| DbValueError::parse(s.clone(), "i64")),
45            DbValue::StringArray(_) => Err(DbValueError::incompatible("StringArray", "i64")),
46            DbValue::Timestamp(_) => Err(DbValueError::incompatible("Timestamp", "i64")),
47            DbValue::NullString
48            | DbValue::NullInt
49            | DbValue::NullFloat
50            | DbValue::NullBool
51            | DbValue::NullBytes
52            | DbValue::NullTimestamp
53            | DbValue::NullStringArray => Err(DbValueError::null_for("i64")),
54            DbValue::Bytes(_) => Err(DbValueError::incompatible("Bytes", "i64")),
55        }
56    }
57}
58
59impl FromDbValue for i32 {
60    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
61        i64::from_db_value(value)
62            .and_then(|v| Self::try_from(v).map_err(|_e| DbValueError::out_of_range("i32")))
63    }
64}
65
66impl FromDbValue for u64 {
67    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
68        i64::from_db_value(value)
69            .and_then(|v| Self::try_from(v).map_err(|_e| DbValueError::out_of_range("u64")))
70    }
71}
72
73impl FromDbValue for u32 {
74    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
75        i64::from_db_value(value)
76            .and_then(|v| Self::try_from(v).map_err(|_e| DbValueError::out_of_range("u32")))
77    }
78}
79
80const I64_MAX_SAFE_F64: i64 = 1 << 53;
81
82const fn i64_to_f64_checked(value: i64) -> Result<f64, DbValueError> {
83    if value.abs() > I64_MAX_SAFE_F64 {
84        return Err(DbValueError::out_of_range("f64"));
85    }
86    Ok(value as f64)
87}
88
89fn f64_to_i64_checked(value: f64) -> Result<i64, DbValueError> {
90    if value.is_nan() || value.is_infinite() {
91        return Err(DbValueError::incompatible("NaN/Infinite", "i64"));
92    }
93    if value < (i64::MIN as f64) || value > (i64::MAX as f64) {
94        return Err(DbValueError::out_of_range("i64"));
95    }
96    Ok(value as i64)
97}
98
99impl FromDbValue for f64 {
100    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
101        match value {
102            DbValue::Float(f) => Ok(*f),
103            DbValue::Int(i) => i64_to_f64_checked(*i),
104            DbValue::String(s) => s
105                .parse()
106                .map_err(|_e| DbValueError::parse(s.clone(), "f64")),
107            DbValue::StringArray(_) => Err(DbValueError::incompatible("StringArray", "f64")),
108            DbValue::Timestamp(_) => Err(DbValueError::incompatible("Timestamp", "f64")),
109            DbValue::NullString
110            | DbValue::NullInt
111            | DbValue::NullFloat
112            | DbValue::NullBool
113            | DbValue::NullBytes
114            | DbValue::NullTimestamp
115            | DbValue::NullStringArray => Err(DbValueError::null_for("f64")),
116            DbValue::Bool(_) => Err(DbValueError::incompatible("Bool", "f64")),
117            DbValue::Bytes(_) => Err(DbValueError::incompatible("Bytes", "f64")),
118        }
119    }
120}
121
122impl FromDbValue for bool {
123    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
124        match value {
125            DbValue::Bool(b) => Ok(*b),
126            DbValue::Int(i) => Ok(*i != 0),
127            DbValue::String(s) => match s.to_lowercase().as_str() {
128                "true" | "1" | "yes" => Ok(true),
129                "false" | "0" | "no" => Ok(false),
130                _ => Err(DbValueError::parse(s.clone(), "bool")),
131            },
132            DbValue::StringArray(_) => Err(DbValueError::incompatible("StringArray", "bool")),
133            DbValue::Timestamp(_) => Err(DbValueError::incompatible("Timestamp", "bool")),
134            DbValue::NullString
135            | DbValue::NullInt
136            | DbValue::NullFloat
137            | DbValue::NullBool
138            | DbValue::NullBytes
139            | DbValue::NullTimestamp
140            | DbValue::NullStringArray => Err(DbValueError::null_for("bool")),
141            DbValue::Float(_) => Err(DbValueError::incompatible("Float", "bool")),
142            DbValue::Bytes(_) => Err(DbValueError::incompatible("Bytes", "bool")),
143        }
144    }
145}
146
147impl FromDbValue for Vec<u8> {
148    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
149        match value {
150            DbValue::Bytes(b) => Ok(b.clone()),
151            DbValue::String(s) => Ok(s.as_bytes().to_vec()),
152            DbValue::NullString
153            | DbValue::NullInt
154            | DbValue::NullFloat
155            | DbValue::NullBool
156            | DbValue::NullBytes
157            | DbValue::NullTimestamp
158            | DbValue::NullStringArray => Err(DbValueError::null_for("Vec<u8>")),
159            DbValue::Int(_)
160            | DbValue::Float(_)
161            | DbValue::Bool(_)
162            | DbValue::Timestamp(_)
163            | DbValue::StringArray(_) => Err(DbValueError::incompatible("non-bytes", "Vec<u8>")),
164        }
165    }
166}
167
168impl<T: FromDbValue> FromDbValue for Option<T> {
169    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
170        match value {
171            DbValue::NullString
172            | DbValue::NullInt
173            | DbValue::NullFloat
174            | DbValue::NullBool
175            | DbValue::NullBytes
176            | DbValue::NullTimestamp
177            | DbValue::NullStringArray => Ok(None),
178            DbValue::String(_)
179            | DbValue::Int(_)
180            | DbValue::Float(_)
181            | DbValue::Bool(_)
182            | DbValue::Bytes(_)
183            | DbValue::Timestamp(_)
184            | DbValue::StringArray(_) => T::from_db_value(value).map(Some),
185        }
186    }
187}
188
189impl FromDbValue for DateTime<Utc> {
190    fn from_db_value(value: &DbValue) -> Result<Self, DbValueError> {
191        match value {
192            DbValue::String(s) => parse_database_datetime(&serde_json::Value::String(s.clone()))
193                .ok_or_else(|| DbValueError::parse(s.clone(), "DateTime<Utc>")),
194            DbValue::Timestamp(dt) => Ok(*dt),
195            DbValue::Int(ts) => Self::from_timestamp(*ts, 0)
196                .ok_or_else(|| DbValueError::parse(ts.to_string(), "DateTime<Utc>")),
197            DbValue::NullString
198            | DbValue::NullInt
199            | DbValue::NullFloat
200            | DbValue::NullBool
201            | DbValue::NullBytes
202            | DbValue::NullTimestamp
203            | DbValue::NullStringArray => Err(DbValueError::null_for("DateTime<Utc>")),
204            DbValue::Float(_) | DbValue::Bool(_) | DbValue::Bytes(_) | DbValue::StringArray(_) => {
205                Err(DbValueError::incompatible("non-datetime", "DateTime<Utc>"))
206            },
207        }
208    }
209}