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, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct StorageNamespace {
111 pub id: String,
112 pub name: String,
113 pub max_bytes: u64,
114 pub overflow_strategy: StorageOverflowStrategy,
115 pub used_bytes: u64,
116 pub reserved_bytes: u64,
117 pub object_count: u64,
118 pub evicted_object_count: u64,
119 pub evicted_bytes: u64,
120 pub rejected_write_count: u64,
121 pub revision: u64,
122 pub created_at_ms: u64,
123 pub updated_at_ms: u64,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct StorageNamespaceList {
129 pub namespaces: Vec<StorageNamespace>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct StorageSession {
135 pub protocol_version: String,
136 pub repository_id: String,
137 pub namespace: StorageNamespace,
138 pub endpoint: String,
139 pub access_token: String,
140 pub expires_at_ms: u64,
141 pub permissions: Vec<StoragePermission>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct StorageObject {
147 pub key: String,
148 pub object_id: String,
149 pub size_bytes: u64,
150 pub sha256: String,
151 pub content_type: Option<String>,
152 pub committed_at_ms: u64,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct StorageObjectList {
158 pub objects: Vec<StorageObject>,
159 pub next_cursor: Option<String>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub enum StorageWriteState {
165 Writable,
166 LowSpace,
167 BlockedByPhysicalSpace,
168 BlockedByStorageLimit,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct StorageRepositorySettings {
174 pub limit_bytes: Option<u64>,
175 pub revision: u64,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct StorageRepositoryStats {
181 pub repository_id: String,
182 pub filesystem_total_bytes: u64,
183 pub filesystem_available_bytes: u64,
184 pub storage_limit_bytes: Option<u64>,
185 pub storage_used_bytes: u64,
186 pub storage_reserved_bytes: u64,
187 pub storage_writable_bytes: u64,
188 pub warning_at_available_bytes: u64,
189 pub stop_at_available_bytes: u64,
190 pub write_state: StorageWriteState,
191 pub settings_revision: u64,
192 pub namespace_count: u64,
193 pub object_count: u64,
194 pub scope_namespace_count: u64,
195 pub scope_object_count: u64,
196 pub scope_used_bytes: u64,
197 pub scope_reserved_bytes: u64,
198 pub active_upload_count: u64,
199 pub pending_deletion_count: u64,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde_json::json;
206
207 #[test]
208 fn storage_request_contracts_are_closed_and_camel_case() {
209 let create: StorageCreateNamespaceRequest = serde_json::from_value(json!({
210 "name": "Camera Recordings",
211 "maxBytes": 1024,
212 "overflowStrategy": "evictOldest"
213 }))
214 .unwrap();
215 assert_eq!(
216 create.overflow_strategy,
217 StorageOverflowStrategy::EvictOldest
218 );
219 assert!(
220 serde_json::from_value::<StorageCreateNamespaceRequest>(json!({
221 "name": "recordings",
222 "maxBytes": 1024,
223 "futureField": true
224 }))
225 .is_err()
226 );
227
228 let update: StorageNamespaceUpdate = serde_json::from_value(json!({
229 "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
230 "expectedRevision": 2,
231 "name": "Recordings",
232 "maxBytes": 2048,
233 "overflowStrategy": "reject"
234 }))
235 .unwrap();
236 assert_eq!(update.expected_revision, 2);
237
238 let settings: StorageRepositorySettingsUpdate = serde_json::from_value(json!({
239 "expectedRevision": 3,
240 "limitBytes": null
241 }))
242 .unwrap();
243 assert_eq!(settings.expected_revision, 3);
244 assert_eq!(settings.limit_bytes, None);
245
246 let session: StorageSessionRequest = serde_json::from_value(json!({
247 "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
248 "permissions": ["read", "write", "delete", "list"],
249 "ttlSeconds": 900
250 }))
251 .unwrap();
252 assert_eq!(session.permissions.len(), 4);
253 }
254
255 #[test]
256 fn storage_responses_are_additive_for_consumers() {
257 let namespace: StorageNamespace = serde_json::from_value(json!({
258 "id": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
259 "name": "recordings",
260 "maxBytes": 1024,
261 "overflowStrategy": "reject",
262 "usedBytes": 0,
263 "reservedBytes": 0,
264 "objectCount": 0,
265 "evictedObjectCount": 0,
266 "evictedBytes": 0,
267 "rejectedWriteCount": 0,
268 "revision": 1,
269 "createdAtMs": 100,
270 "updatedAtMs": 100,
271 "futureField": true
272 }))
273 .unwrap();
274 assert_eq!(namespace.max_bytes, 1024);
275
276 let stats: StorageRepositoryStats = serde_json::from_value(json!({
277 "repositoryId": "repository-1",
278 "filesystemTotalBytes": 4096,
279 "filesystemAvailableBytes": 2048,
280 "storageLimitBytes": 3072,
281 "storageUsedBytes": 1024,
282 "storageReservedBytes": 0,
283 "storageWritableBytes": 1024,
284 "warningAtAvailableBytes": 512,
285 "stopAtAvailableBytes": 256,
286 "writeState": "writable",
287 "settingsRevision": 1,
288 "namespaceCount": 1,
289 "objectCount": 2,
290 "scopeNamespaceCount": 1,
291 "scopeObjectCount": 2,
292 "scopeUsedBytes": 1024,
293 "scopeReservedBytes": 0,
294 "activeUploadCount": 0,
295 "pendingDeletionCount": 1,
296 "futureField": true
297 }))
298 .unwrap();
299 assert_eq!(stats.pending_deletion_count, 1);
300 }
301
302 #[test]
303 fn storage_route_and_strategy_constants_are_canonical() {
304 assert_eq!(STORAGE_NODE_TYPE, "storage");
305 assert_eq!(STORAGE_PROTOCOL_VERSION, "storage.v2");
306 assert_eq!(STORAGE_CREATE_NAMESPACE_ROUTE, "/storage/namespace/create");
307 assert_eq!(STORAGE_UPDATE_NAMESPACE_ROUTE, "/storage/namespace/update");
308 assert_eq!(STORAGE_UPDATE_SETTINGS_ROUTE, "/storage/settings/update");
309 assert_eq!(StorageOverflowStrategy::Reject.as_str(), "reject");
310 assert_eq!(
311 StorageOverflowStrategy::parse("evictOldest").unwrap(),
312 StorageOverflowStrategy::EvictOldest
313 );
314 assert!(StorageOverflowStrategy::parse("deleteOldest").is_err());
315 }
316}