Skip to main content

apiplant_db/
value.rs

1//! Conversions between JSON (the wire format) and typed SQL values.
2
3use apiplant_core::{FieldType, TextCase};
4use sea_orm::sea_query::Value as SqlValue;
5
6/// Convert a JSON value into a typed SQL value for the given column type.
7/// Returns a human-readable error on a type mismatch (surfaced as a 400).
8///
9/// `case` forces a text value's case before it is stored, so the column holds
10/// one spelling of a code rather than however many the callers typed.
11pub fn json_to_sql(
12    ty: FieldType,
13    case: Option<TextCase>,
14    v: &serde_json::Value,
15) -> Result<SqlValue, String> {
16    if v.is_null() {
17        return Ok(null_for(ty));
18    }
19    Ok(match ty {
20        FieldType::String | FieldType::Text | FieldType::File => {
21            SqlValue::from(cased(case, v.as_str().ok_or("expected a string")?))
22        }
23        FieldType::Integer => SqlValue::from(
24            i32::try_from(v.as_i64().ok_or("expected an integer")?)
25                .map_err(|_| "integer out of range")?,
26        ),
27        FieldType::BigInt => SqlValue::from(v.as_i64().ok_or("expected an integer")?),
28        FieldType::Float => SqlValue::from(v.as_f64().ok_or("expected a number")?),
29        FieldType::Boolean => SqlValue::from(v.as_bool().ok_or("expected a boolean")?),
30        FieldType::Uuid | FieldType::Reference => {
31            let s = v.as_str().ok_or("expected a UUID string")?;
32            SqlValue::from(uuid::Uuid::parse_str(s).map_err(|e| e.to_string())?)
33        }
34        FieldType::Timestamp => {
35            let s = v.as_str().ok_or("expected an RFC3339 timestamp")?;
36            SqlValue::from(chrono::DateTime::parse_from_rfc3339(s).map_err(|e| e.to_string())?)
37        }
38        FieldType::Json => SqlValue::from(v.clone()),
39    })
40}
41
42/// A text value in the column's case, or unchanged when it forces none.
43fn cased(case: Option<TextCase>, value: &str) -> String {
44    match case {
45        Some(case) => case.apply(value),
46        None => value.to_string(),
47    }
48}
49
50/// The correctly-typed SQL `NULL` for a column type (Postgres cares about the
51/// type of a bound null).
52pub fn null_for(ty: FieldType) -> SqlValue {
53    match ty {
54        FieldType::String | FieldType::Text | FieldType::File => SqlValue::String(None),
55        FieldType::Integer => SqlValue::Int(None),
56        FieldType::BigInt => SqlValue::BigInt(None),
57        FieldType::Float => SqlValue::Double(None),
58        FieldType::Boolean => SqlValue::Bool(None),
59        FieldType::Uuid | FieldType::Reference => SqlValue::Uuid(None),
60        FieldType::Timestamp => SqlValue::ChronoDateTimeWithTimeZone(None),
61        FieldType::Json => SqlValue::Json(None),
62    }
63}
64
65/// Convert a raw query-string value (always a string) into a typed SQL value
66/// for a column, used for `?field=value` filtering.
67///
68/// The filter is cased the same way the column is, so `?currency=eur` finds
69/// the rows stored as `EUR`. A filter that had to be spelled the way the
70/// storage happens to be is a filter that silently returns nothing.
71pub fn string_to_sql(ty: FieldType, case: Option<TextCase>, s: &str) -> Result<SqlValue, String> {
72    Ok(match ty {
73        FieldType::String | FieldType::Text | FieldType::File => SqlValue::from(cased(case, s)),
74        FieldType::Integer => SqlValue::from(s.parse::<i32>().map_err(|_| "expected an integer")?),
75        FieldType::BigInt => SqlValue::from(s.parse::<i64>().map_err(|_| "expected an integer")?),
76        FieldType::Float => SqlValue::from(s.parse::<f64>().map_err(|_| "expected a number")?),
77        FieldType::Boolean => SqlValue::from(s.parse::<bool>().map_err(|_| "expected a boolean")?),
78        FieldType::Uuid | FieldType::Reference => {
79            SqlValue::from(uuid::Uuid::parse_str(s).map_err(|e| e.to_string())?)
80        }
81        FieldType::Timestamp => {
82            SqlValue::from(chrono::DateTime::parse_from_rfc3339(s).map_err(|e| e.to_string())?)
83        }
84        FieldType::Json => SqlValue::from(serde_json::Value::String(s.to_string())),
85    })
86}
87
88/// Best-effort conversion for *untyped* params coming from function `.so`s via
89/// the raw-query host callback (we don't know the target column type there).
90pub fn json_param(v: &serde_json::Value) -> SqlValue {
91    use serde_json::Value as J;
92    match v {
93        J::Null => SqlValue::String(None),
94        J::Bool(b) => SqlValue::from(*b),
95        J::Number(n) if n.is_i64() => SqlValue::from(n.as_i64().unwrap()),
96        J::Number(n) if n.is_u64() => SqlValue::from(n.as_u64().unwrap() as i64),
97        J::Number(n) => SqlValue::from(n.as_f64().unwrap()),
98        J::String(s) => SqlValue::from(s.clone()),
99        other => SqlValue::from(other.clone()),
100    }
101}