use std::collections::HashMap;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use nodedb_types::protocol::{OpCode, TextFields};
use nodedb_types::value::Value;
use super::core::NativeClient;
fn index_to_wire(index: usize, field_name: &str) -> NodeDbResult<u64> {
u64::try_from(index).map_err(|_| {
NodeDbError::bad_request(format!("{field_name} value {index} exceeds wire u64 range"))
})
}
fn require_object_fields(fields: &Value) -> NodeDbResult<&HashMap<String, Value>> {
match fields {
Value::Object(obj) => Ok(obj),
other => Err(NodeDbError::bad_request(format!(
"list_insert: fields must be Value::Object, got {other:?}"
))),
}
}
pub(super) fn build_list_insert_fields(
collection: &str,
document_id: &str,
list_path: &str,
index: usize,
fields: &Value,
) -> NodeDbResult<TextFields> {
let obj = require_object_fields(fields)?;
let fields_json = sonic_rs::to_string(obj)
.map_err(|e| NodeDbError::serialization("json", format!("list_insert fields: {e}")))?;
let list_index = index_to_wire(index, "list_index")?;
Ok(TextFields {
collection: Some(collection.to_string()),
document_id: Some(document_id.to_string()),
list_path: Some(list_path.to_string()),
list_index: Some(list_index),
list_fields_json: Some(fields_json),
..Default::default()
})
}
pub(super) fn build_list_delete_fields(
collection: &str,
document_id: &str,
list_path: &str,
index: usize,
) -> NodeDbResult<TextFields> {
let list_index = index_to_wire(index, "list_index")?;
Ok(TextFields {
collection: Some(collection.to_string()),
document_id: Some(document_id.to_string()),
list_path: Some(list_path.to_string()),
list_index: Some(list_index),
..Default::default()
})
}
pub(super) fn build_list_move_fields(
collection: &str,
document_id: &str,
list_path: &str,
from_index: usize,
to_index: usize,
) -> NodeDbResult<TextFields> {
let list_from_index = index_to_wire(from_index, "list_from_index")?;
let list_to_index = index_to_wire(to_index, "list_to_index")?;
Ok(TextFields {
collection: Some(collection.to_string()),
document_id: Some(document_id.to_string()),
list_path: Some(list_path.to_string()),
list_from_index: Some(list_from_index),
list_to_index: Some(list_to_index),
..Default::default()
})
}
impl NativeClient {
pub(super) async fn list_insert_impl(
&self,
collection: &str,
document_id: &str,
list_path: &str,
index: usize,
fields: &Value,
) -> NodeDbResult<()> {
let request = build_list_insert_fields(collection, document_id, list_path, index, fields)?;
let mut conn = self.pool.acquire().await?;
conn.send(OpCode::CrdtListInsert, request).await?;
Ok(())
}
pub(super) async fn list_delete_impl(
&self,
collection: &str,
document_id: &str,
list_path: &str,
index: usize,
) -> NodeDbResult<()> {
let request = build_list_delete_fields(collection, document_id, list_path, index)?;
let mut conn = self.pool.acquire().await?;
conn.send(OpCode::CrdtListDelete, request).await?;
Ok(())
}
pub(super) async fn list_move_impl(
&self,
collection: &str,
document_id: &str,
list_path: &str,
from_index: usize,
to_index: usize,
) -> NodeDbResult<()> {
let request =
build_list_move_fields(collection, document_id, list_path, from_index, to_index)?;
let mut conn = self.pool.acquire().await?;
conn.send(OpCode::CrdtListMove, request).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn obj_fields() -> Value {
let mut map = HashMap::new();
map.insert("title".to_string(), Value::String("hello".to_string()));
map.insert("done".to_string(), Value::Bool(false));
Value::Object(map)
}
#[test]
fn list_insert_fields_carries_every_field() {
let fields = obj_fields();
let req = build_list_insert_fields("docs", "doc-1", "items", 3, &fields)
.expect("object fields must build");
assert_eq!(req.collection.as_deref(), Some("docs"));
assert_eq!(req.document_id.as_deref(), Some("doc-1"));
assert_eq!(req.list_path.as_deref(), Some("items"));
assert_eq!(req.list_index, Some(3));
let json = req.list_fields_json.expect("fields_json must be set");
assert!(json.contains("\"title\""));
assert!(json.contains("hello"));
assert!(json.contains("\"done\""));
assert!(req.list_from_index.is_none());
assert!(req.list_to_index.is_none());
}
#[test]
fn list_insert_rejects_non_object_fields() {
let err = build_list_insert_fields("docs", "doc-1", "items", 0, &Value::Integer(1))
.expect_err("scalar fields must be rejected");
assert!(format!("{err}").to_lowercase().contains("object"));
}
#[test]
fn list_delete_fields_carries_index_without_fields_json() {
let req = build_list_delete_fields("docs", "doc-2", "notes", 7)
.expect("delete request must build");
assert_eq!(req.collection.as_deref(), Some("docs"));
assert_eq!(req.document_id.as_deref(), Some("doc-2"));
assert_eq!(req.list_path.as_deref(), Some("notes"));
assert_eq!(req.list_index, Some(7));
assert!(req.list_fields_json.is_none());
}
#[test]
fn list_move_keeps_from_and_to_index_distinct_and_unswapped() {
let req = build_list_move_fields("docs", "doc-1", "items", 1, 5)
.expect("move request must build");
assert_eq!(req.list_from_index, Some(1));
assert_eq!(req.list_to_index, Some(5));
assert_ne!(req.list_from_index, req.list_to_index);
let swapped = build_list_move_fields("docs", "doc-1", "items", 5, 1)
.expect("move request must build");
assert_eq!(swapped.list_from_index, Some(5));
assert_eq!(swapped.list_to_index, Some(1));
}
}