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    pub notice: Option<String>,
154}
155
156pub type Row = Vec<(String, ValueOut)>;
157
158#[derive(Debug, Clone, Default)]
159pub struct ListOptions<'a> {
160    pub filter: Option<&'a str>,
161    pub order_by: Option<&'a str>,
162    pub limit: Option<u64>,
163}
164
165impl<'a> ListOptions<'a> {
166    pub fn new() -> Self {
167        Self::default()
168    }
169
170    pub fn filter(mut self, filter: &'a str) -> Self {
171        self.filter = Some(filter);
172        self
173    }
174
175    pub fn order_by(mut self, order_by: &'a str) -> Self {
176        self.order_by = Some(order_by);
177        self
178    }
179
180    pub fn limit(mut self, limit: u64) -> Self {
181        self.limit = Some(limit);
182        self
183    }
184}
185
186#[derive(Debug, Clone)]
187pub struct ListResult {
188    pub items: Vec<Row>,
189    pub affected: u64,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct DeleteResult {
194    pub affected: u64,
195    pub deleted: bool,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct ExistsResult {
200    pub exists: bool,
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub struct DocumentItem {
205    pub rid: String,
206    pub fields: Row,
207}
208
209#[derive(Debug, Clone, PartialEq)]
210pub struct KvItem {
211    pub collection: String,
212    pub key: String,
213    pub value: ValueOut,
214}
215
216#[derive(Debug, Clone, PartialEq)]
217pub struct KvWatchEvent {
218    pub key: String,
219    pub op: String,
220    pub before: serde_json::Value,
221    pub after: serde_json::Value,
222    pub lsn: u64,
223    pub committed_at: u64,
224    pub dropped_event_count: u64,
225}
226
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                notice: None,
238            };
239        };
240        let result_obj = obj.get("result").and_then(|v| v.as_object()).unwrap_or(obj);
241        let statement = obj
242            .get("statement")
243            .or_else(|| obj.get("statement_type"))
244            .and_then(|v| v.as_str())
245            .unwrap_or("")
246            .to_string();
247        let affected = obj
248            .get("affected")
249            .or_else(|| obj.get("affected_rows"))
250            .and_then(|v| v.as_u64())
251            .unwrap_or(0);
252        let columns: Vec<String> = result_obj
253            .get("columns")
254            .and_then(|v| v.as_array())
255            .map(|cols| {
256                cols.iter()
257                    .filter_map(|col| col.as_str().map(ToOwned::to_owned))
258                    .collect::<Vec<_>>()
259            })
260            .unwrap_or_default();
261        let row_values = result_obj
262            .get("records")
263            .or_else(|| result_obj.get("rows"))
264            .and_then(|v| v.as_array());
265        let rows = row_values
266            .map(|records| {
267                records
268                    .iter()
269                    .map(|record| parse_record(record, &columns))
270                    .collect()
271            })
272            .unwrap_or_default();
273        Self {
274            statement,
275            affected,
276            columns,
277            rows,
278            notice: obj
279                .get("notice")
280                .and_then(|v| v.as_str())
281                .map(ToOwned::to_owned),
282        }
283    }
284}
285
286fn parse_record(record: &serde_json::Value, columns: &[String]) -> Vec<(String, ValueOut)> {
287    let Some(record_obj) = record.as_object() else {
288        return Vec::new();
289    };
290    let values = record_obj
291        .get("values")
292        .and_then(|v| v.as_object())
293        .unwrap_or(record_obj);
294    if columns.is_empty() {
295        return values
296            .iter()
297            .map(|(key, value)| (key.clone(), json_to_value_out(value)))
298            .collect();
299    }
300    columns
301        .iter()
302        .map(|column| {
303            (
304                column.clone(),
305                values
306                    .get(column)
307                    .map(json_to_value_out)
308                    .unwrap_or(ValueOut::Null),
309            )
310        })
311        .collect()
312}
313
314fn json_to_value_out(value: &serde_json::Value) -> ValueOut {
315    match value {
316        serde_json::Value::Null => ValueOut::Null,
317        serde_json::Value::Bool(value) => ValueOut::Bool(*value),
318        serde_json::Value::Number(value) => {
319            if let Some(n) = value.as_i64() {
320                ValueOut::Integer(n)
321            } else if let Some(n) = value.as_f64() {
322                ValueOut::Float(n)
323            } else {
324                ValueOut::String(value.to_string())
325            }
326        }
327        serde_json::Value::String(value) => ValueOut::String(value.clone()),
328        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
329            ValueOut::String(value.to_string())
330        }
331    }
332}
333
334#[derive(Debug, Clone)]
335pub struct InsertResult {
336    pub affected: u64,
337    /// Present when the engine surfaces an inserted RedDB ID.
338    pub rid: Option<String>,
339    /// Legacy alias for `rid`.
340    pub id: Option<String>,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct BulkInsertResult {
345    pub affected: u64,
346    /// Present when the engine surfaces inserted RedDB IDs for the batch.
347    pub rids: Vec<String>,
348    /// Legacy alias for `rids`.
349    pub ids: Vec<String>,
350}