Skip to main content

riffdb_types/
export.rs

1//! Closed identities, scope, and snapshot bindings for symbolic export.
2
3use std::error::Error;
4use std::fmt;
5use std::num::NonZeroU64;
6
7use crate::{
8    AdministrationSequence, ApplicationExportOperationId, CapabilityApplicationExportScopeV1,
9    CapabilityId, CommitSequence, ContractBundleHash, ContractLineage, ContractVersion, DatabaseId,
10    QueryModuleHash, ReactiveModuleHash,
11};
12
13/// Maximum immutable module identities bound into one export snapshot.
14pub const MAX_APPLICATION_EXPORT_MODULES: usize = 256;
15
16/// Maximum records released by one symbolic export page.
17pub const MAX_APPLICATION_EXPORT_PAGE_ROWS: usize = 500;
18/// Maximum content bytes released by one symbolic export page.
19pub const MAX_APPLICATION_EXPORT_PAGE_BYTES: usize = 4 * 1024 * 1024;
20/// Maximum retained page commitments for one symbolic export operation.
21pub const MAX_APPLICATION_EXPORT_PAGES: usize = 4_096;
22
23/// Safe construction failure for one symbolic export identity or selection.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum ApplicationExportValueError {
26    /// A closed combination was invalid.
27    InvalidShape,
28    /// A hard count bound was exceeded.
29    LimitExceeded,
30    /// A canonical identity collection was duplicate or unordered.
31    NonCanonical,
32}
33
34impl fmt::Display for ApplicationExportValueError {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        formatter.write_str(match self {
37            Self::InvalidShape => "application export has an invalid closed shape",
38            Self::LimitExceeded => "application export exceeds a hard limit",
39            Self::NonCanonical => "application export identities are not canonical",
40        })
41    }
42}
43
44impl Error for ApplicationExportValueError {}
45
46/// Closed portable record classes. No table, index, or raw key selector exists.
47#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48#[repr(u8)]
49pub enum ApplicationExportClassV1 {
50    /// Current symbolic entity state.
51    Entity = 1,
52    /// Retained typed domain events.
53    Event = 2,
54    /// Separately authorized command provenance.
55    Provenance = 3,
56    /// Separately authorized public-safe administration audit.
57    PublicAudit = 4,
58}
59
60impl ApplicationExportClassV1 {
61    /// Stable closed semantic tag.
62    #[must_use]
63    pub const fn tag(self) -> u8 {
64        self as u8
65    }
66
67    /// Strictly decodes a stable semantic tag.
68    #[must_use]
69    pub const fn from_tag(tag: u8) -> Option<Self> {
70        match tag {
71            1 => Some(Self::Entity),
72            2 => Some(Self::Event),
73            3 => Some(Self::Provenance),
74            4 => Some(Self::PublicAudit),
75            _ => None,
76        }
77    }
78}
79
80/// Canonical hash preimage for one bounded symbolic export page.
81///
82/// Cursor bytes are deliberately excluded: a cursor is a sealed continuation
83/// capability, while the page hash identifies released application content,
84/// its position, and terminal shape.
85pub fn canonical_application_export_page_preimage(
86    operation_id: ApplicationExportOperationId,
87    page_number: NonZeroU64,
88    class: ApplicationExportClassV1,
89    lines: &[&[u8]],
90    class_complete: bool,
91    operation_complete: bool,
92) -> Result<Vec<u8>, ApplicationExportValueError> {
93    let line_count =
94        u32::try_from(lines.len()).map_err(|_| ApplicationExportValueError::LimitExceeded)?;
95    let mut output = Vec::new();
96    output.extend_from_slice(operation_id.as_bytes());
97    output.extend_from_slice(&page_number.get().to_be_bytes());
98    output.push(class.tag());
99    output.push(u8::from(class_complete));
100    output.push(u8::from(operation_complete));
101    output.extend_from_slice(&line_count.to_be_bytes());
102    for line in lines {
103        let line_length =
104            u32::try_from(line.len()).map_err(|_| ApplicationExportValueError::LimitExceeded)?;
105        output.extend_from_slice(&line_length.to_be_bytes());
106        output.extend_from_slice(line);
107    }
108    Ok(output)
109}
110
111/// Exact caller-selected subset of one current V5 lineage grant.
112#[derive(Clone, Eq, PartialEq)]
113pub struct ApplicationExportSelectionV1 {
114    lineage: ContractLineage,
115    scope: CapabilityApplicationExportScopeV1,
116    entities: bool,
117    events: bool,
118    provenance: bool,
119    public_audit: bool,
120}
121
122impl ApplicationExportSelectionV1 {
123    /// Constructs one closed selection. Supporting records cannot stand alone.
124    #[allow(clippy::fn_params_excessive_bools)]
125    pub fn new(
126        lineage: ContractLineage,
127        scope: CapabilityApplicationExportScopeV1,
128        entities: bool,
129        events: bool,
130        provenance: bool,
131        public_audit: bool,
132    ) -> Result<Self, ApplicationExportValueError> {
133        if !entities && !events {
134            return Err(ApplicationExportValueError::InvalidShape);
135        }
136        Ok(Self {
137            lineage,
138            scope,
139            entities,
140            events,
141            provenance,
142            public_audit,
143        })
144    }
145
146    /// Selected exact application lineage.
147    #[must_use]
148    pub const fn lineage(&self) -> &ContractLineage {
149        &self.lineage
150    }
151
152    /// Selected policy-filtered or whole-application scope.
153    #[must_use]
154    pub const fn scope(&self) -> CapabilityApplicationExportScopeV1 {
155        self.scope
156    }
157
158    /// Whether current entities are selected.
159    #[must_use]
160    pub const fn entities(&self) -> bool {
161        self.entities
162    }
163
164    /// Whether retained events are selected.
165    #[must_use]
166    pub const fn events(&self) -> bool {
167        self.events
168    }
169
170    /// Whether separately authorized provenance is selected.
171    #[must_use]
172    pub const fn provenance(&self) -> bool {
173        self.provenance
174    }
175
176    /// Whether separately authorized public-safe audit is selected.
177    #[must_use]
178    pub const fn public_audit(&self) -> bool {
179        self.public_audit
180    }
181
182    /// True only when this operation selected the class explicitly.
183    #[must_use]
184    pub const fn includes(&self, class: ApplicationExportClassV1) -> bool {
185        match class {
186            ApplicationExportClassV1::Entity => self.entities,
187            ApplicationExportClassV1::Event => self.events,
188            ApplicationExportClassV1::Provenance => self.provenance,
189            ApplicationExportClassV1::PublicAudit => self.public_audit,
190        }
191    }
192}
193
194impl fmt::Debug for ApplicationExportSelectionV1 {
195    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196        formatter
197            .debug_struct("ApplicationExportSelectionV1")
198            .field("lineage", &self.lineage)
199            .field("scope", &self.scope)
200            .field("entities", &self.entities)
201            .field("events", &self.events)
202            .field("provenance", &self.provenance)
203            .field("public_audit", &self.public_audit)
204            .finish()
205    }
206}
207
208/// Exact current capability revision bound into one export operation.
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct ApplicationExportAuthorityV1 {
211    capability_id: CapabilityId,
212    capability_revision: NonZeroU64,
213}
214
215impl ApplicationExportAuthorityV1 {
216    /// Binds one current durable capability revision.
217    #[must_use]
218    pub const fn new(capability_id: CapabilityId, capability_revision: NonZeroU64) -> Self {
219        Self {
220            capability_id,
221            capability_revision,
222        }
223    }
224
225    /// Durable capability identity.
226    #[must_use]
227    pub const fn capability_id(self) -> CapabilityId {
228        self.capability_id
229    }
230
231    /// Exact revision revalidated at every release safe point.
232    #[must_use]
233    pub const fn capability_revision(self) -> NonZeroU64 {
234        self.capability_revision
235    }
236}
237
238/// Immutable published source identity captured atomically at export start.
239#[derive(Clone, Eq, PartialEq)]
240pub struct ApplicationExportSnapshotBindingV1 {
241    database_id: DatabaseId,
242    history_incarnation: NonZeroU64,
243    application_frontier: Option<CommitSequence>,
244    administration_frontier: Option<AdministrationSequence>,
245    contract_version: ContractVersion,
246    contract_bundle_hash: ContractBundleHash,
247    query_modules: Vec<QueryModuleHash>,
248    reactive_modules: Vec<ReactiveModuleHash>,
249}
250
251impl ApplicationExportSnapshotBindingV1 {
252    /// Constructs one canonical source identity with sorted unique modules.
253    #[allow(clippy::too_many_arguments)]
254    pub fn new(
255        database_id: DatabaseId,
256        history_incarnation: NonZeroU64,
257        application_frontier: Option<CommitSequence>,
258        administration_frontier: Option<AdministrationSequence>,
259        contract_version: ContractVersion,
260        contract_bundle_hash: ContractBundleHash,
261        query_modules: Vec<QueryModuleHash>,
262        reactive_modules: Vec<ReactiveModuleHash>,
263    ) -> Result<Self, ApplicationExportValueError> {
264        if query_modules.len() > MAX_APPLICATION_EXPORT_MODULES
265            || reactive_modules.len() > MAX_APPLICATION_EXPORT_MODULES
266        {
267            return Err(ApplicationExportValueError::LimitExceeded);
268        }
269        if query_modules.windows(2).any(|pair| pair[0] >= pair[1])
270            || reactive_modules.windows(2).any(|pair| pair[0] >= pair[1])
271        {
272            return Err(ApplicationExportValueError::NonCanonical);
273        }
274        Ok(Self {
275            database_id,
276            history_incarnation,
277            application_frontier,
278            administration_frontier,
279            contract_version,
280            contract_bundle_hash,
281            query_modules,
282            reactive_modules,
283        })
284    }
285
286    /// Durable database identity.
287    #[must_use]
288    pub const fn database_id(&self) -> DatabaseId {
289        self.database_id
290    }
291
292    /// Positive history incarnation.
293    #[must_use]
294    pub const fn history_incarnation(&self) -> NonZeroU64 {
295        self.history_incarnation
296    }
297
298    /// Published application frontier captured with the snapshot.
299    #[must_use]
300    pub const fn application_frontier(&self) -> Option<CommitSequence> {
301        self.application_frontier
302    }
303
304    /// Published administration frontier captured with the snapshot.
305    #[must_use]
306    pub const fn administration_frontier(&self) -> Option<AdministrationSequence> {
307        self.administration_frontier
308    }
309
310    /// Exact active contract version.
311    #[must_use]
312    pub const fn contract_version(&self) -> ContractVersion {
313        self.contract_version
314    }
315
316    /// Exact active contract bundle identity.
317    #[must_use]
318    pub const fn contract_bundle_hash(&self) -> ContractBundleHash {
319        self.contract_bundle_hash
320    }
321
322    /// Canonical active query-module identities.
323    #[must_use]
324    pub fn query_modules(&self) -> &[QueryModuleHash] {
325        &self.query_modules
326    }
327
328    /// Canonical active reactive-module identities.
329    #[must_use]
330    pub fn reactive_modules(&self) -> &[ReactiveModuleHash] {
331        &self.reactive_modules
332    }
333}
334
335impl fmt::Debug for ApplicationExportSnapshotBindingV1 {
336    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337        formatter
338            .debug_struct("ApplicationExportSnapshotBindingV1")
339            .field("database_id", &self.database_id)
340            .field("history_incarnation", &self.history_incarnation)
341            .field("application_frontier", &self.application_frontier)
342            .field("administration_frontier", &self.administration_frontier)
343            .field("contract_version", &self.contract_version)
344            .field("contract_bundle_hash", &self.contract_bundle_hash)
345            .field("query_module_count", &self.query_modules.len())
346            .field("reactive_module_count", &self.reactive_modules.len())
347            .finish()
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn selection_has_no_supporting_only_or_implicit_class_shape() {
357        let lineage = ContractLineage::new("TicketDesk").expect("lineage");
358        assert_eq!(
359            ApplicationExportSelectionV1::new(
360                lineage.clone(),
361                CapabilityApplicationExportScopeV1::WholeApplication,
362                false,
363                false,
364                true,
365                true,
366            ),
367            Err(ApplicationExportValueError::InvalidShape)
368        );
369        let selection = ApplicationExportSelectionV1::new(
370            lineage,
371            CapabilityApplicationExportScopeV1::PrincipalFiltered,
372            true,
373            false,
374            false,
375            false,
376        )
377        .expect("selection");
378        assert!(selection.includes(ApplicationExportClassV1::Entity));
379        assert!(!selection.includes(ApplicationExportClassV1::Event));
380        assert!(!selection.includes(ApplicationExportClassV1::Provenance));
381        assert!(!selection.includes(ApplicationExportClassV1::PublicAudit));
382    }
383}