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