use std::fmt;
use schemars::JsonSchema;
use serde::Serialize;
use crate::sync::hunk::{CollectionHunk, ItemHunk};
#[derive(Debug, Default, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SyncOutput {
pub account: String,
pub dry_run: bool,
pub collection: PatchOutcome<CollectionHunk>,
pub item: PatchOutcome<ItemHunk>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub collisions: Vec<MessageCollision>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drained: Vec<DrainedQueue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parked: Vec<ParkedQueueAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub submitted: Vec<SubmitEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purged: Option<PurgedItems>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conflicts: Vec<ItemConflict>,
#[serde(default)]
pub outstanding_conflicts: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub refused: Vec<RefusedDuplicate>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rejected: Vec<RejectedWrite>,
}
impl SyncOutput {
pub fn absorb(&mut self, other: Self) {
let Self {
account: _,
dry_run: _,
collection,
item,
collisions,
drained,
parked,
submitted,
purged: _,
conflicts,
outstanding_conflicts: _,
refused,
rejected,
} = other;
self.collection.patch.extend(collection.patch);
self.item.patch.extend(item.patch);
self.collisions.extend(collisions);
self.drained.extend(drained);
self.parked.extend(parked);
self.submitted.extend(submitted);
self.conflicts.extend(conflicts);
self.refused.extend(refused);
self.rejected.extend(rejected);
}
pub fn note_conflict(&mut self, conflict: ItemConflict) {
let named = self.conflicts.iter().any(|named| {
named.side == conflict.side
&& named.collection == conflict.collection
&& named.id == conflict.id
});
if !named {
self.conflicts.push(conflict);
}
}
pub fn left_waiting(&self) -> bool {
self.outstanding_conflicts > 0 || !self.refused.is_empty() || !self.rejected.is_empty()
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RefusedDuplicate {
pub side: String,
pub collection: String,
pub uid: String,
}
impl fmt::Display for RefusedDuplicate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
side,
collection,
uid,
} = self;
write!(
f,
"{side} refused a copy in {collection}: it already holds UID {uid}, so the second copy stays unwritten until one of the two carries a UID of its own"
)
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RejectedWrite {
pub side: String,
pub collection: String,
pub id: String,
pub action: String,
pub reason: String,
}
impl fmt::Display for RejectedWrite {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
side,
collection,
id,
action,
reason,
} = self;
write!(
f,
"{side} refused the {action} of {id} in {collection}, so it stays in the store: {reason}"
)
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ItemConflict {
pub side: String,
pub collection: String,
pub id: String,
}
impl fmt::Display for ItemConflict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
side,
collection,
id,
} = self;
write!(
f,
"item {id} in {collection} on {side} changed on both sides and is left conflicted"
)
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DrainedQueue {
pub collection: String,
pub applied: usize,
}
impl fmt::Display for DrainedQueue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
collection,
applied,
} = self;
write!(f, "applied {applied} queued action(s) in {collection}")
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ParkedQueueAction {
pub id: i64,
pub collection: String,
pub action: String,
pub producer: String,
pub error: String,
}
impl fmt::Display for ParkedQueueAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
id,
collection,
action,
producer,
error,
} = self;
write!(
f,
"parked queue action #{id} ({action} in {collection} from {producer}): {error}"
)
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SubmitEntry {
pub id: i64,
pub collection: String,
pub subject: Option<String>,
pub error: Option<String>,
pub parked: bool,
}
impl fmt::Display for SubmitEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let subject = self.subject.as_deref().unwrap_or("<no subject>");
match (&self.error, self.parked) {
(None, _) => write!(f, "submitted {subject}"),
(Some(err), true) => write!(f, "{subject} parked, never retried: {err}"),
(Some(err), false) => write!(f, "{subject} not submitted, retried next run: {err}"),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PurgedItems {
pub items: usize,
pub objects: usize,
pub bytes: u64,
}
impl fmt::Display for PurgedItems {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
items,
objects,
bytes,
} = self;
write!(
f,
"purged {items} retained item(s), collected {objects} object(s), {bytes} byte(s) reclaimed"
)
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageCollision {
pub side: String,
pub collection: String,
pub message_id: Option<String>,
pub ids: Vec<String>,
}
impl fmt::Display for MessageCollision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
side,
collection,
message_id,
ids,
} = self;
let kept = ids.first().map(String::as_str).unwrap_or("?");
let skipped = ids
.iter()
.skip(1)
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
match message_id {
Some(mid) => write!(
f,
"skip {skipped} on {side} {collection}: same Message-ID {mid} as {kept}"
),
None => write!(
f,
"skip {skipped} on {side} {collection}: same subject/date/sender as {kept} (no Message-ID header)"
),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PatchOutcome<H> {
pub patch: Vec<PatchEntry<H>>,
}
impl<H> Default for PatchOutcome<H> {
fn default() -> Self {
Self { patch: Vec::new() }
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PatchEntry<H> {
pub hunk: H,
pub error: Option<String>,
}
impl<H> PatchEntry<H> {
pub fn new(hunk: H, error: Option<anyhow::Error>) -> Self {
Self {
hunk,
error: error.map(|e| format!("{e:#}")),
}
}
}
impl fmt::Display for SyncOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
let total = self.collection.patch.len() + self.item.patch.len();
let mailbox_errors = self
.collection
.patch
.iter()
.filter(|e| e.error.is_some())
.count();
let item_errors = self.item.patch.iter().filter(|e| e.error.is_some()).count();
let submit_errors = self.submitted.iter().filter(|e| e.error.is_some()).count();
let errors = mailbox_errors + item_errors + submit_errors;
let warnings = self.collisions.len()
+ self.parked.len()
+ self.conflicts.len()
+ self.refused.len()
+ self.rejected.len();
if !self.drained.is_empty() {
writeln!(f, "Queue ({n}):", n = self.drained.len())?;
for entry in &self.drained {
writeln!(f, " - {entry}")?;
}
writeln!(f)?;
}
if !self.submitted.is_empty() {
writeln!(f, "Submissions ({n}):", n = self.submitted.len())?;
for entry in &self.submitted {
writeln!(f, " - {entry}")?;
}
writeln!(f)?;
}
if !self.collection.patch.is_empty() {
writeln!(
f,
"Collection patches ({n}):",
n = self.collection.patch.len()
)?;
for entry in &self.collection.patch {
writeln!(f, " - {hunk}", hunk = entry.hunk)?;
}
writeln!(f)?;
}
if !self.item.patch.is_empty() {
writeln!(f, "Item patches ({n}):", n = self.item.patch.len())?;
for entry in &self.item.patch {
writeln!(f, " - {hunk}", hunk = entry.hunk)?;
}
writeln!(f)?;
}
if let Some(purged) = &self.purged
&& purged.items > 0
{
writeln!(f, "Retention:")?;
writeln!(f, " - {purged}")?;
writeln!(f)?;
}
if warnings > 0 {
writeln!(f, "Warnings ({warnings}):")?;
for c in &self.collisions {
writeln!(f, " - {c}")?;
}
for c in &self.conflicts {
writeln!(f, " - {c}")?;
}
for r in &self.refused {
writeln!(f, " - {r}")?;
}
for r in &self.rejected {
writeln!(f, " - {r}")?;
}
for p in &self.parked {
writeln!(f, " - {p}")?;
}
writeln!(f)?;
}
if self.outstanding_conflicts > 0 {
writeln!(
f,
"Conflicts: {n} item(s) waiting for a decision",
n = self.outstanding_conflicts,
)?;
writeln!(f)?;
}
if errors > 0 {
writeln!(f, "Errors ({errors}):")?;
for entry in self.collection.patch.iter().filter(|e| e.error.is_some()) {
writeln!(
f,
" - {hunk}: {err}",
hunk = entry.hunk,
err = entry.error.as_deref().unwrap_or_default(),
)?;
}
for entry in self.item.patch.iter().filter(|e| e.error.is_some()) {
writeln!(
f,
" - {hunk}: {err}",
hunk = entry.hunk,
err = entry.error.as_deref().unwrap_or_default(),
)?;
}
writeln!(f)?;
}
let account = &self.account;
match (total, errors, warnings, self.dry_run) {
(0, 0, 0, _) => writeln!(f, "Account {account} is already in sync"),
(0, 0, w, _) => writeln!(f, "Account {account} is already in sync ({w} warnings)"),
(n, 0, 0, true) => writeln!(f, "Account {account} would apply {n} hunks"),
(n, 0, w, true) => {
writeln!(f, "Account {account} would apply {n} hunks ({w} warnings)")
}
(n, e, 0, true) => writeln!(
f,
"Account {account} would apply {n} hunks ({e} would fail)"
),
(n, e, w, true) => writeln!(
f,
"Account {account} would apply {n} hunks ({e} would fail, {w} warnings)"
),
(n, 0, 0, false) => writeln!(f, "Account {account} synchronized: {n} hunks"),
(n, 0, w, false) => {
writeln!(f, "Account {account} synchronized: {n} hunks, {w} warnings")
}
(n, e, 0, false) => writeln!(
f,
"Account {account} partially synchronized: {n} hunks, {e} errors"
),
(n, e, w, false) => writeln!(
f,
"Account {account} partially synchronized: {n} hunks, {e} errors, {w} warnings"
),
}
}
}