#[derive(Debug)]
pub(in crate::data::executor) enum TransferError {
TypeMismatch(String),
InsufficientBalance { have: f64, need: f64 },
}
pub(in crate::data::executor) struct TransferComputation {
pub new_source: Vec<u8>,
pub new_dest: Vec<u8>,
pub source_balance_after: f64,
pub dest_balance_after: f64,
}
pub(in crate::data::executor) fn compute_transfer(
source_bytes: &[u8],
dest_bytes: Option<&[u8]>,
field: &str,
amount: f64,
) -> Result<TransferComputation, TransferError> {
let source_balance = extract_numeric_field(source_bytes, field).ok_or_else(|| {
TransferError::TypeMismatch(format!("field '{field}' is not numeric or missing"))
})?;
if source_balance < amount {
return Err(TransferError::InsufficientBalance {
have: source_balance,
need: amount,
});
}
let dest_balance = dest_bytes
.and_then(|b| extract_numeric_field(b, field))
.unwrap_or(0.0);
let new_source = update_numeric_field(source_bytes, field, source_balance - amount)
.map_err(TransferError::TypeMismatch)?;
let new_dest = match dest_bytes.filter(|b| !b.is_empty()) {
None => {
let doc = serde_json::json!({ field: dest_balance + amount });
nodedb_types::json_to_msgpack(&doc)
.map_err(|e| TransferError::TypeMismatch(format!("serialize destination: {e}")))?
}
Some(bytes) => update_numeric_field(bytes, field, dest_balance + amount)
.map_err(TransferError::TypeMismatch)?,
};
Ok(TransferComputation {
new_source,
new_dest,
source_balance_after: source_balance - amount,
dest_balance_after: dest_balance + amount,
})
}
pub(in crate::data::executor) fn extract_numeric_field(value: &[u8], field: &str) -> Option<f64> {
let doc: serde_json::Value = nodedb_types::json_from_msgpack(value).ok()?;
let v = doc.get(field)?;
v.as_f64().or_else(|| v.as_i64().map(|i| i as f64))
}
pub(in crate::data::executor) fn update_numeric_field(
value: &[u8],
field: &str,
new_value: f64,
) -> Result<Vec<u8>, String> {
let mut doc: serde_json::Value =
nodedb_types::json_from_msgpack(value).map_err(|e| format!("deserialize value: {e}"))?;
if let Some(obj) = doc.as_object_mut() {
if new_value.fract() == 0.0 && new_value >= i64::MIN as f64 && new_value <= i64::MAX as f64
{
obj.insert(field.to_string(), serde_json::json!(new_value as i64));
} else {
obj.insert(field.to_string(), serde_json::json!(new_value));
}
}
nodedb_types::json_to_msgpack(&doc).map_err(|e| format!("serialize value: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(field: &str, value: f64) -> Vec<u8> {
nodedb_types::json_to_msgpack(&serde_json::json!({ field: value })).unwrap()
}
#[test]
fn transfer_moves_balance_between_existing_docs() {
let source = doc("balance", 100.0);
let dest = doc("balance", 10.0);
let result = compute_transfer(&source, Some(&dest), "balance", 30.0).unwrap();
assert_eq!(result.source_balance_after, 70.0);
assert_eq!(result.dest_balance_after, 40.0);
}
#[test]
fn transfer_creates_dest_when_absent() {
let source = doc("balance", 100.0);
let result = compute_transfer(&source, None, "balance", 30.0).unwrap();
assert_eq!(result.dest_balance_after, 30.0);
assert_eq!(
extract_numeric_field(&result.new_dest, "balance"),
Some(30.0)
);
}
#[test]
fn transfer_rejects_insufficient_balance() {
let source = doc("balance", 10.0);
let err = compute_transfer(&source, None, "balance", 30.0);
assert!(matches!(
err,
Err(TransferError::InsufficientBalance { .. })
));
}
#[test]
fn transfer_rejects_non_numeric_field() {
let source = nodedb_types::json_to_msgpack(&serde_json::json!({"balance": "abc"})).unwrap();
let err = compute_transfer(&source, None, "balance", 30.0);
assert!(matches!(err, Err(TransferError::TypeMismatch(_))));
}
}