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
155#[derive(Debug, Clone, PartialEq)]
156pub struct KvWatchEvent {
157    pub key: String,
158    pub op: String,
159    pub before: serde_json::Value,
160    pub after: serde_json::Value,
161    pub lsn: u64,
162    pub committed_at: u64,
163    pub dropped_event_count: u64,
164}
165
166#[cfg(any(feature = "redwire", feature = "http"))]
167impl QueryResult {
168    /// Build a `QueryResult` from the JSON envelope the server
169    /// emits in a `Result` frame.
170    pub fn from_envelope(value: serde_json::Value) -> Self {
171        let Some(obj) = value.as_object() else {
172            return Self {
173                statement: String::new(),
174                affected: 0,
175                columns: Vec::new(),
176                rows: Vec::new(),
177            };
178        };
179        let result_obj = obj.get("result").and_then(|v| v.as_object()).unwrap_or(obj);
180        let statement = obj
181            .get("statement")
182            .or_else(|| obj.get("statement_type"))
183            .and_then(|v| v.as_str())
184            .unwrap_or("")
185            .to_string();
186        let affected = obj
187            .get("affected")
188            .or_else(|| obj.get("affected_rows"))
189            .and_then(|v| v.as_u64())
190            .unwrap_or(0);
191        let columns: Vec<String> = result_obj
192            .get("columns")
193            .and_then(|v| v.as_array())
194            .map(|cols| {
195                cols.iter()
196                    .filter_map(|col| col.as_str().map(ToOwned::to_owned))
197                    .collect::<Vec<_>>()
198            })
199            .unwrap_or_default();
200        let row_values = result_obj
201            .get("records")
202            .or_else(|| result_obj.get("rows"))
203            .and_then(|v| v.as_array());
204        let rows = row_values
205            .map(|records| {
206                records
207                    .iter()
208                    .map(|record| parse_record(record, &columns))
209                    .collect()
210            })
211            .unwrap_or_default();
212        Self {
213            statement,
214            affected,
215            columns,
216            rows,
217        }
218    }
219}
220
221#[cfg(any(feature = "redwire", feature = "http"))]
222fn parse_record(record: &serde_json::Value, columns: &[String]) -> Vec<(String, ValueOut)> {
223    let Some(record_obj) = record.as_object() else {
224        return Vec::new();
225    };
226    let values = record_obj
227        .get("values")
228        .and_then(|v| v.as_object())
229        .unwrap_or(record_obj);
230    if columns.is_empty() {
231        return values
232            .iter()
233            .map(|(key, value)| (key.clone(), json_to_value_out(value)))
234            .collect();
235    }
236    columns
237        .iter()
238        .map(|column| {
239            (
240                column.clone(),
241                values
242                    .get(column)
243                    .map(json_to_value_out)
244                    .unwrap_or(ValueOut::Null),
245            )
246        })
247        .collect()
248}
249
250#[cfg(any(feature = "redwire", feature = "http"))]
251fn json_to_value_out(value: &serde_json::Value) -> ValueOut {
252    match value {
253        serde_json::Value::Null => ValueOut::Null,
254        serde_json::Value::Bool(value) => ValueOut::Bool(*value),
255        serde_json::Value::Number(value) => {
256            if let Some(n) = value.as_i64() {
257                ValueOut::Integer(n)
258            } else if let Some(n) = value.as_f64() {
259                ValueOut::Float(n)
260            } else {
261                ValueOut::String(value.to_string())
262            }
263        }
264        serde_json::Value::String(value) => ValueOut::String(value.clone()),
265        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
266            ValueOut::String(value.to_string())
267        }
268    }
269}
270
271#[derive(Debug, Clone)]
272pub struct InsertResult {
273    pub affected: u64,
274    /// Present when the engine surfaces an inserted RedDB ID.
275    pub rid: Option<String>,
276    /// Legacy alias for `rid`.
277    pub id: Option<String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct BulkInsertResult {
282    pub affected: u64,
283    /// Present when the engine surfaces inserted RedDB IDs for the batch.
284    pub rids: Vec<String>,
285    /// Legacy alias for `rids`.
286    pub ids: Vec<String>,
287}