Skip to main content

weavatrix_edit/application/
batch.rs

1use crate::{
2    error::{EditError, ErrorCode},
3    limits::BatchLimits,
4    model::ByteEdit,
5};
6
7use super::{
8    PreparedEdit, PreparedEdits,
9    batch_support::{
10        Occupancy, Totals, add_totals, admission_order, check_output, find_overlap, into_prepared,
11        merge_sorted, stage_edit, validate_before,
12    },
13    prepare::validate_byte_edit,
14    ranges::{compare_prepared, sort_prepared},
15};
16
17/// Transactional builder for a bounded batch over one immutable source revision.
18///
19/// Every range and `before` value always refers to `source`, including edits
20/// submitted by later calls. This is intentionally not a current-document edit
21/// session: accepted edits do not shift coordinates for subsequent admission.
22#[derive(Debug)]
23pub struct ByteEditBatch<'source> {
24    source: &'source str,
25    edits: Vec<PreparedEdit>,
26    occupied: Occupancy,
27    totals: Totals,
28    output_size: usize,
29    limits: BatchLimits,
30}
31
32impl<'source> ByteEditBatch<'source> {
33    /// Starts an empty batch with default hard resource limits.
34    pub fn new(source: &'source str) -> Result<Self, EditError> {
35        Self::with_limits(source, BatchLimits::default())
36    }
37
38    /// Starts an empty batch with explicit hard resource limits.
39    pub fn with_limits(source: &'source str, limits: BatchLimits) -> Result<Self, EditError> {
40        if source.len() > limits.max_source_bytes {
41            return Err(EditError::new(
42                ErrorCode::PlanTooLarge,
43                "source exceeds the batch source limit",
44            ));
45        }
46        Ok(Self {
47            source,
48            edits: Vec::new(),
49            occupied: Occupancy::new(),
50            totals: Totals::empty(),
51            output_size: source.len(),
52            limits,
53        })
54    }
55
56    /// Admits one edit or leaves the batch unchanged on failure.
57    pub fn push(&mut self, edit: ByteEdit) -> Result<(), EditError> {
58        let order = self.edits.len();
59        self.ensure_count(order)?;
60        validate_byte_edit(self.source, &edit, order)?;
61        validate_before(self.source, &edit, order)?;
62        let totals = add_totals(self.totals, &edit, self.limits, order)?;
63        self.ensure_no_overlap(&edit, order, None)?;
64        let output_size = check_output(
65            self.source.len(),
66            totals,
67            self.limits.max_output_bytes,
68            Some(order),
69        )?;
70        self.commit_one(edit, order, totals, output_size);
71        Ok(())
72    }
73
74    /// Admits every edit atomically, preserving input order at equal offsets.
75    pub fn push_batch(&mut self, edits: Vec<ByteEdit>) -> Result<(), EditError> {
76        let base = self.edits.len();
77        self.ensure_batch_count(edits.len())?;
78        for (offset, edit) in edits.iter().enumerate() {
79            let order = admission_order(base, offset)?;
80            validate_byte_edit(self.source, edit, order)?;
81        }
82        let mut totals = self.totals;
83        for (offset, edit) in edits.iter().enumerate() {
84            let order = admission_order(base, offset)?;
85            validate_before(self.source, edit, order)?;
86        }
87        for (offset, edit) in edits.iter().enumerate() {
88            let order = admission_order(base, offset)?;
89            totals = add_totals(totals, edit, self.limits, order)?;
90        }
91        let mut staged = Vec::with_capacity(edits.len());
92        let mut staged_occupancy = Occupancy::new();
93        for (offset, edit) in edits.into_iter().enumerate() {
94            let order = admission_order(base, offset)?;
95            self.ensure_no_overlap(&edit, order, Some(&staged_occupancy))?;
96            stage_edit(edit, order, &mut staged, &mut staged_occupancy);
97        }
98        let output_size = check_output(
99            self.source.len(),
100            totals,
101            self.limits.max_output_bytes,
102            base.checked_add(staged.len())
103                .and_then(|value| value.checked_sub(1)),
104        )?;
105        sort_prepared(&mut staged);
106        self.edits = merge_sorted(core::mem::take(&mut self.edits), staged);
107        self.occupied.extend(staged_occupancy);
108        self.totals = totals;
109        self.output_size = output_size;
110        Ok(())
111    }
112
113    /// Consumes the builder without cloning replacement strings.
114    pub fn finish(self) -> Result<PreparedEdits<'source>, EditError> {
115        check_output(
116            self.source.len(),
117            self.totals,
118            self.limits.max_output_bytes,
119            None,
120        )?;
121        Ok(PreparedEdits::from_validated_parts(
122            self.source,
123            self.edits,
124            self.output_size,
125            self.limits.max_edits,
126            self.limits.max_output_bytes,
127        ))
128    }
129
130    #[must_use]
131    pub fn len(&self) -> usize {
132        self.edits.len()
133    }
134
135    #[must_use]
136    pub fn is_empty(&self) -> bool {
137        self.edits.is_empty()
138    }
139
140    fn ensure_count(&self, order: usize) -> Result<(), EditError> {
141        if order >= self.limits.max_edits {
142            return Err(EditError::new(
143                ErrorCode::PlanTooLarge,
144                "edit count exceeds the batch limit",
145            )
146            .at_edit(order));
147        }
148        Ok(())
149    }
150
151    fn ensure_batch_count(&self, incoming: usize) -> Result<(), EditError> {
152        if incoming > self.limits.max_edits.saturating_sub(self.edits.len()) {
153            return Err(EditError::new(
154                ErrorCode::PlanTooLarge,
155                "edit count exceeds the batch limit",
156            )
157            .at_edit(self.limits.max_edits));
158        }
159        Ok(())
160    }
161
162    fn ensure_no_overlap(
163        &self,
164        edit: &ByteEdit,
165        order: usize,
166        staged: Option<&Occupancy>,
167    ) -> Result<(), EditError> {
168        if let Some(related) = find_overlap(&self.occupied, staged, edit.start, edit.end) {
169            return Err(EditError::new(
170                ErrorCode::OverlappingEdits,
171                format!("edits {related} and {order} overlap"),
172            )
173            .at_edit(order)
174            .with_related_edit(related));
175        }
176        Ok(())
177    }
178
179    fn commit_one(&mut self, edit: ByteEdit, order: usize, totals: Totals, output_size: usize) {
180        let prepared = into_prepared(edit, order, &mut self.occupied);
181        let position = self
182            .edits
183            .partition_point(|current| compare_prepared(current, &prepared).is_le());
184        self.edits.insert(position, prepared);
185        self.totals = totals;
186        self.output_size = output_size;
187    }
188}