You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Support complex values (dicts/lists) in replace and upsert (#19)
* test: add failing tests for replace/upsert with complex values (dicts/lists)
* feat: support complex values (dicts/lists) in Replace operations
Intercept Replace operations with Mapping/Sequence values in
apply_patches before they reach yamlpatch. Perform direct string
surgery using yamlpath for feature location and serde_yaml for
block-style serialization.
Handles: key-colon splitting, inline comment detection and relocation,
indentation computation, batch flushing for mixed patch sequences.
Fixes#18
* 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"
* Support complex values (dicts/lists) in replace and upsert
## Summary
- Intercept `Replace` operations with Mapping/Sequence values in the Rust layer before they reach yamlpatch (which can't handle multi-line serialized values)
- Perform direct string surgery: find key-colon boundary, extract inline comments, compute indentation, serialize via `serde_yaml::to_string()`, and reassemble
- Extract shared `apply_patches_impl` so both `Document.apply_patches()` and `_core.apply_patches()` handle complex values consistently
## Changes
- **src/document.rs**: Add `apply_complex_replace` + 3 helpers (`find_key_colon`, `extract_inline_comment`, `indent_block`), extract `apply_patches_impl` as shared batch-flush logic
- **src/ops.rs**: Delegate to `apply_patches_impl` instead of passing everything to yamlpatch directly
- **tests/test_document.py**: 17 new tests (replace with dict/list, nested, comment relocation, root-level, indentation depths)
- **tests/test_core_ops.py**: 2 new tests for `_core.apply_patches` with complex values
Fixes#18
* Harden complex-replace string surgery and add edge-case tests
- find_key_colon: add bounds guards on post-quote index increments to
prevent overshoot on malformed input; add precondition doc comment
- apply_patches_impl: add doc comment explaining route validity across
batch flushes (symbolic paths, not byte offsets)
- apply_complex_replace: add safety comment on ws_len subtraction
- Tests: empty dict/list, block/folded scalars, flow mappings, quoted
keys with colons, hash-in-value, mixed scalar/complex batch
* fix: collapse nested if into match guard to satisfy clippy
* docs: document 2-space indent assumption as yamlpatch-wide limitation
* docs: clarify indent_block preserves blank lines without trailing whitespace
* docs: explain why single-quote handling is correct for '' escapes
* docs: note O(N) re-parse is consistent with yamlpatch behavior
* refactor: remove find_key_colon and extract_inline_comment to match yamlpatch behavior
Simplify find_key_colon to naive str::find(':'), consistent with
yamlpatch's own Replace implementation. Remove extract_inline_comment
entirely since yamlpatch doesn't preserve inline comments during
Replace either. Removes ~90 lines of quote-aware parsing logic.
When yamlpatch fixes these limitations upstream, yamltrip will
inherit the fixes uniformly across both scalar and complex replaces.
* docs: update spec to reflect removal of comment preservation and simplified colon finding
`Document.replace()` and `Document.upsert()` only accept scalar values. Passing a dict or list raises `PatchError: Patch failed: input is not valid YAML` because yamlpatch's `apply_value_replacement` serializes the value inline after the key colon, producing invalid YAML when the serialized value is multi-line.
9
+
10
+
`Op.add` and `Op.append` already handle complex values correctly. Only `Op.replace` is broken.
11
+
12
+
## Approach
13
+
14
+
Intercept `Replace` operations with complex values (Mapping or Sequence) in yamltrip's Rust layer before they reach yamlpatch. Perform direct string surgery on the document source. Scalar `Replace` and all other operation types pass through to yamlpatch unchanged.
15
+
16
+
## Change Location
17
+
18
+
All Rust changes are in `src/document.rs`. No Python API changes. No yamlpatch changes. No new files.
19
+
20
+
A new function `apply_complex_replace` handles the complex case, called from `PyDocument::apply_patches`.
21
+
22
+
## Algorithm: `apply_complex_replace`
23
+
24
+
Input: a `yamlpath::Document`, a `yamlpath::Route`, and a `serde_yaml::Value` (Mapping or Sequence).
25
+
26
+
1.**Locate the feature** — `document.query_pretty(&route)` to get the byte span with surrounding key context.
27
+
28
+
2.**Extract content** — `extract_with_leading_whitespace(feature)` to get the full `key: value` text. Split at the first `:` (naive `find(':')`, consistent with yamlpatch's own Replace implementation) into `key_part` (through the colon) and the rest.
29
+
30
+
3.**Compute indentation** — Derive base indent from the number of leading spaces on the feature's line. Value content indentation = base indent + 2 spaces.
31
+
32
+
4.**Serialize the new value** — `serde_yaml::to_string(&value)`, strip trailing newline, re-indent each line to the computed value indentation.
33
+
34
+
5.**Assemble replacement text** — `key:\n indented_value`. Inline comments on the value line are not preserved, consistent with yamlpatch's own Replace behavior for scalar values.
35
+
36
+
6.**String surgery** — Replace the byte range from the feature's span (adjusted for leading whitespace) in the document source.
37
+
38
+
7.**Re-parse** — `yamlpath::Document::new(patched_source)` to validate.
39
+
40
+
### Root-level replace
41
+
42
+
If the route is empty, skip key extraction. Replace the entire document content with the serialized value. This matches yamlpatch's existing root-replace behavior.
43
+
44
+
## Patch Routing in `apply_patches`
45
+
46
+
In `PyDocument::apply_patches`, before building the `Vec<yamlpatch::Patch>`:
47
+
48
+
```
49
+
for each patch:
50
+
if patch.operation is Replace(value) AND value is Mapping or Sequence:
51
+
apply_complex_replace(document, route, value)
52
+
re-parse document for subsequent patches
53
+
else:
54
+
collect into yamlpatch batch
55
+
```
56
+
57
+
Patches are applied sequentially. If a complex replace is encountered mid-batch, the preceding yamlpatch batch is flushed first, then the complex replace is applied, then the next batch starts from the updated document.
58
+
59
+
## How Upsert Benefits
60
+
61
+
`Document.upsert()` delegates to `replace()` when the path exists, and to `_create_at()` (which uses `Op.add`/`Op.merge_into`) when it doesn't. Since `Op.add` and `Op.merge_into` already handle complex values, the only broken path is the `replace` delegation — fixed by this change. No changes to `document.py`.
62
+
63
+
## Serialization Style
64
+
65
+
No changes to `Op.add` behavior. Complex values in `Replace` use `serde_yaml::to_string()` which produces block style by default (block mappings, block sequences). This matches the pre-commit config use case where block style is expected.
66
+
67
+
## Testing
68
+
69
+
New tests in `tests/test_document.py`:
70
+
71
+
-`replace()` with a dict value
72
+
-`replace()` with a list value
73
+
-`replace()` with nested dict-in-list-in-dict
74
+
-`upsert()` with a dict value (path exists → goes through replace)
75
+
-`upsert()` with a dict value (path doesn't exist → goes through add, already works)
76
+
- Replacing a complex value with another complex value
77
+
- Root-level replace with complex value
78
+
- Indentation correctness at various nesting depths (0, 2, 4 spaces)
79
+
80
+
## Known Limitations
81
+
82
+
-**Quoted keys with colons** — `find_key_colon` uses a naive `str::find(':')`, which will misparse keys like `"host:port": value`. This is consistent with yamlpatch's own Replace implementation. When yamlpatch fixes this upstream, yamltrip should inherit the fix.
83
+
-**Inline comments** — Not preserved during complex replace, consistent with yamlpatch's scalar Replace behavior.
0 commit comments