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            Ok(self.$field.as_mut())
101        }
102
103        #[doc = concat!("Check if the document has ", $label, ".")]
104        #[must_use]
105        pub fn $has(&self) -> bool {
106            self.$field.is_some()
107        }
108
109        #[doc = concat!("Set the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")]
110        pub fn $set(&mut self, value: $type) -> Result<()> {
111            self.require_mutable(concat!("set ", $label))?;
112            self.$field = Some(value);
113            Ok(())
114        }
115
116        #[doc = concat!("Remove the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")]
117        pub fn $clear(&mut self) -> Result<()> {
118            self.require_mutable(concat!("remove ", $label))?;
119            self.$field = None;
120            Ok(())
121        }
122    };
123}
124
125// Make macro available to submodules
126pub(crate) use define_extension_accessors;
127
128impl MutableResource for Document {
129    fn state(&self) -> DocumentState {
130        self.manifest.state
131    }
132}
133
134/// A Codex document.
135///
136/// `Document` provides a high-level interface for working with Codex documents,
137/// abstracting away the underlying archive structure.
138#[derive(Debug, Clone)]
139pub struct Document {
140    manifest: Manifest,
141    content: Content,
142    dublin_core: DublinCore,
143    #[cfg(feature = "signatures")]
144    signature_file: Option<SignatureFile>,
145    #[cfg(feature = "encryption")]
146    encryption_metadata: Option<EncryptionMetadata>,
147    /// Academic extension numbering configuration.
148    academic_numbering: Option<NumberingConfig>,
149    /// Collaboration extension comments.
150    comments: Option<CommentThread>,
151    /// Phantom extension clusters.
152    phantom_clusters: Option<PhantomClusters>,
153    /// Forms extension data.
154    form_data: Option<FormData>,
155    /// Semantic extension bibliography.
156    bibliography: Option<Bibliography>,
157    /// JSON-LD metadata for semantic web integration.
158    jsonld_metadata: Option<JsonLdMetadata>,
159}
160
161impl Document {
162    /// Create a new document builder.
163    #[must_use]
164    pub fn builder() -> DocumentBuilder {
165        DocumentBuilder::new()
166    }
167
168    /// Get a reference to the manifest.
169    #[must_use]
170    pub fn manifest(&self) -> &Manifest {
171        &self.manifest
172    }
173
174    /// Get a reference to the content.
175    #[must_use]
176    pub fn content(&self) -> &Content {
177        &self.content
178    }
179
180    /// Get a mutable reference to the content.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the document is in an immutable state.
185    pub fn content_mut(&mut self) -> Result<&mut Content> {
186        self.require_mutable("modify content")?;
187        self.manifest.modified = Utc::now();
188        Ok(&mut self.content)
189    }
190
191    /// Get a reference to the Dublin Core metadata.
192    #[must_use]
193    pub fn dublin_core(&self) -> &DublinCore {
194        &self.dublin_core
195    }
196
197    /// Get a mutable reference to the Dublin Core metadata.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the document is in an immutable state.
202    pub fn dublin_core_mut(&mut self) -> Result<&mut DublinCore> {
203        self.require_mutable("modify Dublin Core metadata")?;
204        self.manifest.modified = Utc::now();
205        Ok(&mut self.dublin_core)
206    }
207
208    /// Get the document title.
209    #[must_use]
210    pub fn title(&self) -> &str {
211        self.dublin_core.title()
212    }
213
214    /// Get the document creators.
215    #[must_use]
216    pub fn creators(&self) -> Vec<&str> {
217        self.dublin_core.creators()
218    }
219
220    /// Get the document state.
221    #[must_use]
222    pub fn state(&self) -> DocumentState {
223        self.manifest.state
224    }
225
226    /// Get the document ID.
227    #[must_use]
228    pub fn id(&self) -> &DocumentId {
229        &self.manifest.id
230    }
231
232    /// Get the hash algorithm used.
233    #[must_use]
234    pub fn hash_algorithm(&self) -> HashAlgorithm {
235        self.manifest.hash_algorithm
236    }
237
238    /// Get a mutable reference to the manifest for advanced modifications.
239    ///
240    /// Use with caution - this bypasses state machine validation.
241    #[must_use]
242    pub fn manifest_mut(&mut self) -> &mut Manifest {
243        &mut self.manifest
244    }
245}