Skip to content

Commit b0a4909

Browse files
refactor: extract shared apply_patches_impl, fix ops::apply_patches for complex values\n\n- Extract batch-flush logic into pub(crate) apply_patches_impl in document.rs\n- ops::apply_patches now delegates to shared impl (fixes complex Replace via _core API)\n- Add tests: root-level replace, indentation depths 0/2, _core.apply_patches with dict/list\n- 183 tests pass"
1 parent 00313b8 commit b0a4909

4 files changed

Lines changed: 93 additions & 70 deletions

File tree

src/document.rs

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -137,73 +137,69 @@ impl PyDocument {
137137
}
138138

139139
/// Apply patches to this document and return a new document.
140-
/// NOTE: Similar patch-application logic exists in ops::apply_patches (returns String).
141140
fn apply_patches(&self, patches: Vec<PyPatch>) -> PyResult<Self> {
142-
let mut current_doc = self.inner.clone();
143-
let mut batch: Vec<usize> = Vec::new();
144-
145-
for (idx, patch) in patches.iter().enumerate() {
146-
let is_complex_replace = matches!(
147-
&patch.operation.inner,
148-
yamlpatch::Op::Replace(v) if matches!(v, serde_yaml::Value::Mapping(_) | serde_yaml::Value::Sequence(_))
149-
);
150-
151-
if is_complex_replace {
152-
// Flush any pending yamlpatch batch first
153-
if !batch.is_empty() {
154-
let yaml_patches: Vec<yamlpatch::Patch<'_>> = batch
155-
.iter()
156-
.map(|&i| yamlpatch::Patch {
157-
route: patches[i].route.to_yamlpath_route(),
158-
operation: patches[i].operation.inner.clone(),
159-
})
160-
.collect();
161-
current_doc = yamlpatch::apply_yaml_patches(&current_doc, &yaml_patches)
162-
.map_err(|e| {
163-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
164-
"Patch failed: {e}"
165-
))
166-
})?;
167-
batch.clear();
168-
}
141+
let doc = apply_patches_impl(&self.inner, &patches).map_err(|e| {
142+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Patch failed: {e}"))
143+
})?;
144+
Ok(Self { inner: doc })
145+
}
146+
}
169147

170-
// Apply the complex replace directly
171-
let route = patch.route.to_yamlpath_route();
172-
let value = match &patch.operation.inner {
173-
yamlpatch::Op::Replace(v) => v,
174-
_ => unreachable!(),
175-
};
176-
current_doc =
177-
apply_complex_replace(&current_doc, &route, value).map_err(|e| {
178-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
179-
"Patch failed: {e}"
180-
))
181-
})?;
182-
} else {
183-
batch.push(idx);
148+
/// Shared patch-application logic used by both PyDocument::apply_patches and ops::apply_patches.
149+
pub(crate) fn apply_patches_impl(
150+
doc: &yamlpath::Document,
151+
patches: &[PyPatch],
152+
) -> Result<yamlpath::Document, String> {
153+
let mut current_doc = doc.clone();
154+
let mut batch: Vec<usize> = Vec::new();
155+
156+
for (idx, patch) in patches.iter().enumerate() {
157+
let is_complex_replace = matches!(
158+
&patch.operation.inner,
159+
yamlpatch::Op::Replace(v) if matches!(v, serde_yaml::Value::Mapping(_) | serde_yaml::Value::Sequence(_))
160+
);
161+
162+
if is_complex_replace {
163+
// Flush any pending yamlpatch batch first
164+
if !batch.is_empty() {
165+
let yaml_patches: Vec<yamlpatch::Patch<'_>> = batch
166+
.iter()
167+
.map(|&i| yamlpatch::Patch {
168+
route: patches[i].route.to_yamlpath_route(),
169+
operation: patches[i].operation.inner.clone(),
170+
})
171+
.collect();
172+
current_doc = yamlpatch::apply_yaml_patches(&current_doc, &yaml_patches)
173+
.map_err(|e| e.to_string())?;
174+
batch.clear();
184175
}
185-
}
186176

187-
// Flush remaining batch
188-
if !batch.is_empty() {
189-
let yaml_patches: Vec<yamlpatch::Patch<'_>> = batch
190-
.iter()
191-
.map(|&i| yamlpatch::Patch {
192-
route: patches[i].route.to_yamlpath_route(),
193-
operation: patches[i].operation.inner.clone(),
194-
})
195-
.collect();
196-
current_doc = yamlpatch::apply_yaml_patches(&current_doc, &yaml_patches).map_err(
197-
|e| {
198-
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
199-
"Patch failed: {e}"
200-
))
201-
},
202-
)?;
177+
// Apply the complex replace directly
178+
let route = patch.route.to_yamlpath_route();
179+
let value = match &patch.operation.inner {
180+
yamlpatch::Op::Replace(v) => v,
181+
_ => unreachable!(),
182+
};
183+
current_doc = apply_complex_replace(&current_doc, &route, value)?;
184+
} else {
185+
batch.push(idx);
203186
}
187+
}
204188

205-
Ok(Self { inner: current_doc })
189+
// Flush remaining batch
190+
if !batch.is_empty() {
191+
let yaml_patches: Vec<yamlpatch::Patch<'_>> = batch
192+
.iter()
193+
.map(|&i| yamlpatch::Patch {
194+
route: patches[i].route.to_yamlpath_route(),
195+
operation: patches[i].operation.inner.clone(),
196+
})
197+
.collect();
198+
current_doc = yamlpatch::apply_yaml_patches(&current_doc, &yaml_patches)
199+
.map_err(|e| e.to_string())?;
206200
}
201+
202+
Ok(current_doc)
207203
}
208204

209205
fn apply_complex_replace(

src/ops.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,13 @@ impl PyPatch {
157157
}
158158

159159
/// Apply a list of patches to a YAML source string.
160-
/// NOTE: Similar patch-application logic exists in PyDocument::apply_patches (returns Document).
161160
#[pyfunction]
162161
pub fn apply_patches(source: &str, patches: Vec<PyPatch>) -> PyResult<String> {
163162
let document = yamlpath::Document::new(source).map_err(|e| {
164163
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Invalid YAML: {e}"))
165164
})?;
166165

167-
let yaml_patches: Vec<yamlpatch::Patch<'_>> = patches
168-
.iter()
169-
.map(|p| yamlpatch::Patch {
170-
route: p.route.to_yamlpath_route(),
171-
operation: p.operation.inner.clone(),
172-
})
173-
.collect();
174-
175-
let result = yamlpatch::apply_yaml_patches(&document, &yaml_patches).map_err(|e| {
166+
let result = crate::document::apply_patches_impl(&document, &patches).map_err(|e| {
176167
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Patch failed: {e}"))
177168
})?;
178169

tests/test_core_ops.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ def test_append_to_sequence(self):
4949
assert "- c" in result
5050
assert "- a" in result
5151

52+
def test_replace_with_dict(self):
53+
source = "config:\n key: value\n"
54+
patches = [Patch(route=Route(["config"]), operation=Op.replace({"a": 1}))]
55+
result = apply_patches(source, patches)
56+
assert "a: 1" in result
57+
assert "key: value" not in result
58+
59+
def test_replace_with_list(self):
60+
source = "repos: []\n"
61+
patches = [
62+
Patch(route=Route(["repos"]), operation=Op.replace([{"repo": "local"}]))
63+
]
64+
result = apply_patches(source, patches)
65+
assert "repo: local" in result
66+
5267
def test_preserves_comments(self):
5368
source = "# top comment\nname: foo # inline"
5469
patches = [Patch(route=Route(["name"]), operation=Op.replace("bar"))]

tests/test_document.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,27 @@ def test_replace_comment_relocation(self):
248248
result = doc2["repos"]
249249
assert result == [{"repo": "local"}]
250250

251+
def test_replace_root_level_with_dict(self):
252+
doc = Document("key: value\n")
253+
doc2 = doc.replace(value={"new_key": "new_val", "another": 42})
254+
assert doc2["new_key"] == "new_val"
255+
assert doc2["another"] == 42
256+
257+
def test_replace_top_level_key_with_dict(self):
258+
"""Indentation depth 0: top-level key gets value indented at 2 spaces."""
259+
doc = Document("config: old\n")
260+
doc2 = doc.replace("config", value={"a": 1})
261+
assert doc2["config"] == {"a": 1}
262+
# Value should be indented at 2 spaces (base_indent=0 + 2)
263+
assert " a: 1" in doc2.source
264+
265+
def test_replace_depth2_key_with_dict(self):
266+
"""Indentation depth 2: nested key gets value indented at 4 spaces."""
267+
doc = Document("outer:\n config: old\n")
268+
doc2 = doc.replace("outer", "config", value={"a": 1})
269+
assert doc2["outer", "config"] == {"a": 1}
270+
assert " a: 1" in doc2.source
271+
251272

252273
class TestDocumentAdd:
253274
def test_add_key(self):

0 commit comments

Comments
 (0)