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 (call set_lineage for root documents)"
71                    .to_string(),
72            });
73        }
74
75        if !self.manifest.has_precise_layout() {
76            return Err(crate::Error::StateRequirementNotMet {
77                state: DocumentState::Frozen,
78                requirement: "at least one precise layout".to_string(),
79            });
80        }
81
82        // Ensure document ID is computed
83        if self.manifest.id.is_pending() {
84            let doc_id = self.compute_id()?;
85            self.manifest.id = doc_id;
86        }
87
88        self.manifest.state = DocumentState::Frozen;
89        self.manifest.modified = Utc::now();
90
91        Ok(())
92    }
93
94    /// Publish the document.
95    ///
96    /// Transitions from `frozen` to `published` state.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the document is not in frozen state.
101    pub fn publish(&mut self) -> Result<()> {
102        if self.manifest.state != DocumentState::Frozen {
103            return Err(crate::Error::InvalidStateTransition {
104                from: self.manifest.state,
105                to: DocumentState::Published,
106            });
107        }
108
109        self.manifest.state = DocumentState::Published;
110        self.manifest.modified = Utc::now();
111
112        Ok(())
113    }
114
115    /// Revert the document to draft state.
116    ///
117    /// Transitions from `review` back to `draft` state. This is only allowed
118    /// if the document has no signatures (to prevent removing signed content).
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if:
123    /// - The document is not in review state
124    /// - The document has signatures
125    pub fn revert_to_draft(&mut self) -> Result<()> {
126        if self.manifest.state != DocumentState::Review {
127            return Err(crate::Error::InvalidStateTransition {
128                from: self.manifest.state,
129                to: DocumentState::Draft,
130            });
131        }
132
133        if self.has_signatures() {
134            return Err(crate::Error::ValidationFailed {
135                reason: "cannot revert to draft: document has signatures".to_string(),
136            });
137        }
138
139        self.manifest.state = DocumentState::Draft;
140        self.manifest.id = DocumentId::pending();
141        self.manifest.modified = Utc::now();
142
143        Ok(())
144    }
145
146    /// Fork the document to create a new draft with lineage.
147    ///
148    /// Creates a new document in draft state that references this document
149    /// as its parent in the lineage chain. The forked document:
150    /// - Has a new (pending) document ID
151    /// - Is in draft state
152    /// - Has lineage pointing to this document with ancestor chain
153    /// - Has incremented version number and depth
154    /// - Has no signatures
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if computing the document ID fails.
159    pub fn fork(&self) -> Result<Document> {
160        // Compute the current document's ID for lineage
161        let parent_id = if self.manifest.id.is_pending() {
162            self.compute_id()?
163        } else {
164            self.manifest.id.clone()
165        };
166
167        // Create lineage using from_parent to properly track ancestors
168        let lineage = Lineage::from_parent(parent_id, self.manifest.lineage.as_ref());
169
170        // Clone the document
171        let mut forked = self.clone();
172
173        // Reset to draft state
174        forked.manifest.id = DocumentId::pending();
175        forked.manifest.state = DocumentState::Draft;
176        forked.manifest.created = Utc::now();
177        forked.manifest.modified = Utc::now();
178        forked.manifest.lineage = Some(lineage);
179        forked.manifest.security = None;
180        #[cfg(feature = "signatures")]
181        {
182            forked.signature_file = None;
183        }
184        #[cfg(feature = "encryption")]
185        {
186            forked.encryption_metadata = None;
187        }
188
189        Ok(forked)
190    }
191
192    /// Set lineage information for this document.
193    ///
194    /// This is used to establish lineage before freezing a document.
195    /// For the first version of a document, call with `None` as parent
196    /// to create a root lineage entry.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if the document is in an immutable state.
201    pub fn set_lineage(
202        &mut self,
203        parent: Option<DocumentId>,
204        version: u32,
205        note: Option<String>,
206    ) -> Result<()> {
207        self.require_mutable("modify lineage")?;
208
209        let lineage = if let Some(parent_id) = parent {
210            let mut l = Lineage::from_parent(parent_id, None);
211            l.version = Some(version);
212            if let Some(n) = note {
213                l = l.with_note(n);
214            }
215            l
216        } else {
217            let mut l = Lineage::root();
218            l.version = Some(version);
219            if let Some(n) = note {
220                l = l.with_note(n);
221            }
222            l
223        };
224
225        self.manifest.lineage = Some(lineage);
226        self.manifest.modified = Utc::now();
227
228        Ok(())
229    }
230}