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 /// The format's capabilities.
24 fn features(&self) -> &'static [Feature];
25
26 /// Apply a mutation expression (assignment / `del`) in place,
27 /// format-preserving. Query expressions should be evaluated against
28 /// [`Document::to_value`] instead; use [`Expr::is_mutation`] to choose.
29 fn apply(&mut self, expr: &Expr) -> Result<(), EditError>;
30
31 /// Whether the source contains any comments - used to warn on conversion,
32 /// which drops them.
33 fn has_comments(&self) -> bool;
34
35 /// Project to the comment-annotated value model ([`Commented`]) so
36 /// conversion can carry comments across formats. Shape and order must match
37 /// [`Document::to_value`] exactly (same keys, same merge/resolution rules) -
38 /// the CLI pairs the two projections by position. `None` means the format
39 /// doesn't extract comments; conversion then falls back to the plain value
40 /// path and warns that comments were dropped.
41 fn to_commented(&self) -> Option<Commented> {
42 None
43 }
44
45 /// The **original source text** of each node selected by `path`, in document
46 /// order (aligned 1:1 with [`crate::eval`]'s results for the same path). This
47 /// is the format-preserving "get": a structural query returns the exact bytes
48 /// - comments, indentation, quoting - rather than a re-serialized value.
49 ///
50 /// The default returns empty, meaning "this format doesn't source-slice";
51 /// the caller then falls back to emitting the value in the target format.
52 /// Only formats with structural values (JSONC, YAML) need override it.
53 fn source_slice(&self, path: &[Step]) -> Vec<String> {
54 let _ = path;
55 Vec::new()
56 }
57
58 /// Set the `kind` comment on the node at `path` to `text` (raw, unwrapped),
59 /// format-preserving: only that comment's bytes change, or one comment line
60 /// is inserted. Multi-line head/foot text is wrapped to the document's
61 /// envelope by the implementation. Returns any warnings (a layout that had
62 /// to expand to hold the comment, or a kind remapped to one the format
63 /// supports). The default rejects - comment editing is added per format.
64 fn set_comment(
65 &mut self,
66 path: &[Step],
67 kind: CommentKind,
68 text: &str,
69 ) -> Result<Vec<String>, EditError> {
70 let _ = (path, kind, text);
71 Err(EditError::new(
72 "editing comments (`#`) isn't supported for this format yet",
73 ))
74 }
75
76 /// Delete the `kind` comment on the node at `path` (a miss is a no-op).
77 /// The default rejects - added per format alongside [`Document::set_comment`].
78 fn delete_comment(&mut self, path: &[Step], kind: CommentKind) -> Result<(), EditError> {
79 let _ = (path, kind);
80 Err(EditError::new(
81 "deleting comments (`#`) isn't supported for this format yet",
82 ))
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 /// A minimal document that overrides only the required methods, so the
91 /// trait defaults (no comment extraction, no source slices, comment editing
92 /// rejected) are exercised.
93 struct Bare;
94 impl Document for Bare {
95 fn to_source(&self) -> String {
96 String::new()
97 }
98 fn to_value(&self) -> Value {
99 Value::Null
100 }
101 fn features(&self) -> &'static [Feature] {
102 &[]
103 }
104 fn apply(&mut self, _expr: &Expr) -> Result<(), EditError> {
105 Ok(())
106 }
107 fn has_comments(&self) -> bool {
108 false
109 }
110 }
111
112 #[test]
113 fn trait_defaults() {
114 let mut d = Bare;
115 assert!(d.to_commented().is_none());
116 assert!(d.source_slice(&[]).is_empty());
117 assert!(d.set_comment(&[], CommentKind::Head, "x").is_err());
118 assert!(d.delete_comment(&[], CommentKind::Head).is_err());
119 }
120}