patch.rs
//! Accumulating hunk rewrites, and applying them in one pass.
//!
//! `PatchAccumulator` is pure format manipulation, with no awareness of testing:
//! it holds owned (hunk name, new body) pairs,
//! and `Document::patched_all` splices them all into new document text.
//! The compare-or-regen logic that test harnesses want lives a layer up, in the `check` module;
//! this layer is equally at home in any tool that runs hunks and writes results back
//! (the accumulator owns its contents because the new bodies are newly produced values,
//! while reads of a `Document` stay borrow-only throughout).
use std::fmt;
use crate::document::{Document, Hunk, HunkContent};
/// Pending rewrites of code-hunk bodies, keyed by hunk name.
///
/// Recording the same hunk again replaces the earlier value:
/// at this layer, re-recording is a normal use (think: re-running a producer of the body).
/// Treating a re-record as an error is a testing judgement, and is made a layer up.
#[derive(Default, Clone, Debug)]
pub struct PatchAccumulator {
/// Insertion-ordered; names unique (`put` replaces in place).
patches: Vec<(String, String)>,
}
impl PatchAccumulator {
pub fn new() -> PatchAccumulator {
PatchAccumulator::default()
}
/// Record (or replace) the pending body for a hunk.
pub fn put(&mut self, hunk: impl Into<String>, body: impl Into<String>) {
let hunk = hunk.into();
let body = body.into();
match self.patches.iter_mut().find(|(name, _)| *name == hunk) {
Some(entry) => entry.1 = body,
None => self.patches.push((hunk, body)),
}
}
/// The pending body for a hunk, if one is recorded.
pub fn get(&self, hunk: &str) -> Option<&str> {
self.patches
.iter()
.find(|(name, _)| name == hunk)
.map(|(_, body)| body.as_str())
}
pub fn is_empty(&self) -> bool {
self.patches.is_empty()
}
}
/// Problems applying a `PatchAccumulator` to a document.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PatchError {
/// A patch names a hunk not present in the document.
NoSuchHunk {
name: String,
},
/// A patch names a table hunk; accumulated patches are code-hunk bodies.
/// (Table rewriting stays one-shot via `Document::patched` until something
/// actually generates tables.)
NotACodeHunk {
name: String,
},
}
impl fmt::Display for PatchError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PatchError::NoSuchHunk {
name,
} => write!(f, "patch names hunk {name:?}, which is not in the document"),
PatchError::NotACodeHunk {
name,
} => write!(f, "patch names hunk {name:?}, which is not a code hunk"),
}
}
}
impl std::error::Error for PatchError {}
impl Document {
/// Apply all accumulated patches, producing new document text.
/// Pure: the caller writes the result back to wherever the text came from.
///
/// Each patched hunk keeps its fence lines (and block_tag) unchanged;
/// all prose and formatting outside the patched bodies is preserved
/// byte-for-byte, in one splice pass, with no intermediate re-parse.
pub fn patched_all(&self, acc: &PatchAccumulator) -> Result<String, PatchError> {
// Resolve every patch to its hunk up front: fail before splicing anything.
let mut targets: Vec<(&Hunk, &str)> = Vec::with_capacity(acc.patches.len());
for (name, body) in &acc.patches {
let h = self.hunk(name).ok_or_else(|| PatchError::NoSuchHunk {
name: name.clone(),
})?;
if !matches!(h.content, HunkContent::Code { .. }) {
return Err(PatchError::NotACodeHunk {
name: name.clone(),
});
}
targets.push((h, body));
}
targets.sort_by_key(|(h, _)| h.line_comment);
let mut out: Vec<&str> = Vec::new();
let mut cursor = 0;
for (h, body) in targets {
// Emit through the fence-open line, then the new body.
// The closing fence (at line_end) is emitted by the next segment.
for l in &self.lines[cursor..=h.line_comment + 1] {
out.push(l);
}
// A multi-line body goes in as one element; the final join flattens it.
let trimmed = body.strip_suffix('\n').unwrap_or(body);
if !trimmed.is_empty() {
out.push(trimmed);
}
cursor = h.line_end;
}
for l in &self.lines[cursor..] {
out.push(l);
}
Ok(out.join("\n"))
}
}
include!("patch_test.rs");
crates/testmark/src/patch.rs · 4.1 KiB · 9049dd4f1c