use serde_json::{Value, json};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemStatus {
Ok,
Error,
Skipped,
}
impl ItemStatus {
fn as_str(self) -> &'static str {
match self {
ItemStatus::Ok => "ok",
ItemStatus::Error => "error",
ItemStatus::Skipped => "skipped",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ItemOutcome {
pub index: usize,
pub status: ItemStatus,
pub id: Option<Value>,
pub error: Option<Value>,
}
impl ItemOutcome {
pub fn ok(index: usize, id: Option<Value>) -> Self {
Self {
index,
status: ItemStatus::Ok,
id,
error: None,
}
}
pub fn error(index: usize, error: Value) -> Self {
Self {
index,
status: ItemStatus::Error,
id: None,
error: Some(error),
}
}
pub fn skipped(index: usize) -> Self {
Self {
index,
status: ItemStatus::Skipped,
id: None,
error: None,
}
}
fn to_json(&self) -> Value {
let mut o = json!({ "index": self.index, "status": self.status.as_str() });
if let Some(id) = &self.id {
o["id"] = id.clone();
}
if let Some(e) = &self.error {
o["error"] = e.clone();
}
o
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BulkOutcome {
pub items: Vec<ItemOutcome>,
}
impl BulkOutcome {
pub fn all_ok(ids: Vec<Option<Value>>) -> Self {
Self {
items: ids
.into_iter()
.enumerate()
.map(|(i, id)| ItemOutcome::ok(i, id))
.collect(),
}
}
pub fn count(&self, status: ItemStatus) -> usize {
self.items.iter().filter(|i| i.status == status).count()
}
pub fn inserted(&self) -> usize {
self.count(ItemStatus::Ok)
}
pub fn is_partial(&self) -> bool {
self.inserted() > 0 && self.inserted() < self.items.len()
}
pub fn nothing_applied(&self) -> bool {
self.inserted() == 0 && !self.items.is_empty()
}
pub fn ids(&self) -> Vec<Value> {
self.items
.iter()
.filter(|i| i.status == ItemStatus::Ok)
.filter_map(|i| i.id.clone())
.collect()
}
pub fn first_error(&self) -> Option<&Value> {
self.items
.iter()
.find(|i| i.status == ItemStatus::Error)
.and_then(|i| i.error.as_ref())
}
pub fn to_json(&self) -> Value {
let failed = self.count(ItemStatus::Error);
let skipped = self.count(ItemStatus::Skipped);
let mut out = json!({
"status": if self.is_partial() { "partial" } else { "ok" },
"inserted": self.inserted(),
"ids": self.ids(),
});
if failed > 0 {
out["failed"] = json!(failed);
}
if skipped > 0 {
out["skipped"] = json!(skipped);
}
if self.is_partial() {
out["items"] = Value::Array(self.items.iter().map(ItemOutcome::to_json).collect());
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_all_ok_bulk_reports_ok_and_no_item_array() {
let out = BulkOutcome::all_ok(vec![Some(json!("a")), Some(json!("b"))]);
assert!(!out.is_partial());
let j = out.to_json();
assert_eq!(j["status"], "ok");
assert_eq!(j["inserted"], 2);
assert_eq!(j["ids"], json!(["a", "b"]));
assert!(
j.get("items").is_none(),
"a clean bulk should not carry a per-item array: {j}"
);
assert!(j.get("failed").is_none(), "{j}");
}
#[test]
fn a_mixed_bulk_is_partial_and_names_every_item() {
let out = BulkOutcome {
items: vec![
ItemOutcome::ok(0, Some(json!("a"))),
ItemOutcome::error(1, json!({ "type": "version_conflict" })),
ItemOutcome::ok(2, Some(json!("c"))),
],
};
assert!(out.is_partial());
assert!(!out.nothing_applied());
let j = out.to_json();
assert_eq!(j["status"], "partial", "{j}");
assert_eq!(j["inserted"], 2, "{j}");
assert_eq!(j["failed"], 1, "{j}");
assert_eq!(j["ids"], json!(["a", "c"]));
let items = j["items"].as_array().expect("items array");
assert_eq!(items.len(), 3, "every item must be reported: {j}");
assert_eq!(items[1]["index"], 1);
assert_eq!(items[1]["status"], "error");
assert_eq!(items[1]["error"]["type"], "version_conflict");
}
#[test]
fn an_all_failed_bulk_is_not_partial() {
let out = BulkOutcome {
items: vec![
ItemOutcome::error(0, json!({ "m": "x" })),
ItemOutcome::error(1, json!({ "m": "y" })),
],
};
assert!(!out.is_partial());
assert!(out.nothing_applied());
assert_eq!(out.first_error(), Some(&json!({ "m": "x" })));
}
#[test]
fn skipped_items_are_counted_separately() {
let out = BulkOutcome {
items: vec![
ItemOutcome::ok(0, None),
ItemOutcome::error(1, json!({ "code": 11000 })),
ItemOutcome::skipped(2),
],
};
assert!(out.is_partial());
let j = out.to_json();
assert_eq!(j["inserted"], 1, "{j}");
assert_eq!(j["failed"], 1, "{j}");
assert_eq!(j["skipped"], 1, "{j}");
assert_eq!(j["items"][2]["status"], "skipped", "{j}");
}
}