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 free of reserved keys in any
4//! payload, with every composable `$kind` passing `meta::is_valid_kind_name`,
5//! and safely serializable via [`Document::to_plate_json`]. Mutators never
6//! modify `warnings` — those are immutable parse-time observations.
7//!
8//! Payload/body mutators live on [`Card`] (`set_field`, `set_fill`,
9//! `remove_field`, `replace_body`); [`Document`] keeps document-level ops
10//! (quill-ref, push/insert/remove/move card).
11
12use unicode_normalization::UnicodeNormalization;
13
14use crate::document::meta::{validate_composable_kind, CardKindError};
15use crate::document::{Card, Document, Payload};
16use crate::value::QuillValue;
17use crate::version::QuillReference;
18
19/// Reserved field names (`BODY`, `CARDS`, `QUILL`, `CARD`). Their presence in
20/// user fields would corrupt the plate wire format or structural invariants.
21pub const RESERVED_NAMES: &[&str] = &["BODY", "CARDS", "QUILL", "CARD"];
22
23#[inline]
24pub fn is_reserved_name(name: &str) -> bool {
25    RESERVED_NAMES.contains(&name)
26}
27
28/// `true` if `name` matches `[a-z_][a-z0-9_]*` after NFC normalisation.
29pub fn is_valid_field_name(name: &str) -> bool {
30    let normalized: String = name.nfc().collect();
31    if normalized.is_empty() {
32        return false;
33    }
34    let mut chars = normalized.chars();
35    let first = chars.next().unwrap();
36    if !first.is_ascii_lowercase() && first != '_' {
37        return false;
38    }
39    for ch in chars {
40        if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() && ch != '_' {
41            return false;
42        }
43    }
44    true
45}
46
47/// Errors returned by document and card mutators.
48#[derive(Debug, Clone, PartialEq, thiserror::Error)]
49pub enum EditError {
50    #[error("reserved name '{0}' cannot be used as a field name")]
51    ReservedName(String),
52
53    #[error("invalid field name '{0}': must match [a-z_][a-z0-9_]*")]
54    InvalidFieldName(String),
55
56    #[error("invalid card kind '{0}': must match [a-z_][a-z0-9_]*")]
57    InvalidKindName(String),
58
59    #[error("card kind 'main' is reserved for the document root")]
60    ReservedKind,
61
62    #[error("index {index} is out of range (len = {len})")]
63    IndexOutOfRange { index: usize, len: usize },
64}
65
66impl Document {
67    pub fn set_quill_ref(&mut self, reference: QuillReference) {
68        self.main_mut().payload_mut().set_quill(reference);
69    }
70
71    pub fn card_mut(&mut self, index: usize) -> Option<&mut Card> {
72        self.cards_mut().get_mut(index)
73    }
74
75    pub fn push_card(&mut self, card: Card) {
76        self.cards_vec_mut().push(card);
77    }
78
79    /// Insert a composable card at `index` (`index > len` → [`EditError::IndexOutOfRange`]).
80    pub fn insert_card(&mut self, index: usize, card: Card) -> Result<(), EditError> {
81        let len = self.cards().len();
82        if index > len {
83            return Err(EditError::IndexOutOfRange { index, len });
84        }
85        self.cards_vec_mut().insert(index, card);
86        Ok(())
87    }
88
89    pub fn remove_card(&mut self, index: usize) -> Option<Card> {
90        if index >= self.cards().len() {
91            return None;
92        }
93        Some(self.cards_vec_mut().remove(index))
94    }
95
96    /// Replace the `$kind` of the composable card at `index`.
97    ///
98    /// Only the `$kind` metadata changes; the payload and body are untouched
99    /// (field-bag semantics). Old-schema fields linger in the bag; new-schema
100    /// fields are absent until set explicitly. Schema migration is the caller's
101    /// responsibility — this is a structural primitive.
102    ///
103    /// Returns [`EditError::IndexOutOfRange`], [`EditError::InvalidKindName`],
104    /// or [`EditError::ReservedKind`] on constraint violations.
105    pub fn set_card_kind(
106        &mut self,
107        index: usize,
108        new_kind: impl Into<String>,
109    ) -> Result<(), EditError> {
110        let new_kind = new_kind.into();
111        validate_composable_kind(&new_kind).map_err(|e| match e {
112            CardKindError::InvalidName => EditError::InvalidKindName(new_kind.clone()),
113            CardKindError::Reserved => EditError::ReservedKind,
114        })?;
115        let len = self.cards().len();
116        let card = self
117            .card_mut(index)
118            .ok_or(EditError::IndexOutOfRange { index, len })?;
119        card.payload_mut().set_kind(new_kind);
120        Ok(())
121    }
122
123    /// Move card at `from` to position `to`. No-op when `from == to`.
124    /// Either index out of range → [`EditError::IndexOutOfRange`].
125    pub fn move_card(&mut self, from: usize, to: usize) -> Result<(), EditError> {
126        let len = self.cards().len();
127        if from >= len {
128            return Err(EditError::IndexOutOfRange { index: from, len });
129        }
130        if to >= len {
131            return Err(EditError::IndexOutOfRange { index: to, len });
132        }
133        if from == to {
134            return Ok(());
135        }
136        let card = self.cards_vec_mut().remove(from);
137        self.cards_vec_mut().insert(to, card);
138        Ok(())
139    }
140}
141
142impl Card {
143    /// Create a composable card with the given kind, no fields, and an empty body.
144    pub fn new(kind: impl Into<String>) -> Result<Self, EditError> {
145        let kind = kind.into();
146        validate_composable_kind(&kind).map_err(|e| match e {
147            CardKindError::InvalidName => EditError::InvalidKindName(kind.clone()),
148            CardKindError::Reserved => EditError::ReservedKind,
149        })?;
150        let mut payload = Payload::new();
151        payload.set_kind(kind);
152        Ok(Card::from_parts(payload, String::new()))
153    }
154
155    /// Set a payload field, clearing any `!fill` marker on that key.
156    ///
157    /// Returns [`EditError::ReservedName`] or [`EditError::InvalidFieldName`].
158    pub fn set_field(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
159        if is_reserved_name(name) {
160            return Err(EditError::ReservedName(name.to_string()));
161        }
162        if !is_valid_field_name(name) {
163            return Err(EditError::InvalidFieldName(name.to_string()));
164        }
165        self.payload_mut().insert(name.to_string(), value);
166        Ok(())
167    }
168
169    /// Set a payload field and mark it as a `!fill` placeholder.
170    /// `Null` emits as `key: !fill`; scalars/sequences as `key: !fill <value>`.
171    /// Same validation as [`Card::set_field`].
172    pub fn set_fill(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
173        if is_reserved_name(name) {
174            return Err(EditError::ReservedName(name.to_string()));
175        }
176        if !is_valid_field_name(name) {
177            return Err(EditError::InvalidFieldName(name.to_string()));
178        }
179        self.payload_mut().insert_fill(name.to_string(), value);
180        Ok(())
181    }
182
183    /// Remove a payload field; returns `Ok(None)` if the name is absent.
184    /// Same validation as [`Card::set_field`].
185    pub fn remove_field(&mut self, name: &str) -> Result<Option<QuillValue>, EditError> {
186        if is_reserved_name(name) {
187            return Err(EditError::ReservedName(name.to_string()));
188        }
189        if !is_valid_field_name(name) {
190            return Err(EditError::InvalidFieldName(name.to_string()));
191        }
192        Ok(self.payload_mut().remove(name))
193    }
194
195    pub fn replace_body(&mut self, body: impl Into<String>) {
196        self.overwrite_body(body.into());
197    }
198}