1use rustlavel_core::{Error, Json, Result};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum Value {
12 Null,
13 Bool(bool),
14 Int(i64),
15 Float(f64),
16 Text(String),
17 Bytes(Vec<u8>),
18 Json(Json),
20}
21
22impl Value {
23 pub fn is_null(&self) -> bool {
24 matches!(self, Value::Null)
25 }
26
27 pub fn to_sql_text(&self) -> Option<String> {
32 match self {
33 Value::Null => None,
34 Value::Bool(true) => Some("t".into()),
35 Value::Bool(false) => Some("f".into()),
36 Value::Int(n) => Some(n.to_string()),
37 Value::Float(n) => Some(n.to_string()),
38 Value::Text(s) => Some(s.clone()),
39 Value::Json(j) => Some(j.to_string()),
40 Value::Bytes(bytes) => {
42 let mut out = String::with_capacity(2 + bytes.len() * 2);
43 out.push_str("\\x");
44 for byte in bytes {
45 out.push_str(&format!("{byte:02x}"));
46 }
47 Some(out)
48 }
49 }
50 }
51
52 pub fn to_display(&self) -> String {
54 match self {
55 Value::Null => "NULL".into(),
56 Value::Bytes(bytes) => format!("<{} bytes>", bytes.len()),
57 other => other.to_sql_text().unwrap_or_default(),
58 }
59 }
60}
61
62impl fmt::Display for Value {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.write_str(&self.to_display())
65 }
66}
67
68impl From<Value> for Json {
69 fn from(value: Value) -> Json {
70 match value {
71 Value::Null => Json::Null,
72 Value::Bool(b) => Json::Bool(b),
73 Value::Int(n) => Json::Number(n as f64),
74 Value::Float(n) => Json::Number(n),
75 Value::Text(s) => Json::String(s),
76 Value::Json(j) => j,
77 Value::Bytes(bytes) => Json::String(format!("<{} bytes>", bytes.len())),
79 }
80 }
81}
82
83macro_rules! from_int {
84 ($($t:ty),*) => {
85 $(impl From<$t> for Value {
86 fn from(v: $t) -> Value {
87 Value::Int(v as i64)
88 }
89 })*
90 };
91}
92from_int!(i8, i16, i32, i64, u8, u16, u32, usize, isize);
93
94impl From<bool> for Value {
95 fn from(v: bool) -> Value {
96 Value::Bool(v)
97 }
98}
99
100impl From<f32> for Value {
101 fn from(v: f32) -> Value {
102 Value::Float(v as f64)
103 }
104}
105
106impl From<f64> for Value {
107 fn from(v: f64) -> Value {
108 Value::Float(v)
109 }
110}
111
112impl From<String> for Value {
113 fn from(v: String) -> Value {
114 Value::Text(v)
115 }
116}
117
118impl From<&str> for Value {
119 fn from(v: &str) -> Value {
120 Value::Text(v.to_string())
121 }
122}
123
124impl From<&String> for Value {
125 fn from(v: &String) -> Value {
126 Value::Text(v.clone())
127 }
128}
129
130impl From<Vec<u8>> for Value {
131 fn from(v: Vec<u8>) -> Value {
132 Value::Bytes(v)
133 }
134}
135
136impl From<Json> for Value {
137 fn from(v: Json) -> Value {
138 Value::Json(v)
139 }
140}
141
142impl<T: Into<Value>> From<Option<T>> for Value {
143 fn from(v: Option<T>) -> Value {
144 v.map_or(Value::Null, Into::into)
145 }
146}
147
148pub trait FromValue: Sized {
154 fn from_value(value: &Value) -> Result<Self>;
155}
156
157fn mismatch<T>(value: &Value) -> Result<T> {
158 Err(Error::msg(format!(
159 "cannot read a {} column as {}",
160 variant_name(value),
161 std::any::type_name::<T>()
162 )))
163}
164
165fn variant_name(value: &Value) -> &'static str {
166 match value {
167 Value::Null => "NULL",
168 Value::Bool(_) => "boolean",
169 Value::Int(_) => "integer",
170 Value::Float(_) => "float",
171 Value::Text(_) => "text",
172 Value::Bytes(_) => "bytea",
173 Value::Json(_) => "json",
174 }
175}
176
177impl FromValue for Value {
178 fn from_value(value: &Value) -> Result<Self> {
179 Ok(value.clone())
180 }
181}
182
183impl FromValue for String {
184 fn from_value(value: &Value) -> Result<Self> {
185 match value {
186 Value::Text(s) => Ok(s.clone()),
187 Value::Json(j) => Ok(j.to_string()),
188 other => mismatch(other),
189 }
190 }
191}
192
193impl FromValue for i64 {
194 fn from_value(value: &Value) -> Result<Self> {
195 match value {
196 Value::Int(n) => Ok(*n),
197 other => mismatch(other),
198 }
199 }
200}
201
202impl FromValue for i32 {
203 fn from_value(value: &Value) -> Result<Self> {
204 i64::from_value(value).map(|n| n as i32)
205 }
206}
207
208impl FromValue for f64 {
209 fn from_value(value: &Value) -> Result<Self> {
210 match value {
211 Value::Float(n) => Ok(*n),
212 Value::Int(n) => Ok(*n as f64),
213 other => mismatch(other),
214 }
215 }
216}
217
218impl FromValue for bool {
219 fn from_value(value: &Value) -> Result<Self> {
220 match value {
221 Value::Bool(b) => Ok(*b),
222 Value::Int(0) => Ok(false),
230 Value::Int(1) => Ok(true),
231 Value::Int(n) => Err(Error::msg(format!(
235 "cannot read {n} as a bool. A `bool` field maps to 0 or 1; this column holds \
236 something else, so it is probably a number rather than a flag."
237 ))),
238 other => mismatch(other),
239 }
240 }
241}
242
243impl FromValue for Vec<u8> {
244 fn from_value(value: &Value) -> Result<Self> {
245 match value {
246 Value::Bytes(bytes) => Ok(bytes.clone()),
247 Value::Text(s) => Ok(s.clone().into_bytes()),
248 other => mismatch(other),
249 }
250 }
251}
252
253impl FromValue for Json {
254 fn from_value(value: &Value) -> Result<Self> {
255 match value {
256 Value::Json(j) => Ok(j.clone()),
257 Value::Text(s) => Json::parse(s),
258 other => Ok(other.clone().into()),
259 }
260 }
261}
262
263impl<T: FromValue> FromValue for Option<T> {
266 fn from_value(value: &Value) -> Result<Self> {
267 match value {
268 Value::Null => Ok(None),
269 other => T::from_value(other).map(Some),
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn converts_from_rust_types() {
280 assert_eq!(Value::from(7i32), Value::Int(7));
281 assert_eq!(Value::from("hi"), Value::Text("hi".into()));
282 assert_eq!(Value::from(true), Value::Bool(true));
283 assert_eq!(Value::from(Option::<i64>::None), Value::Null);
284 assert_eq!(Value::from(Some(3i64)), Value::Int(3));
285 }
286
287 #[test]
288 fn reads_into_rust_types() {
289 assert_eq!(i64::from_value(&Value::Int(7)).unwrap(), 7);
290 assert_eq!(f64::from_value(&Value::Int(7)).unwrap(), 7.0);
291 assert_eq!(String::from_value(&Value::Text("a".into())).unwrap(), "a");
292 assert_eq!(Option::<i64>::from_value(&Value::Null).unwrap(), None);
293 }
294
295 #[test]
296 fn a_null_column_read_as_a_non_option_is_an_error() {
297 let error = i64::from_value(&Value::Null).unwrap_err();
298 assert!(error.to_string().contains("NULL"));
299 }
300
301 #[test]
307 fn a_mysql_tinyint_reads_into_a_bool_but_a_counter_does_not() {
308 assert!(!bool::from_value(&Value::Int(0)).unwrap());
309 assert!(bool::from_value(&Value::Int(1)).unwrap());
310 assert!(bool::from_value(&Value::Bool(true)).unwrap());
311
312 let error = bool::from_value(&Value::Int(7)).unwrap_err().to_string();
316 assert!(error.contains("cannot read 7 as a bool"), "{error}");
317 assert!(error.contains("probably a number rather than a flag"), "{error}");
318
319 assert!(bool::from_value(&Value::Text("true".into())).is_err());
321 }
322
323 #[test]
324 fn text_never_silently_becomes_a_number() {
325 assert!(i64::from_value(&Value::Text("7".into())).is_err());
326 }
327
328 #[test]
329 fn encodes_parameters_as_postgres_text() {
330 assert_eq!(Value::Bool(true).to_sql_text().as_deref(), Some("t"));
331 assert_eq!(Value::Null.to_sql_text(), None);
332 assert_eq!(Value::Bytes(vec![0xde, 0xad]).to_sql_text().as_deref(), Some("\\xdead"));
333 }
334}