Skip to main content

cdx_core/document/
mod.rs

1//! High-level Document API.
2//!
3//! This module provides the main [`Document`] type and [`DocumentBuilder`]
4//! for working with Codex documents.
5//!
6//! # Opening Documents
7//!
8//! ```rust,ignore
9//! use cdx_core::Document;
10//!
11//! let doc = Document::open("example.cdx")?;
12//! println!("Title: {}", doc.title());
13//! ```
14//!
15//! # Creating Documents
16//!
17//! ```rust,ignore
18//! use cdx_core::{Document, content::{Block, Text}};
19//!
20//! let doc = Document::builder()
21//!     .title("My Document")
22//!     .creator("Jane Doe")
23//!     .add_heading(1, "Introduction")
24//!     .add_paragraph("This is the first paragraph.")
25//!     .build()?;
26//!
27//! doc.save("output.cdx")?;
28//! ```
29
30mod builder;
31mod extensions;
32mod io;
33mod provenance;
34mod security;
35mod state;
36mod verification;
37
38#[cfg(test)]
39mod tests;
40
41pub use builder::DocumentBuilder;
42pub use verification::{ExtensionValidationReport, VerificationReport};
43
44use chrono::Utc;
45
46use crate::content::Content;
47use crate::metadata::DublinCore;
48use crate::{DocumentId, DocumentState, HashAlgorithm, Manifest, Result};
49
50#[cfg(feature = "signatures")]
51use crate::security::SignatureFile;
52
53#[cfg(feature = "encryption")]
54use crate::security::EncryptionMetadata;
55
56use crate::extensions::academic::NumberingConfig;
57use crate::extensions::{Bibliography, CommentThread, FormData, JsonLdMetadata, PhantomClusters};
58
59/// Trait for resources that can be in mutable or immutable states.
60///
61/// This trait provides a common pattern for checking if a resource
62/// can be modified and returning an appropriate error if not.
63trait MutableResource {
64    /// Get the current document state.
65    fn state(&self) -> DocumentState;
66
67    /// Check if the resource can be modified, returning an error if not.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`Error::ImmutableDocument`] if the resource is in an immutable state.
72    fn require_mutable(&self, action: &str) -> Result<()> {
73        if self.state().is_immutable() {
74            return Err(crate::Error::ImmutableDocument {
75                action: action.to_string(),
76                state: self.state(),
77            });
78        }
79        Ok(())
80    }
81}
82
83/// Generates the five standard accessor methods for an optional extension field:
84/// - `$field(&self) -> Option<&$type>` — immutable access
85/// - `$field_mut(&mut self) -> Result<Option<&mut $type>>` — mutable access
86/// - `has_$field(&self) -> bool` — presence check
87/// - `set_$field(&mut self, value: $type) -> Result<()>` — set value
88/// - `clear_$field(&mut self) -> Result<()>` — remove value
89macro_rules! define_extension_accessors {
90    ($field:ident, $field_mut:ident, $has:ident, $set:ident, $clear:ident, $type:ty, $label:expr) => {
91        #[doc = concat!("Get the ", $label, ", if present.")]
92        #[must_use]
93        pub fn $field(&self) -> Option<&$type> {
94            self.$field.as_ref()
95        }
96
97        #[doc = concat!("Get a mutable reference to the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")]
98        pub fn $field_mut(&mut self) -> Result<Option<&mut $type>> {
99            self.require_mutable(concat!("modify ", $label))?;
100            self.manifest.modified = chrono::Utc::now();
101            Ok(self.$field.as_mut())
102        }
103
104        #[doc = concat!("Check if the document has ", $label, ".")]
105        #[must_use]
106        pub fn $has(&self) -> bool {
107            self.$field.is_some()
108        }
109
110        #[doc = concat!("Set the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")]
111        pub fn $set(&mut self, value: $type) -> Result<()> {
112            self.require_mutable(concat!("set ", $label))?;
113            self.$field = Some(value);
114            self.manifest.modified = chrono::Utc::now();
115            Ok(())
116        }
117
118        #[doc = concat!("Remove the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")]
119        pub fn $clear(&mut self) -> Result<()> {
120            self.require_mutable(concat!("remove ", $label))?;
121            self.$field = None;
122            self.manifest.modified = chrono::Utc::now();
123            Ok(())
124        }
125    };
126}
127
128// Make macro available to submodules
129pub(crate) use define_extension_accessors;
130
131impl MutableResource for Document {
132    fn state(&self) -> DocumentState {
133        self.manifest.state
134    }
135}
136
137/// A Codex document.
138///
139/// `Document` provides a high-level interface for working with Codex documents,
140/// abstracting away the underlying archive structure.
141#[derive(Debug, Clone)]
142pub struct Document {
143    manifest: Manifest,
144    content: Content,
145    dublin_core: DublinCore,
146    #[cfg(feature = "signatures")]
147    signature_file: Option<SignatureFile>,
148    #[cfg(feature = "encryption")]
149    encryption_metadata: Option<EncryptionMetadata>,
150    /// Academic extension numbering configuration.
151    academic_numbering: Option<NumberingConfig>,
152    /// Collaboration extension comments.
153    comments: Option<CommentThread>,
154    /// Phantom extension clusters.
155    phantom_clusters: Option<PhantomClusters>,
156    /// Forms extension data.
157    form_data: Option<FormData>,
158    /// Semantic extension bibliography.
159    bibliography: Option<Bibliography>,
160    /// JSON-LD metadata for semantic web integration.
161    jsonld_metadata: Option<JsonLdMetadata>,
162}
163
164impl Document {
165    /// Create a new document builder.
166    #[must_use]
167    pub fn builder() -> DocumentBuilder {
168        DocumentBuilder::new()
169    }
170
171    /// Get a reference to the manifest.
172    #[must_use]
173    pub fn manifest(&self) -> &Manifest {
174        &self.manifest
175    }
176
177    /// Get a reference to the content.
178    #[must_use]
179    pub fn content(&self) -> &Content {
180        &self.content
181    }
182
183    /// Get a mutable reference to the content.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the document is in an immutable state.
188    pub fn content_mut(&mut self) -> Result<&mut Content> {
189        self.require_mutable("modify content")?;
190        self.manifest.modified = Utc::now();
191        // Reset document ID so freeze() recomputes it from the modified content
192        if !self.manifest.id.is_pending() {
193            self.manifest.id = DocumentId::pending();
194        }
195        Ok(&mut self.content)
196    }
197
198    /// Get a reference to the Dublin Core metadata.
199    #[must_use]
200    pub fn dublin_core(&self) -> &DublinCore {
201        &self.dublin_core
202    }
203
204    /// Get a mutable reference to the Dublin Core metadata.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the document is in an immutable state.
209    pub fn dublin_core_mut(&mut self) -> Result<&mut DublinCore> {
210        self.require_mutable("modify Dublin Core metadata")?;
211        self.manifest.modified = Utc::now();
212        // Reset document ID so freeze() recomputes it from the modified metadata
213        if !self.manifest.id.is_pending() {
214            self.manifest.id = DocumentId::pending();
215        }
216        Ok(&mut self.dublin_core)
217    }
218
219    /// Get the document title.
220    #[must_use]
221    pub fn title(&self) -> &str {
222        self.dublin_core.title()
223    }
224
225    /// Get the document creators.
226    #[must_use]
227    pub fn creators(&self) -> Vec<&str> {
228        self.dublin_core.creators()
229    }
230
231    /// Get the document state.
232    #[must_use]
233    pub fn state(&self) -> DocumentState {
234        self.manifest.state
235    }
236
237    /// Get the document ID.
238    #[must_use]
239    pub fn id(&self) -> &DocumentId {
240        &self.manifest.id
241    }
242
243    /// Get the hash algorithm used.
244    #[must_use]
245    pub fn hash_algorithm(&self) -> HashAlgorithm {
246        self.manifest.hash_algorithm
247    }
248
249    /// Get a mutable reference to the manifest for advanced modifications.
250    ///
251    /// Use with caution - this bypasses state machine validation.
252    #[must_use]
253    pub fn manifest_mut(&mut self) -> &mut Manifest {
254        &mut self.manifest
255    }
256}