aster_forge_cloud_files_core/
item.rs

1//! Product-neutral cloud item metadata and directory enumeration pages.
2
3use crate::{
4    CloudFilesCoreError, CloudItemId, CloudItemKey, ContentDigest, ContentRevision,
5    MetadataRevision, PageCursor, Result,
6};
7
8/// Item kind shared by the initial platform-independent model.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CloudItemKind {
11    /// Regular file with content metadata.
12    File,
13    /// Directory whose children are enumerated separately.
14    Directory,
15}
16
17/// Content-specific metadata attached to a regular file.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CloudContentMetadata {
20    revision: ContentRevision,
21    digest: Option<ContentDigest>,
22    size: u64,
23}
24
25impl CloudContentMetadata {
26    /// Creates content metadata for one exact content revision.
27    #[must_use]
28    pub const fn new(revision: ContentRevision, digest: Option<ContentDigest>, size: u64) -> Self {
29        Self {
30            revision,
31            digest,
32            size,
33        }
34    }
35
36    /// Returns the opaque content revision.
37    #[must_use]
38    pub const fn revision(&self) -> &ContentRevision {
39        &self.revision
40    }
41
42    /// Returns the optional algorithm-tagged content digest.
43    #[must_use]
44    pub const fn digest(&self) -> Option<&ContentDigest> {
45        self.digest.as_ref()
46    }
47
48    /// Returns the logical content size in bytes.
49    #[must_use]
50    pub const fn size(&self) -> u64 {
51        self.size
52    }
53
54    /// Consumes the metadata and returns its revision, digest, and size.
55    #[must_use]
56    pub fn into_parts(self) -> (ContentRevision, Option<ContentDigest>, u64) {
57        (self.revision, self.digest, self.size)
58    }
59}
60
61/// Stable item identity plus its current parent, name, kind, and revisions.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CloudItem {
64    key: CloudItemKey,
65    parent_id: Option<CloudItemId>,
66    name: String,
67    kind: CloudItemKind,
68    metadata_revision: MetadataRevision,
69    content: Option<CloudContentMetadata>,
70}
71
72impl CloudItem {
73    /// Creates a directory. `parent_id = None` identifies the root item for the scope.
74    /// # Errors
75    ///
76    /// Returns an error when validation fails or an underlying backend, store, or platform
77    /// operation fails.
78    pub fn directory(
79        key: CloudItemKey,
80        parent_id: Option<CloudItemId>,
81        name: impl Into<String>,
82        metadata_revision: MetadataRevision,
83    ) -> Result<Self> {
84        let name = name.into();
85        validate_item_location(&key, parent_id.as_ref(), &name)?;
86        Ok(Self {
87            key,
88            parent_id,
89            name,
90            kind: CloudItemKind::Directory,
91            metadata_revision,
92            content: None,
93        })
94    }
95
96    /// Creates a regular file. Files always require a parent within the same scope.
97    /// # Errors
98    ///
99    /// Returns an error when validation fails or an underlying backend, store, or platform
100    /// operation fails.
101    pub fn file(
102        key: CloudItemKey,
103        parent_id: CloudItemId,
104        name: impl Into<String>,
105        metadata_revision: MetadataRevision,
106        content: CloudContentMetadata,
107    ) -> Result<Self> {
108        let name = name.into();
109        validate_item_location(&key, Some(&parent_id), &name)?;
110        Ok(Self {
111            key,
112            parent_id: Some(parent_id),
113            name,
114            kind: CloudItemKind::File,
115            metadata_revision,
116            content: Some(content),
117        })
118    }
119
120    /// Returns the fully scoped stable identity.
121    #[must_use]
122    pub const fn key(&self) -> &CloudItemKey {
123        &self.key
124    }
125
126    /// Returns the parent item identity, or `None` for the root item.
127    #[must_use]
128    pub const fn parent_id(&self) -> Option<&CloudItemId> {
129        self.parent_id.as_ref()
130    }
131
132    /// Returns the current item name.
133    #[must_use]
134    pub fn name(&self) -> &str {
135        &self.name
136    }
137
138    /// Returns the current item kind.
139    #[must_use]
140    pub const fn kind(&self) -> CloudItemKind {
141        self.kind
142    }
143
144    /// Returns the opaque metadata revision.
145    #[must_use]
146    pub const fn metadata_revision(&self) -> &MetadataRevision {
147        &self.metadata_revision
148    }
149
150    /// Returns file content metadata, or `None` for a directory.
151    #[must_use]
152    pub const fn content(&self) -> Option<&CloudContentMetadata> {
153        self.content.as_ref()
154    }
155
156    /// Returns whether this item is the root directory of its scope.
157    #[must_use]
158    pub const fn is_root(&self) -> bool {
159        self.parent_id.is_none()
160    }
161
162    /// Applies a same-root rename or move while preserving the stable item key and content state.
163    /// # Errors
164    ///
165    /// Returns an error when validation fails or an underlying backend, store, or platform
166    /// operation fails.
167    pub fn moved(
168        mut self,
169        parent_id: CloudItemId,
170        name: impl Into<String>,
171        metadata_revision: MetadataRevision,
172    ) -> Result<Self> {
173        if self.is_root() {
174            return Err(CloudFilesCoreError::invalid_item(
175                "the root item must not be moved",
176            ));
177        }
178        let name = name.into();
179        if name.is_empty() {
180            return Err(CloudFilesCoreError::empty("cloud item name"));
181        }
182        if &parent_id == self.key.item_id() {
183            return Err(CloudFilesCoreError::invalid_item(
184                "an item must not be its own parent",
185            ));
186        }
187        self.parent_id = Some(parent_id);
188        self.name = name;
189        self.metadata_revision = metadata_revision;
190        Ok(self)
191    }
192}
193
194fn validate_item_location(
195    key: &CloudItemKey,
196    parent_id: Option<&CloudItemId>,
197    name: &str,
198) -> Result<()> {
199    if name.is_empty() {
200        return Err(CloudFilesCoreError::empty("cloud item name"));
201    }
202    if parent_id == Some(key.item_id()) {
203        return Err(CloudFilesCoreError::invalid_item(
204            "an item must not be its own parent",
205        ));
206    }
207    Ok(())
208}
209
210/// One page of directory items returned by a backend adapter.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct CloudItemPage {
213    items: Vec<CloudItem>,
214    next_cursor: Option<PageCursor>,
215}
216
217impl CloudItemPage {
218    /// Creates a directory page and its optional continuation cursor.
219    #[must_use]
220    pub const fn new(items: Vec<CloudItem>, next_cursor: Option<PageCursor>) -> Self {
221        Self { items, next_cursor }
222    }
223
224    /// Returns the items in backend-defined page order.
225    #[must_use]
226    pub fn items(&self) -> &[CloudItem] {
227        &self.items
228    }
229
230    /// Returns the next page cursor, or `None` when enumeration is complete.
231    #[must_use]
232    pub const fn next_cursor(&self) -> Option<&PageCursor> {
233        self.next_cursor.as_ref()
234    }
235
236    /// Consumes the page and returns its items and continuation cursor.
237    #[must_use]
238    pub fn into_parts(self) -> (Vec<CloudItem>, Option<PageCursor>) {
239        (self.items, self.next_cursor)
240    }
241}