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