Skip to main content

corium_client/
value.rs

1//! Conversions between Rust values and boundary [`Edn`], used throughout the
2//! fluent API for query constants, transaction values, and typed result
3//! extraction.
4
5use corium_core::{EntityId, Keyword, TotalF64};
6use corium_query::edn::Edn;
7
8use crate::ClientError;
9
10/// A Rust value that can be lowered into boundary [`Edn`].
11///
12/// Implemented for the scalar types that appear as query constants,
13/// transaction values, and pull/lookup arguments. This is the seam the
14/// builders use so callers write `lit(42)` or `attr("person/name")` instead
15/// of constructing [`Edn`] by hand.
16pub trait IntoEdn {
17    /// Lowers `self` into its boundary [`Edn`] form.
18    fn into_edn(self) -> Edn;
19}
20
21impl IntoEdn for Edn {
22    fn into_edn(self) -> Edn {
23        self
24    }
25}
26
27impl IntoEdn for &Edn {
28    fn into_edn(self) -> Edn {
29        self.clone()
30    }
31}
32
33impl IntoEdn for bool {
34    fn into_edn(self) -> Edn {
35        Edn::Bool(self)
36    }
37}
38
39impl IntoEdn for i64 {
40    fn into_edn(self) -> Edn {
41        Edn::Long(self)
42    }
43}
44
45impl IntoEdn for i32 {
46    fn into_edn(self) -> Edn {
47        Edn::Long(i64::from(self))
48    }
49}
50
51impl IntoEdn for u32 {
52    fn into_edn(self) -> Edn {
53        Edn::Long(i64::from(self))
54    }
55}
56
57impl IntoEdn for f64 {
58    fn into_edn(self) -> Edn {
59        Edn::Double(TotalF64(self))
60    }
61}
62
63impl IntoEdn for &str {
64    fn into_edn(self) -> Edn {
65        Edn::Str(self.to_owned())
66    }
67}
68
69impl IntoEdn for String {
70    fn into_edn(self) -> Edn {
71        Edn::Str(self)
72    }
73}
74
75impl IntoEdn for &String {
76    fn into_edn(self) -> Edn {
77        Edn::Str(self.clone())
78    }
79}
80
81impl IntoEdn for Keyword {
82    fn into_edn(self) -> Edn {
83        Edn::Keyword(self)
84    }
85}
86
87impl IntoEdn for &Keyword {
88    fn into_edn(self) -> Edn {
89        Edn::Keyword(self.clone())
90    }
91}
92
93impl IntoEdn for EntityId {
94    fn into_edn(self) -> Edn {
95        Edn::Long(i64::try_from(self.raw()).unwrap_or(i64::MAX))
96    }
97}
98
99/// A Rust value that can be read back out of boundary [`Edn`], for typed
100/// extraction from query rows and pull results.
101///
102/// # Examples
103/// ```no_run
104/// # use corium_client::{FromEdn, QueryResult};
105/// # fn demo(result: QueryResult) -> Result<(), corium_client::ClientError> {
106/// for row in result.rows() {
107///     let name: String = row.get(0)?;
108///     let age: i64 = row.get(1)?;
109///     println!("{name} is {age}");
110/// }
111/// # Ok(())
112/// # }
113/// ```
114pub trait FromEdn: Sized {
115    /// Reads `self` from a boundary [`Edn`] form.
116    ///
117    /// # Errors
118    /// Returns [`ClientError::Decode`] when the form is not the expected
119    /// shape.
120    fn from_edn(form: &Edn) -> Result<Self, ClientError>;
121}
122
123fn decode_err(what: &str, form: &Edn) -> ClientError {
124    ClientError::Decode(format!("expected {what}, got {form}"))
125}
126
127impl FromEdn for Edn {
128    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
129        Ok(form.clone())
130    }
131}
132
133impl FromEdn for bool {
134    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
135        match form {
136            Edn::Bool(value) => Ok(*value),
137            other => Err(decode_err("bool", other)),
138        }
139    }
140}
141
142impl FromEdn for i64 {
143    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
144        match form {
145            Edn::Long(value) => Ok(*value),
146            other => Err(decode_err("long", other)),
147        }
148    }
149}
150
151impl FromEdn for f64 {
152    #[allow(clippy::cast_precision_loss)]
153    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
154        match form {
155            Edn::Double(value) => Ok(value.0),
156            // A whole-number long widens for convenience; very large
157            // magnitudes lose precision, as with any i64-to-f64 conversion.
158            Edn::Long(value) => Ok(*value as Self),
159            other => Err(decode_err("double", other)),
160        }
161    }
162}
163
164impl FromEdn for String {
165    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
166        match form {
167            Edn::Str(value) => Ok(value.clone()),
168            other => Err(decode_err("string", other)),
169        }
170    }
171}
172
173impl FromEdn for Keyword {
174    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
175        match form {
176            Edn::Keyword(value) => Ok(value.clone()),
177            other => Err(decode_err("keyword", other)),
178        }
179    }
180}
181
182impl FromEdn for EntityId {
183    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
184        match form {
185            // Entity ids surface as longs at the boundary (as in Datomic).
186            Edn::Long(value) => u64::try_from(*value)
187                .map(EntityId::from_raw)
188                .map_err(|_| decode_err("entity id", form)),
189            other => Err(decode_err("entity id", other)),
190        }
191    }
192}
193
194impl<T: FromEdn> FromEdn for Option<T> {
195    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
196        match form {
197            Edn::Nil => Ok(None),
198            other => T::from_edn(other).map(Some),
199        }
200    }
201}
202
203impl<T: FromEdn> FromEdn for Vec<T> {
204    fn from_edn(form: &Edn) -> Result<Self, ClientError> {
205        match form {
206            Edn::Vector(items) | Edn::List(items) | Edn::Set(items) => {
207                items.iter().map(T::from_edn).collect()
208            }
209            other => Err(decode_err("collection", other)),
210        }
211    }
212}