Skip to main content

edikt_core/
document.rs

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