use serde_json::{Map, Value, json};
use crate::changeset::Op;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PlanDisposition {
Same,
Edited,
Moved,
EditedMoved,
Inserted,
Deleted,
SplitFrom,
MergedInto,
CopiedFrom,
Resurrected,
BulkRewrite,
Retiled,
}
impl PlanDisposition {
pub const ALL: [PlanDisposition; 12] = [
PlanDisposition::Same,
PlanDisposition::Edited,
PlanDisposition::Moved,
PlanDisposition::EditedMoved,
PlanDisposition::Inserted,
PlanDisposition::Deleted,
PlanDisposition::SplitFrom,
PlanDisposition::MergedInto,
PlanDisposition::CopiedFrom,
PlanDisposition::Resurrected,
PlanDisposition::BulkRewrite,
PlanDisposition::Retiled,
];
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
PlanDisposition::Same => "same",
PlanDisposition::Edited => "edited",
PlanDisposition::Moved => "moved",
PlanDisposition::EditedMoved => "edited_moved",
PlanDisposition::Inserted => "inserted",
PlanDisposition::Deleted => "deleted",
PlanDisposition::SplitFrom => "split_from",
PlanDisposition::MergedInto => "merged_into",
PlanDisposition::CopiedFrom => "copied_from",
PlanDisposition::Resurrected => "resurrected",
PlanDisposition::BulkRewrite => "bulk_rewrite",
PlanDisposition::Retiled => "retiled",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|d| d.as_str() == s)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlanOp {
pub op: Op,
pub disposition: PlanDisposition,
pub blocks: Vec<String>,
pub confidence: Option<f64>,
pub reason: Option<String>,
pub detail: Option<Value>,
}
impl PlanOp {
#[must_use]
pub fn to_json(&self) -> Value {
let mut m = Map::new();
m.insert("op".to_owned(), self.op.to_json());
m.insert("disposition".to_owned(), json!(self.disposition.as_str()));
m.insert("blocks".to_owned(), json!(self.blocks));
m.insert("confidence".to_owned(), json!(self.confidence));
m.insert("reason".to_owned(), json!(self.reason));
if let Some(d) = &self.detail {
m.insert("detail".to_owned(), d.clone());
}
Value::Object(m)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct OpsetSummary {
pub preserved: usize,
pub updated: usize,
pub moved: usize,
pub created: usize,
pub removed: usize,
pub split: usize,
pub merged: usize,
pub ambiguous: usize,
}
impl OpsetSummary {
#[must_use]
pub fn to_json(&self) -> Value {
json!({
"preserved": self.preserved, "updated": self.updated, "moved": self.moved,
"created": self.created, "removed": self.removed, "split": self.split,
"merged": self.merged, "ambiguous": self.ambiguous,
})
}
}
#[must_use]
pub fn summarize(ops: &[PlanOp], preserved: usize, ambiguous: usize) -> OpsetSummary {
let mut s = OpsetSummary {
preserved,
ambiguous,
..OpsetSummary::default()
};
for p in ops {
match p.disposition {
PlanDisposition::Edited => s.updated += 1,
PlanDisposition::Moved => s.moved += 1,
PlanDisposition::EditedMoved => {
s.updated += 1;
s.moved += 1;
}
PlanDisposition::Inserted
| PlanDisposition::CopiedFrom
| PlanDisposition::Resurrected => {
s.created += 1;
}
PlanDisposition::Deleted => s.removed += 1,
PlanDisposition::SplitFrom => s.split += 1,
PlanDisposition::MergedInto => s.merged += 1,
PlanDisposition::Same | PlanDisposition::BulkRewrite | PlanDisposition::Retiled => {}
}
}
s
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpsetPrecondition {
pub doc: String,
pub path: String,
pub base_revision: Option<String>,
pub base_content_hash: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Opset {
pub target_doc: String,
pub target_path: String,
pub precondition: OpsetPrecondition,
pub matcher_v: String,
pub ops: Vec<PlanOp>,
pub frontmatter: Option<Option<String>>,
pub summary: OpsetSummary,
pub converges: bool,
pub diagnostics: Vec<String>,
}
impl Opset {
#[must_use]
pub fn kernel_ops(&self) -> Vec<Op> {
self.ops.iter().map(|p| p.op.clone()).collect()
}
#[must_use]
pub fn to_json(&self) -> Value {
let mut m = Map::new();
m.insert("version".to_owned(), json!(1));
m.insert("kind".to_owned(), json!("doc_update"));
m.insert(
"target".to_owned(),
json!({ "doc": self.target_doc, "path": self.target_path }),
);
m.insert(
"precondition".to_owned(),
json!({
"doc": self.precondition.doc,
"path": self.precondition.path,
"base_revision": self.precondition.base_revision,
"base_content_hash": self.precondition.base_content_hash,
}),
);
m.insert("matcher_v".to_owned(), json!(self.matcher_v));
m.insert(
"ops".to_owned(),
Value::Array(self.ops.iter().map(PlanOp::to_json).collect()),
);
if let Some(fm) = &self.frontmatter {
m.insert("frontmatter".to_owned(), json!({ "raw": fm }));
}
m.insert("summary".to_owned(), self.summary.to_json());
m.insert("converges".to_owned(), json!(self.converges));
m.insert("diagnostics".to_owned(), json!(self.diagnostics));
Value::Object(m)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::{At, Parent, To};
fn plan(d: PlanDisposition) -> PlanOp {
PlanOp {
op: Op::Move {
blocks: vec!["b_1".into()],
to: To {
parent: Parent::Doc,
at: At::Start,
},
},
disposition: d,
blocks: vec!["b_1".into()],
confidence: Some(1.0),
reason: Some("exact_hash".into()),
detail: None,
}
}
#[test]
fn summary_buckets() {
let ops: Vec<PlanOp> = PlanDisposition::ALL.iter().map(|d| plan(*d)).collect();
let s = summarize(&ops, 3, 1);
assert_eq!(
s,
OpsetSummary {
preserved: 3,
updated: 2,
moved: 2,
created: 3,
removed: 1,
split: 1,
merged: 1,
ambiguous: 1,
}
);
for d in PlanDisposition::ALL {
assert_eq!(PlanDisposition::parse(d.as_str()), Some(d));
}
}
#[test]
fn opset_json_shape() {
let opset = Opset {
target_doc: "d_0".into(),
target_path: "a.md".into(),
precondition: OpsetPrecondition {
doc: "d_0".into(),
path: "a.md".into(),
base_revision: Some("r_0".into()),
base_content_hash: "ab".into(),
},
matcher_v: "m2.3".into(),
ops: vec![plan(PlanDisposition::Moved)],
frontmatter: Some(None),
summary: OpsetSummary::default(),
converges: true,
diagnostics: vec![],
};
let v = opset.to_json();
assert_eq!(v["version"], json!(1));
assert_eq!(v["kind"], json!("doc_update"));
assert_eq!(v["precondition"]["base_revision"], json!("r_0"));
assert_eq!(v["frontmatter"], json!({ "raw": null }));
assert_eq!(v["ops"][0]["disposition"], json!("moved"));
assert_eq!(v["ops"][0]["blocks"], json!(["b_1"]));
assert!(v["ops"][0].get("detail").is_none());
assert_eq!(opset.kernel_ops().len(), 1);
}
}