Skip to main content

quillmark_core/document/
edit.rs

1//! Typed mutators for [`Document`] and [`Card`] with invariant enforcement.
2//!
3//! Every successful mutator leaves the document with every user field name
4//! matching `[a-z_][a-z0-9_]*` and every composable `$kind` passing
5//! `meta::is_valid_kind_name`, so the result is safely serializable via
6//! [`Document::to_plate_json`]. Mutators never modify `warnings` — those
7//! are immutable parse-time observations.
8//!
9//! Payload/body mutators live on [`Card`] (`set_field`, `set_fill`,
10//! `remove_field`, `set_ext`, `remove_ext`, `set_ext_namespace`,
11//! `remove_ext_namespace`, `replace_body`); [`Document`] keeps
12//! document-level ops (quill-ref, push/insert/remove/move card).
13//!
14//! The `$ext` mutators are unguarded: `$ext` is an opaque mapping that
15//! never reaches the plate JSON backends consume, so there is no field-name
16//! or kind invariant to enforce — only that the value is a mapping, which
17//! the types already guarantee.
18
19use unicode_normalization::UnicodeNormalization;
20
21use crate::document::meta::{validate_composable_kind, CardKindError};
22use crate::document::{Card, Document, Payload};
23use crate::value::QuillValue;
24use crate::version::QuillReference;
25
26/// `true` if `name` matches `[a-z_][a-z0-9_]*` after NFC normalisation.
27pub fn is_valid_field_name(name: &str) -> bool {
28    let normalized: String = name.nfc().collect();
29    if normalized.is_empty() {
30        return false;
31    }
32    let mut chars = normalized.chars();
33    let first = chars.next().unwrap();
34    if !first.is_ascii_lowercase() && first != '_' {
35        return false;
36    }
37    for ch in chars {
38        if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() && ch != '_' {
39            return false;
40        }
41    }
42    true
43}
44
45/// Errors returned by document and card mutators.
46#[derive(Debug, Clone, PartialEq, thiserror::Error)]
47pub enum EditError {
48    #[error("invalid field name '{0}': must match [a-z_][a-z0-9_]*")]
49    InvalidFieldName(String),
50
51    #[error("invalid card kind '{0}': must match [a-z_][a-z0-9_]*")]
52    InvalidKindName(String),
53
54    #[error("card kind 'main' is reserved for the document root")]
55    ReservedKind,
56
57    #[error("index {index} is out of range (len = {len})")]
58    IndexOutOfRange { index: usize, len: usize },
59}
60
61impl Document {
62    pub fn set_quill_ref(&mut self, reference: QuillReference) {
63        self.main_mut().payload_mut().set_quill(reference);
64    }
65
66    pub fn card_mut(&mut self, index: usize) -> Option<&mut Card> {
67        self.cards_mut().get_mut(index)
68    }
69
70    /// Append a composable card. Its `$kind` must be a valid, non-reserved
71    /// composable kind ([`EditError::InvalidKindName`] /
72    /// [`EditError::ReservedKind`] otherwise) — the invariant for any card in
73    /// the cards list, enforced here so every entry path shares it.
74    pub fn push_card(&mut self, card: Card) -> Result<(), EditError> {
75        Self::check_composable_kind(&card)?;
76        self.cards_vec_mut().push(card);
77        Ok(())
78    }
79
80    /// Insert a composable card at `index` (`index > len` →
81    /// [`EditError::IndexOutOfRange`]; invalid `$kind` →
82    /// [`EditError::InvalidKindName`] / [`EditError::ReservedKind`]).
83    pub fn insert_card(&mut self, index: usize, card: Card) -> Result<(), EditError> {
84        let len = self.cards().len();
85        if index > len {
86            return Err(EditError::IndexOutOfRange { index, len });
87        }
88        Self::check_composable_kind(&card)?;
89        self.cards_vec_mut().insert(index, card);
90        Ok(())
91    }
92
93    /// Validate that `card`'s `$kind` is a valid, non-reserved composable kind.
94    /// A card with no `$kind` is rejected as an invalid (empty) name.
95    fn check_composable_kind(card: &Card) -> Result<(), EditError> {
96        let kind = card.kind().unwrap_or("");
97        validate_composable_kind(kind).map_err(|e| match e {
98            CardKindError::InvalidName => EditError::InvalidKindName(kind.to_string()),
99            CardKindError::Reserved => EditError::ReservedKind,
100        })
101    }
102
103    pub fn remove_card(&mut self, index: usize) -> Option<Card> {
104        if index >= self.cards().len() {
105            return None;
106        }
107        Some(self.cards_vec_mut().remove(index))
108    }
109
110    /// Replace the `$kind` of the composable card at `index`.
111    ///
112    /// Only the `$kind` metadata changes; the payload and body are untouched
113    /// (field-bag semantics). Old-schema fields linger in the bag; new-schema
114    /// fields are absent until set explicitly. Schema migration is the caller's
115    /// responsibility — this is a structural primitive.
116    ///
117    /// Returns [`EditError::IndexOutOfRange`], [`EditError::InvalidKindName`],
118    /// or [`EditError::ReservedKind`] on constraint violations.
119    pub fn set_card_kind(
120        &mut self,
121        index: usize,
122        new_kind: impl Into<String>,
123    ) -> Result<(), EditError> {
124        let new_kind = new_kind.into();
125        validate_composable_kind(&new_kind).map_err(|e| match e {
126            CardKindError::InvalidName => EditError::InvalidKindName(new_kind.clone()),
127            CardKindError::Reserved => EditError::ReservedKind,
128        })?;
129        let len = self.cards().len();
130        let card = self
131            .card_mut(index)
132            .ok_or(EditError::IndexOutOfRange { index, len })?;
133        card.payload_mut().set_kind(new_kind);
134        Ok(())
135    }
136
137    /// Move card at `from` to position `to`. No-op when `from == to`.
138    /// Either index out of range → [`EditError::IndexOutOfRange`].
139    pub fn move_card(&mut self, from: usize, to: usize) -> Result<(), EditError> {
140        let len = self.cards().len();
141        if from >= len {
142            return Err(EditError::IndexOutOfRange { index: from, len });
143        }
144        if to >= len {
145            return Err(EditError::IndexOutOfRange { index: to, len });
146        }
147        if from == to {
148            return Ok(());
149        }
150        let card = self.cards_vec_mut().remove(from);
151        self.cards_vec_mut().insert(to, card);
152        Ok(())
153    }
154}
155
156impl Card {
157    /// Create a composable card with the given kind, no fields, and an empty body.
158    pub fn new(kind: impl Into<String>) -> Result<Self, EditError> {
159        let kind = kind.into();
160        validate_composable_kind(&kind).map_err(|e| match e {
161            CardKindError::InvalidName => EditError::InvalidKindName(kind.clone()),
162            CardKindError::Reserved => EditError::ReservedKind,
163        })?;
164        let mut payload = Payload::new();
165        payload.set_kind(kind);
166        Ok(Card::from_parts(payload, String::new()))
167    }
168
169    /// Set a payload field, clearing any `!fill` marker on that key.
170    ///
171    /// Returns [`EditError::InvalidFieldName`] when `name` does not match
172    /// `[a-z_][a-z0-9_]*`.
173    pub fn set_field(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
174        if !is_valid_field_name(name) {
175            return Err(EditError::InvalidFieldName(name.to_string()));
176        }
177        self.payload_mut().insert(name.to_string(), value);
178        Ok(())
179    }
180
181    /// Set a payload field and mark it as a `!fill` placeholder.
182    /// `Null` emits as `key: !fill`; scalars/sequences as `key: !fill <value>`.
183    /// Same validation as [`Card::set_field`].
184    pub fn set_fill(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
185        if !is_valid_field_name(name) {
186            return Err(EditError::InvalidFieldName(name.to_string()));
187        }
188        self.payload_mut().insert_fill(name.to_string(), value);
189        Ok(())
190    }
191
192    /// Remove a payload field; returns `Ok(None)` if the name is absent.
193    /// Same validation as [`Card::set_field`].
194    pub fn remove_field(&mut self, name: &str) -> Result<Option<QuillValue>, EditError> {
195        if !is_valid_field_name(name) {
196            return Err(EditError::InvalidFieldName(name.to_string()));
197        }
198        Ok(self.payload_mut().remove(name))
199    }
200
201    /// Replace the card's opaque `$ext` map wholesale, inserting it at the
202    /// canonical position (after `$quill`/`$kind`/`$id`, before user fields)
203    /// when none existed. Passing an empty map records an explicit `$ext: {}`.
204    ///
205    /// `$ext` carries out-of-band consumer state (editor renames, agent
206    /// annotations, …) and is stripped from [`Document::to_plate_json`], so a
207    /// write here can never affect a render. Any nested comments attached to a
208    /// replaced `$ext` are dropped.
209    pub fn set_ext(&mut self, value: serde_json::Map<String, serde_json::Value>) {
210        self.payload_mut().set_ext(value);
211    }
212
213    /// Remove the card's `$ext` map *entirely*, returning the previous map if
214    /// present. This is a blunt escape hatch — it discards every namespace
215    /// (`$ext.presentation`, `$ext.agent`, …) at once. To clear consumer
216    /// state, prefer [`Card::remove_ext_namespace`], which drops only your
217    /// own slot and leaves sibling consumers' state intact.
218    pub fn remove_ext(&mut self) -> Option<serde_json::Map<String, serde_json::Value>> {
219        self.payload_mut().take_ext()
220    }
221
222    /// Merge `value` into the card's `$ext` map under `namespace`, creating
223    /// the map when absent and replacing any existing value at that key.
224    ///
225    /// This is the recommended way to write `$ext`: it preserves sibling
226    /// namespaces, so independent consumers keying on their own slot
227    /// (`$ext.presentation`, `$ext.agent`, …) don't clobber each other.
228    pub fn set_ext_namespace(&mut self, namespace: impl Into<String>, value: serde_json::Value) {
229        let mut map = self.payload_mut().take_ext().unwrap_or_default();
230        map.insert(namespace.into(), value);
231        self.payload_mut().set_ext(map);
232    }
233
234    /// Remove `namespace` from the card's `$ext` map, returning the value
235    /// that was stored there (or `None` when the map or the key was absent).
236    ///
237    /// This is the recommended way to clear `$ext` state: it is the
238    /// namespace-scoped inverse of [`Card::set_ext_namespace`] and preserves
239    /// sibling namespaces, where [`Card::remove_ext`] would wipe them all.
240    /// When removing the last namespace empties the map, the `$ext` entry is
241    /// dropped entirely (not left as `$ext: {}`), so
242    /// `set_ext_namespace(ns, v)` followed by `remove_ext_namespace(ns)`
243    /// restores a card that had no `$ext` to its original state.
244    pub fn remove_ext_namespace(&mut self, namespace: &str) -> Option<serde_json::Value> {
245        let mut map = self.payload_mut().take_ext()?;
246        let removed = map.remove(namespace);
247        if !map.is_empty() {
248            self.payload_mut().set_ext(map);
249        }
250        removed
251    }
252
253    pub fn replace_body(&mut self, body: impl Into<String>) {
254        self.overwrite_body(body.into());
255    }
256}