Skip to main content

reddb_client/
types.rs

1//! Public value types — kept dependency-free on purpose so that
2//! `reddb-client` does not force a serde version on consumers.
3//!
4//! Users that want to plug serde in can implement their own
5//! conversions on top of these types. They mirror the JSON-RPC
6//! shapes documented in `PLAN_DRIVERS.md`.
7
8use std::fmt;
9
10/// A small, hand-rolled JSON value used for `insert` / `bulk_insert`
11/// payloads. We intentionally do not depend on `serde_json` so that
12/// downstream crates can pick their own serde major version.
13#[derive(Debug, Clone, PartialEq)]
14pub enum JsonValue {
15    Null,
16    Bool(bool),
17    Number(f64),
18    String(String),
19    Array(Vec<JsonValue>),
20    Object(Vec<(String, JsonValue)>),
21}
22
23impl JsonValue {
24    pub fn null() -> Self {
25        JsonValue::Null
26    }
27
28    pub fn bool(b: bool) -> Self {
29        JsonValue::Bool(b)
30    }
31
32    pub fn number(n: impl Into<f64>) -> Self {
33        JsonValue::Number(n.into())
34    }
35
36    pub fn string(s: impl Into<String>) -> Self {
37        JsonValue::String(s.into())
38    }
39
40    pub fn object<I, K>(entries: I) -> Self
41    where
42        I: IntoIterator<Item = (K, JsonValue)>,
43        K: Into<String>,
44    {
45        JsonValue::Object(entries.into_iter().map(|(k, v)| (k.into(), v)).collect())
46    }
47
48    pub fn array<I>(items: I) -> Self
49    where
50        I: IntoIterator<Item = JsonValue>,
51    {
52        JsonValue::Array(items.into_iter().collect())
53    }
54
55    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
56        match self {
57            JsonValue::Object(entries) => Some(entries.as_slice()),
58            _ => None,
59        }
60    }
61
62    pub fn to_json_string(&self) -> String {
63        let mut out = String::new();
64        write_json(self, &mut out);
65        out
66    }
67}
68
69fn write_json(value: &JsonValue, out: &mut String) {
70    match value {
71        JsonValue::Null => out.push_str("null"),
72        JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
73        JsonValue::Number(n) => {
74            if n.fract() == 0.0 && n.is_finite() {
75                out.push_str(&format!("{}", *n as i64));
76            } else {
77                out.push_str(&format!("{n}"));
78            }
79        }
80        JsonValue::String(s) => {
81            out.push('"');
82            for c in s.chars() {
83                match c {
84                    '"' => out.push_str("\\\""),
85                    '\\' => out.push_str("\\\\"),
86                    '\n' => out.push_str("\\n"),
87                    '\r' => out.push_str("\\r"),
88                    '\t' => out.push_str("\\t"),
89                    c if (c as u32) < 0x20 => {
90                        out.push_str(&format!("\\u{:04x}", c as u32));
91                    }
92                    c => out.push(c),
93                }
94            }
95            out.push('"');
96        }
97        JsonValue::Array(items) => {
98            out.push('[');
99            for (i, item) in items.iter().enumerate() {
100                if i > 0 {
101                    out.push(',');
102                }
103                write_json(item, out);
104            }
105            out.push(']');
106        }
107        JsonValue::Object(entries) => {
108            out.push('{');
109            for (i, (k, v)) in entries.iter().enumerate() {
110                if i > 0 {
111                    out.push(',');
112                }
113                write_json(&JsonValue::String(k.clone()), out);
114                out.push(':');
115                write_json(v, out);
116            }
117            out.push('}');
118        }
119    }
120}
121
122/// A scalar value as it comes out of a query. Mirrors the JSON-RPC
123/// row shape but with native Rust types.
124#[derive(Debug, Clone, PartialEq)]
125pub enum ValueOut {
126    Null,
127    Bool(bool),
128    Integer(i64),
129    Float(f64),
130    String(String),
131}
132
133impl fmt::Display for ValueOut {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            ValueOut::Null => f.write_str("null"),
137            ValueOut::Bool(b) => write!(f, "{b}"),
138            ValueOut::Integer(n) => write!(f, "{n}"),
139            ValueOut::Float(n) => write!(f, "{n}"),
140            ValueOut::String(s) => write!(f, "{s}"),
141        }
142    }
143}
144
145/// Shape returned by [`crate::Reddb::query`]. Field order matches
146/// the JSON-RPC protocol so cross-language tests are trivial.
147#[derive(Debug, Clone)]
148pub struct QueryResult {
149    pub statement: String,
150    pub affected: u64,
151    pub columns: Vec<String>,
152    pub rows: Vec<Vec<(String, ValueOut)>>,
153}
154
155pub type Row = Vec<(String, ValueOut)>;
156
157#[derive(Debug, Clone, Default)]
158pub struct ListOptions<'a> {
159    pub filter: Option<&'a str>,
160    pub order_by: Option<&'a str>,
161    pub limit: Option<u64>,
162}
163
164impl<'a> ListOptions<'a> {
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    pub fn filter(mut self, filter: &'a str) -> Self {
170        self.filter = Some(filter);
171        self
172    }
173
174    pub fn order_by(mut self, order_by: &'a str) -> Self {
175        self.order_by = Some(order_by);
176        self
177    }
178
179    pub fn limit(mut self, limit: u64) -> Self {
180        self.limit = Some(limit);
181        self
182    }
183}
184
185#[derive(Debug, Clone)]
186pub struct ListResult {
187    pub items: Vec<Row>,
188    pub affected: u64,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct DeleteResult {
193    pub affected: u64,
194    pub deleted: bool,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ExistsResult {
199    pub exists: bool,
200}
201
202#[derive(Debug, Clone, PartialEq)]
203pub struct DocumentItem {
204    pub rid: String,
205    pub fields: Row,
206}
207
208#[derive(Debug, Clone, PartialEq)]
209pub struct KvItem {
210    pub collection: String,
211    pub key: String,
212    pub value: ValueOut,
213}
214
215#[derive(Debug, Clone, PartialEq)]
216pub struct KvWatchEvent {
217    pub key: String,
218    pub op: String,
219    pub before: serde_json::Value,
220    pub after: serde_json::Value,
221    pub lsn: u64,
222    pub committed_at: u64,
223    pub dropped_event_count: u64,
224}
225
226#[cfg(any(feature = "redwire", feature = "http"))]
227impl QueryResult {
228    /// Build a `QueryResult` from the JSON envelope the server
229    /// emits in a `Result` frame.
230    pub fn from_envelope(value: serde_json::Value) -> Self {
231        let Some(obj) = value.as_object() else {
232            return Self {
233                statement: String::new(),
234                affected: 0,
235                columns: Vec::new(),
236                rows: Vec::new(),
237            };
238        };
239        let result_obj = obj.get("result").and_then(|v| v.as_object()).unwrap_or(obj);
240        let statement = obj
241            .get("statement")
242            .or_else(|| obj.get("statement_type"))
243            .and_then(|v| v.as_str())
244            .unwrap_or("")
245            .to_string();
246        let affected = obj
247            .get("affected")
248            .or_else(|| obj.get("affected_rows"))
249            .and_then(|v| v.as_u64())
250            .unwrap_or(0);
251        let columns: Vec<String> = result_obj
252            .get("columns")
253            .and_then(|v| v.as_array())
254            .map(|cols| {
255                cols.iter()
256                    .filter_map(|col| col.as_str().map(ToOwned::to_owned))
257                    .collect::<Vec<_>>()
258            })
259            .unwrap_or_default();
260        let row_values = result_obj
261            .get("records")
262            .or_else(|| result_obj.get("rows"))
263            .and_then(|v| v.as_array());
264        let rows = row_values
265            .map(|records| {
266                records
267                    .iter()
268                    .map(|record| parse_record(record, &columns))
269                    .collect()
270            })
271            .unwrap_or_default();
272        Self {
273            statement,
274            affected,
275            columns,
276            rows,
277        }
278    }
279}
280
281#[cfg(any(feature = "redwire", feature = "http"))]
282fn parse_record(record: &serde_json::Value, columns: &[String]) -> Vec<(String, ValueOut)> {
283    let Some(record_obj) = record.as_object() else {
284        return Vec::new();
285    };
286    let values = record_obj
287        .get("values")
288        .and_then(|v| v.as_object())
289        .unwrap_or(record_obj);
290    if columns.is_empty() {
291        return values
292            .iter()
293            .map(|(key, value)| (key.clone(), json_to_value_out(value)))
294            .collect();
295    }
296    columns
297        .iter()
298        .map(|column| {
299            (
300                column.clone(),
301                values
302                    .get(column)
303                    .map(json_to_value_out)
304                    .unwrap_or(ValueOut::Null),
305            )
306        })
307        .collect()
308}
309
310#[cfg(any(feature = "redwire", feature = "http"))]
311fn json_to_value_out(value: &serde_json::Value) -> ValueOut {
312    match value {
313        serde_json::Value::Null => ValueOut::Null,
314        serde_json::Value::Bool(value) => ValueOut::Bool(*value),
315        serde_json::Value::Number(value) => {
316            if let Some(n) = value.as_i64() {
317                ValueOut::Integer(n)
318            } else if let Some(n) = value.as_f64() {
319                ValueOut::Float(n)
320            } else {
321                ValueOut::String(value.to_string())
322            }
323        }
324        serde_json::Value::String(value) => ValueOut::String(value.clone()),
325        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
326            ValueOut::String(value.to_string())
327        }
328    }
329}
330
331#[derive(Debug, Clone)]
332pub struct InsertResult {
333    pub affected: u64,
334    /// Present when the engine surfaces an inserted RedDB ID.
335    pub rid: Option<String>,
336    /// Legacy alias for `rid`.
337    pub id: Option<String>,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct BulkInsertResult {
342    pub affected: u64,
343    /// Present when the engine surfaces inserted RedDB IDs for the batch.
344    pub rids: Vec<String>,
345    /// Legacy alias for `rids`.
346    pub ids: Vec<String>,
347}