Skip to main content

cdx_core/document/
state.rs

1use chrono::Utc;
2
3use crate::manifest::Lineage;
4use crate::{DocumentId, DocumentState, Result};
5
6use super::Document;
7use super::MutableResource;
8
9impl Document {
10    /// Submit the document for review.
11    ///
12    /// Transitions from `draft` to `review` state. This computes the document ID
13    /// and stores it in the manifest.
14    ///
15    /// # Errors
16    ///
17    /// Returns an error if:
18    /// - The document is not in draft state
19    /// - Computing the document ID fails
20    pub fn submit_for_review(&mut self) -> Result<()> {
21        if self.manifest.state != DocumentState::Draft {
22            return Err(crate::Error::InvalidStateTransition {
23                from: self.manifest.state,
24                to: DocumentState::Review,
25            });
26        }
27
28        // Compute and store the document ID
29        let doc_id = self.compute_id()?;
30        self.manifest.id = doc_id;
31        self.manifest.state = DocumentState::Review;
32        self.manifest.modified = Utc::now();
33
34        Ok(())
35    }
36
37    /// Freeze the document.
38    ///
39    /// Transitions from `review` to `frozen` state. This requires:
40    /// - At least one signature
41    /// - Lineage information (parent reference or explicit root)
42    /// - At least one precise layout (for visual reproduction)
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if:
47    /// - The document is not in review state
48    /// - No signatures are present
49    /// - No lineage is set
50    /// - No precise layout is present
51    pub fn freeze(&mut self) -> Result<()> {
52        if self.manifest.state != DocumentState::Review {
53            return Err(crate::Error::InvalidStateTransition {
54                from: self.manifest.state,
55                to: DocumentState::Frozen,
56            });
57        }
58
59        // Verify requirements
60        if !self.has_signatures() {
61            return Err(crate::Error::StateRequirementNotMet {
62                state: DocumentState::Frozen,
63                requirement: "at least one signature".to_string(),
64            });
65        }
66
67        if self.manifest.lineage.is_none() {
68            return Err(crate::Error::StateRequirementNotMet {
69                state: DocumentState::Frozen,
70                requirement: "lineage information".to_string(),
71            });
72        }
73
74        if !self.manifest.has_precise_layout() {
75            return Err(crate::Error::StateRequirementNotMet {
76                state: DocumentState::Frozen,
77                requirement: "at least one precise layout".to_string(),
78            });
79        }
80
81        // Ensure document ID is computed
82        if self.manifest.id.is_pending() {
83            let doc_id = self.compute_id()?;
84            self.manifest.id = doc_id;
85        }
86
87        self.manifest.state = DocumentState::Frozen;
88        self.manifest.modified = Utc::now();
89
90        Ok(())
91    }
92
93    /// Publish the document.
94    ///
95    /// Transitions from `frozen` to `published` state.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the document is not in frozen state.
100    pub fn publish(&mut self) -> Result<()> {
101        if self.manifest.state != DocumentState::Frozen {
102            return Err(crate::Error::InvalidStateTransition {
103                from: self.manifest.state,
104                to: DocumentState::Published,
105            });
106        }
107
108        self.manifest.state = DocumentState::Published;
109        self.manifest.modified = Utc::now();
110
111        Ok(())
112    }
113
114    /// Revert the document to draft state.
115    ///
116    /// Transitions from `review` back to `draft` state. This is only allowed
117    /// if the document has no signatures (to prevent removing signed content).
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if:
122    /// - The document is not in review state
123    /// - The document has signatures
124    pub fn revert_to_draft(&mut self) -> Result<()> {
125        if self.manifest.state != DocumentState::Review {
126            return Err(crate::Error::InvalidStateTransition {
127                from: self.manifest.state,
128                to: DocumentState::Draft,
129            });
130        }
131
132        if self.has_signatures() {
133            return Err(crate::Error::ValidationFailed {
134                reason: "cannot revert to draft: document has signatures".to_string(),
135            });
136        }
137
138        self.manifest.state = DocumentState::Draft;
139        self.manifest.id = DocumentId::pending();
140        self.manifest.modified = Utc::now();
141
142        Ok(())
143    }
144
145    /// Fork the document to create a new draft with lineage.
146    ///
147    /// Creates a new document in draft state that references this document
148    /// as its parent in the lineage chain. The forked document:
149    /// - Has a new (pending) document ID
150    /// - Is in draft state
151    /// - Has lineage pointing to this document with ancestor chain
152    /// - Has incremented version number and depth
153    /// - Has no signatures
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if computing the document ID fails.
158    pub fn fork(&self) -> Result<Document> {
159        // Compute the current document's ID for lineage
160        let parent_id = if self.manifest.id.is_pending() {
161            self.compute_id()?
162        } else {
163            self.manifest.id.clone()
164        };
165
166        // Create lineage using from_parent to properly track ancestors
167        let lineage = Lineage::from_parent(parent_id, self.manifest.lineage.as_ref());
168
169        // Clone the document
170        let mut forked = self.clone();
171
172        // Reset to draft state
173        forked.manifest.id = DocumentId::pending();
174        forked.manifest.state = DocumentState::Draft;
175        forked.manifest.created = Utc::now();
176        forked.manifest.modified = Utc::now();
177        forked.manifest.lineage = Some(lineage);
178        forked.manifest.security = None;
179        #[cfg(feature = "signatures")]
180        {
181            forked.signature_file = None;
182        }
183        #[cfg(feature = "encryption")]
184        {
185            forked.encryption_metadata = None;
186        }
187
188        Ok(forked)
189    }
190
191    /// Set lineage information for this document.
192    ///
193    /// This is used to establish lineage before freezing a document.
194    /// For the first version of a document, call with `None` as parent
195    /// to create a root lineage entry.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the document is in an immutable state.
200    pub fn set_lineage(
201        &mut self,
202        parent: Option<DocumentId>,
203        version: u32,
204        note: Option<String>,
205    ) -> Result<()> {
206        self.require_mutable("modify lineage")?;
207
208        let lineage = if let Some(parent_id) = parent {
209            Lineage::from_parent(parent_id, None).with_note(note.unwrap_or_default())
210        } else {
211            let mut l = Lineage::root();
212            l.version = Some(version);
213            if let Some(n) = note {
214                l = l.with_note(n);
215            }
216            l
217        };
218
219        self.manifest.lineage = Some(lineage);
220        self.manifest.modified = Utc::now();
221
222        Ok(())
223    }
224}