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}