Skip to main content

core_api/
storage.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub const STORAGE_NODE_TYPE: &str = "storage";
5pub const STORAGE_PROTOCOL_VERSION: &str = "storage.v1";
6
7pub const STORAGE_DESCRIBE_ROUTE: &str = "/storage/describe";
8pub const STORAGE_ENSURE_NAMESPACE_ROUTE: &str = "/storage/namespace/ensure";
9pub const STORAGE_NAMESPACE_STATUS_ROUTE: &str = "/storage/namespace/status";
10pub const STORAGE_UPDATE_NAMESPACE_POLICY_ROUTE: &str = "/storage/namespace/update-policy";
11pub const STORAGE_LIST_NAMESPACES_ROUTE: &str = "/storage/namespaces/list";
12pub const STORAGE_ISSUE_SESSION_ROUTE: &str = "/storage/session/issue";
13pub const STORAGE_STATS_ROUTE: &str = "/storage/stats";
14pub const STORAGE_READINESS_ROUTE: &str = "/storage/readiness";
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub enum StorageOverflowStrategy {
19    #[default]
20    Reject,
21    EvictOldest,
22}
23
24impl StorageOverflowStrategy {
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Self::Reject => "reject",
28            Self::EvictOldest => "evictOldest",
29        }
30    }
31
32    pub fn parse(value: &str) -> Result<Self, UnsupportedStorageOverflowStrategy> {
33        match value {
34            "reject" => Ok(Self::Reject),
35            "evictOldest" => Ok(Self::EvictOldest),
36            _ => Err(UnsupportedStorageOverflowStrategy(value.to_string())),
37        }
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct UnsupportedStorageOverflowStrategy(String);
43
44impl fmt::Display for UnsupportedStorageOverflowStrategy {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(
47            formatter,
48            "unsupported storage overflow strategy: {}",
49            self.0
50        )
51    }
52}
53
54impl std::error::Error for UnsupportedStorageOverflowStrategy {}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub enum StoragePermission {
59    Read,
60    Write,
61    Delete,
62    List,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase", deny_unknown_fields)]
67pub struct StorageEnsureNamespaceRequest {
68    pub name: String,
69    pub created_by_node_type: String,
70    pub created_by_node_id: String,
71    pub max_bytes: u64,
72    #[serde(default)]
73    pub overflow_strategy: StorageOverflowStrategy,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct StorageNamespacePolicyUpdate {
79    pub namespace_id: String,
80    pub expected_revision: u64,
81    pub max_bytes: u64,
82    pub overflow_strategy: StorageOverflowStrategy,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct StorageNamespaceIdRequest {
88    pub namespace_id: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct StorageSessionRequest {
94    pub namespace_id: String,
95    #[serde(default)]
96    pub permissions: Vec<StoragePermission>,
97    #[serde(default)]
98    pub ttl_seconds: Option<u64>,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct StorageNamespacePolicy {
104    pub max_bytes: u64,
105    pub overflow_strategy: StorageOverflowStrategy,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct StorageNamespace {
111    pub id: String,
112    pub name: String,
113    pub created_by_node_type: String,
114    pub created_by_node_id: String,
115    pub max_bytes: u64,
116    pub overflow_strategy: StorageOverflowStrategy,
117    pub used_bytes: u64,
118    pub reserved_bytes: u64,
119    pub object_count: u64,
120    pub evicted_object_count: u64,
121    pub evicted_bytes: u64,
122    pub rejected_write_count: u64,
123    pub revision: u64,
124    pub created_at_ms: u64,
125    pub updated_at_ms: u64,
126}
127
128impl StorageNamespace {
129    pub const fn policy(&self) -> StorageNamespacePolicy {
130        StorageNamespacePolicy {
131            max_bytes: self.max_bytes,
132            overflow_strategy: self.overflow_strategy,
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct StorageNamespaceList {
140    pub namespaces: Vec<StorageNamespace>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct StorageSession {
146    pub protocol_version: String,
147    pub repository_id: String,
148    pub namespace: StorageNamespace,
149    pub endpoint: String,
150    pub access_token: String,
151    pub expires_at_ms: u64,
152    pub permissions: Vec<StoragePermission>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct StorageAvailability {
158    pub available: bool,
159    pub protocol_version: String,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct StorageObject {
165    pub key: String,
166    pub object_id: String,
167    pub size_bytes: u64,
168    pub sha256: String,
169    pub content_type: Option<String>,
170    pub committed_at_ms: u64,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct StorageObjectList {
176    pub objects: Vec<StorageObject>,
177    pub next_cursor: Option<String>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct StorageRepositoryStats {
183    pub repository_id: String,
184    pub total_bytes: u64,
185    pub available_bytes: u64,
186    pub namespace_count: u64,
187    pub object_count: u64,
188    pub used_bytes: u64,
189    pub reserved_bytes: u64,
190    pub active_upload_count: u64,
191    pub pending_deletion_count: u64,
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use serde_json::json;
198
199    #[test]
200    fn storage_request_contracts_are_closed_and_camel_case() {
201        let ensure: StorageEnsureNamespaceRequest = serde_json::from_value(json!({
202            "name": "recordings",
203            "createdByNodeType": "camera",
204            "createdByNodeId": "camera-1",
205            "maxBytes": 1024,
206            "overflowStrategy": "evictOldest"
207        }))
208        .unwrap();
209        assert_eq!(
210            ensure.overflow_strategy,
211            StorageOverflowStrategy::EvictOldest
212        );
213        assert!(
214            serde_json::from_value::<StorageEnsureNamespaceRequest>(json!({
215                "name": "recordings",
216                "createdByNodeType": "camera",
217                "createdByNodeId": "camera-1",
218                "maxBytes": 1024,
219                "futureField": true
220            }))
221            .is_err()
222        );
223
224        let update: StorageNamespacePolicyUpdate = serde_json::from_value(json!({
225            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
226            "expectedRevision": 2,
227            "maxBytes": 2048,
228            "overflowStrategy": "reject"
229        }))
230        .unwrap();
231        assert_eq!(update.expected_revision, 2);
232
233        let session: StorageSessionRequest = serde_json::from_value(json!({
234            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
235            "permissions": ["read", "write", "delete", "list"],
236            "ttlSeconds": 900
237        }))
238        .unwrap();
239        assert_eq!(session.permissions.len(), 4);
240    }
241
242    #[test]
243    fn storage_responses_are_additive_for_consumers() {
244        let namespace: StorageNamespace = serde_json::from_value(json!({
245            "id": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
246            "name": "recordings",
247            "createdByNodeType": "camera",
248            "createdByNodeId": "camera-1",
249            "maxBytes": 1024,
250            "overflowStrategy": "reject",
251            "usedBytes": 0,
252            "reservedBytes": 0,
253            "objectCount": 0,
254            "evictedObjectCount": 0,
255            "evictedBytes": 0,
256            "rejectedWriteCount": 0,
257            "revision": 1,
258            "createdAtMs": 100,
259            "updatedAtMs": 100,
260            "futureField": true
261        }))
262        .unwrap();
263        assert_eq!(namespace.policy().max_bytes, 1024);
264
265        let stats: StorageRepositoryStats = serde_json::from_value(json!({
266            "repositoryId": "repository-1",
267            "totalBytes": 4096,
268            "availableBytes": 2048,
269            "namespaceCount": 1,
270            "objectCount": 2,
271            "usedBytes": 1024,
272            "reservedBytes": 0,
273            "activeUploadCount": 0,
274            "pendingDeletionCount": 1,
275            "futureField": true
276        }))
277        .unwrap();
278        assert_eq!(stats.pending_deletion_count, 1);
279    }
280
281    #[test]
282    fn storage_route_and_strategy_constants_are_canonical() {
283        assert_eq!(STORAGE_NODE_TYPE, "storage");
284        assert_eq!(STORAGE_PROTOCOL_VERSION, "storage.v1");
285        assert_eq!(
286            STORAGE_UPDATE_NAMESPACE_POLICY_ROUTE,
287            "/storage/namespace/update-policy"
288        );
289        assert_eq!(StorageOverflowStrategy::Reject.as_str(), "reject");
290        assert_eq!(
291            StorageOverflowStrategy::parse("evictOldest").unwrap(),
292            StorageOverflowStrategy::EvictOldest
293        );
294        assert!(StorageOverflowStrategy::parse("deleteOldest").is_err());
295    }
296}