1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! The format-agnostic document seam.
use crate::{CommentKind, Commented, EditError, Expr, Feature, Step, Value};
/// A parsed config document.
///
/// Each format module implements this over its own lossless CST. It is the
/// interface the CLI drives, uniform across JSONC/INI/env: serialize
/// losslessly, project to the [`Value`] model for querying/conversion, and
/// report the format's [`Feature`] set.
///
/// Mutation (`set`/`delete`/`append`) will extend this trait with M2; for now it
/// covers the read/query path.
pub trait Document {
/// Byte-identical serialization for an unedited document (the round-trip
/// invariant). Reflects in-place edits once mutation lands.
fn to_source(&self) -> String;
/// Project to the value model for querying and conversion. Trivia (comments,
/// layout) is dropped - this is the data-model view, not the source view.
fn to_value(&self) -> Value;
/// Project to one value **per top-level document**. Only YAML has a
/// multi-document stream (`---`-separated); every other format is a single
/// document, so the default is `[to_value()]`. The CLI evaluates a query
/// against each in turn and concatenates the results, so `.kind` over a
/// multi-doc stream yields one result per document.
fn to_values(&self) -> Vec<Value> {
vec![self.to_value()]
}
/// The format's capabilities.
fn features(&self) -> &'static [Feature];
/// Apply a mutation expression (assignment / `del`) in place,
/// format-preserving. Query expressions should be evaluated against
/// [`Document::to_value`] instead; use [`Expr::is_mutation`] to choose.
/// Returns any non-fatal warnings (e.g. a multi-document `select` predicate
/// that could not be evaluated against some document, so that document was
/// skipped) for the CLI to surface; an empty vec on a clean edit.
fn apply(&mut self, expr: &Expr) -> Result<Vec<String>, EditError>;
/// Whether the source contains any comments - used to warn on conversion,
/// which drops them.
fn has_comments(&self) -> bool;
/// Project to the comment-annotated value model ([`Commented`]) so
/// conversion can carry comments across formats. Shape and order must match
/// [`Document::to_value`] exactly (same keys, same merge/resolution rules) -
/// the CLI pairs the two projections by position. `None` means the format
/// doesn't extract comments; conversion then falls back to the plain value
/// path and warns that comments were dropped.
fn to_commented(&self) -> Option<Commented> {
None
}
/// One comment-annotated projection **per top-level document** (the comment
/// analogue of [`Document::to_values`]). Only YAML has multiple; the default
/// is the single [`Document::to_commented`], so a comment query maps over
/// every document of a multi-document stream.
fn to_commented_all(&self) -> Vec<Commented> {
self.to_commented().into_iter().collect()
}
/// The **original source text** of each node selected by `path`, in document
/// order (aligned 1:1 with [`crate::eval`]'s results for the same path). This
/// is the format-preserving "get": a structural query returns the exact bytes
/// - comments, indentation, quoting - rather than a re-serialized value.
///
/// The default returns empty, meaning "this format doesn't source-slice";
/// the caller then falls back to emitting the value in the target format.
/// Only formats with structural values (JSONC, YAML) need override it.
fn source_slice(&self, path: &[Step]) -> Vec<String> {
let _ = path;
Vec::new()
}
/// The name of the format a query should default its output to, when this
/// document's own format isn't itself emittable. A lens (frontmatter) is not
/// an output format, so a query over it renders in the underlying block's
/// format (`"yaml"`/`"toml"`/`"json"`) instead. `None` (the default) means
/// "my own format is the natural output" - the ordinary case.
fn inner_format(&self) -> Option<&'static str> {
None
}
/// Set the `kind` comment on the node at `path` to `text` (raw, unwrapped),
/// format-preserving: only that comment's bytes change, or one comment line
/// is inserted. Multi-line head/foot text is wrapped to the document's
/// envelope by the implementation. Returns any warnings (a layout that had
/// to expand to hold the comment, or a kind remapped to one the format
/// supports). The default rejects - comment editing is added per format.
fn set_comment(
&mut self,
path: &[Step],
kind: CommentKind,
text: &str,
) -> Result<Vec<String>, EditError> {
let _ = (path, kind, text);
Err(EditError::new(
"editing comments (`#`) isn't supported for this format yet",
))
}
/// Delete the `kind` comment on the node at `path` (a miss is a no-op).
/// The default rejects - added per format alongside [`Document::set_comment`].
fn delete_comment(&mut self, path: &[Step], kind: CommentKind) -> Result<(), EditError> {
let _ = (path, kind);
Err(EditError::new(
"deleting comments (`#`) isn't supported for this format yet",
))
}
/// Set the `kind` comment at `path`, scoped to a **single document** of a
/// multi-document stream (used by bulk `comments |= f`, where each new
/// comment derives from that comment's own text and so must not leak across
/// documents). Single-document formats have one document, so the default
/// ignores `doc` and sets it the ordinary way.
fn set_comment_in_doc(
&mut self,
doc: usize,
path: &[Step],
kind: CommentKind,
text: &str,
) -> Result<Vec<String>, EditError> {
let _ = doc;
self.set_comment(path, kind, text)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A minimal document that overrides only the required methods, so the
/// trait defaults (no comment extraction, no source slices, comment editing
/// rejected) are exercised.
struct Bare;
impl Document for Bare {
fn to_source(&self) -> String {
String::new()
}
fn to_value(&self) -> Value {
Value::Null
}
fn features(&self) -> &'static [Feature] {
&[]
}
fn apply(&mut self, _expr: &Expr) -> Result<Vec<String>, EditError> {
Ok(Vec::new())
}
fn has_comments(&self) -> bool {
false
}
}
#[test]
fn trait_defaults() {
let mut d = Bare;
assert!(d.to_commented().is_none());
assert!(d.source_slice(&[]).is_empty());
assert!(d.set_comment(&[], CommentKind::Head, "x").is_err());
assert!(d.delete_comment(&[], CommentKind::Head).is_err());
}
}