mhome-core-api 1.10.0

Shared wire types and service contracts for the mHome core
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;

pub const STORAGE_NODE_TYPE: &str = "storage";
pub const STORAGE_PROTOCOL_VERSION: &str = "storage.v2";

pub const STORAGE_DESCRIBE_ROUTE: &str = "/storage/describe";
pub const STORAGE_CREATE_NAMESPACE_ROUTE: &str = "/storage/namespace/create";
pub const STORAGE_NAMESPACE_STATUS_ROUTE: &str = "/storage/namespace/status";
pub const STORAGE_UPDATE_NAMESPACE_ROUTE: &str = "/storage/namespace/update";
pub const STORAGE_LIST_NAMESPACES_ROUTE: &str = "/storage/namespaces/list";
pub const STORAGE_UPDATE_SETTINGS_ROUTE: &str = "/storage/settings/update";
pub const STORAGE_ISSUE_SESSION_ROUTE: &str = "/storage/session/issue";
pub const STORAGE_STATS_ROUTE: &str = "/storage/stats";
pub const STORAGE_READINESS_ROUTE: &str = "/storage/readiness";

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StorageOverflowStrategy {
    #[default]
    Reject,
    EvictOldest,
}

impl StorageOverflowStrategy {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Reject => "reject",
            Self::EvictOldest => "evictOldest",
        }
    }

    pub fn parse(value: &str) -> Result<Self, UnsupportedStorageOverflowStrategy> {
        match value {
            "reject" => Ok(Self::Reject),
            "evictOldest" => Ok(Self::EvictOldest),
            _ => Err(UnsupportedStorageOverflowStrategy(value.to_string())),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnsupportedStorageOverflowStrategy(String);

impl fmt::Display for UnsupportedStorageOverflowStrategy {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "unsupported storage overflow strategy: {}",
            self.0
        )
    }
}

impl std::error::Error for UnsupportedStorageOverflowStrategy {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StoragePermission {
    Read,
    Write,
    Delete,
    List,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageCreateNamespaceRequest {
    pub name: String,
    pub max_bytes: u64,
    #[serde(default)]
    pub overflow_strategy: StorageOverflowStrategy,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageNamespaceUpdate {
    pub namespace_id: String,
    pub expected_revision: u64,
    pub name: String,
    pub max_bytes: u64,
    pub overflow_strategy: StorageOverflowStrategy,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageRepositorySettingsUpdate {
    pub expected_revision: u64,
    pub limit_bytes: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageNamespaceIdRequest {
    pub namespace_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageSessionRequest {
    pub namespace_id: String,
    #[serde(default)]
    pub permissions: Vec<StoragePermission>,
    #[serde(default)]
    pub ttl_seconds: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageNamespace {
    pub id: String,
    pub name: String,
    pub max_bytes: u64,
    pub overflow_strategy: StorageOverflowStrategy,
    pub used_bytes: u64,
    pub reserved_bytes: u64,
    pub object_count: u64,
    pub evicted_object_count: u64,
    pub evicted_bytes: u64,
    pub rejected_write_count: u64,
    pub revision: u64,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageNamespaceList {
    pub namespaces: Vec<StorageNamespace>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageSession {
    pub protocol_version: String,
    pub repository_id: String,
    pub namespace: StorageNamespace,
    pub endpoint: String,
    pub access_token: String,
    pub expires_at_ms: u64,
    pub permissions: Vec<StoragePermission>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageObject {
    pub key: String,
    pub object_id: String,
    pub size_bytes: u64,
    pub sha256: String,
    pub content_type: Option<String>,
    pub committed_at_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageObjectList {
    pub objects: Vec<StorageObject>,
    pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StorageWriteState {
    Writable,
    LowSpace,
    BlockedByPhysicalSpace,
    BlockedByStorageLimit,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageRepositorySettings {
    pub limit_bytes: Option<u64>,
    pub revision: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageRepositoryStats {
    pub repository_id: String,
    pub filesystem_total_bytes: u64,
    pub filesystem_available_bytes: u64,
    pub storage_limit_bytes: Option<u64>,
    pub storage_used_bytes: u64,
    pub storage_reserved_bytes: u64,
    pub storage_writable_bytes: u64,
    pub warning_at_available_bytes: u64,
    pub stop_at_available_bytes: u64,
    pub write_state: StorageWriteState,
    pub settings_revision: u64,
    pub namespace_count: u64,
    pub object_count: u64,
    pub scope_namespace_count: u64,
    pub scope_object_count: u64,
    pub scope_used_bytes: u64,
    pub scope_reserved_bytes: u64,
    pub active_upload_count: u64,
    pub pending_deletion_count: u64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn storage_request_contracts_are_closed_and_camel_case() {
        let create: StorageCreateNamespaceRequest = serde_json::from_value(json!({
            "name": "Camera Recordings",
            "maxBytes": 1024,
            "overflowStrategy": "evictOldest"
        }))
        .unwrap();
        assert_eq!(
            create.overflow_strategy,
            StorageOverflowStrategy::EvictOldest
        );
        assert!(
            serde_json::from_value::<StorageCreateNamespaceRequest>(json!({
                "name": "recordings",
                "maxBytes": 1024,
                "futureField": true
            }))
            .is_err()
        );

        let update: StorageNamespaceUpdate = serde_json::from_value(json!({
            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
            "expectedRevision": 2,
            "name": "Recordings",
            "maxBytes": 2048,
            "overflowStrategy": "reject"
        }))
        .unwrap();
        assert_eq!(update.expected_revision, 2);

        let settings: StorageRepositorySettingsUpdate = serde_json::from_value(json!({
            "expectedRevision": 3,
            "limitBytes": null
        }))
        .unwrap();
        assert_eq!(settings.expected_revision, 3);
        assert_eq!(settings.limit_bytes, None);

        let session: StorageSessionRequest = serde_json::from_value(json!({
            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
            "permissions": ["read", "write", "delete", "list"],
            "ttlSeconds": 900
        }))
        .unwrap();
        assert_eq!(session.permissions.len(), 4);
    }

    #[test]
    fn storage_responses_are_additive_for_consumers() {
        let namespace: StorageNamespace = serde_json::from_value(json!({
            "id": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
            "name": "recordings",
            "maxBytes": 1024,
            "overflowStrategy": "reject",
            "usedBytes": 0,
            "reservedBytes": 0,
            "objectCount": 0,
            "evictedObjectCount": 0,
            "evictedBytes": 0,
            "rejectedWriteCount": 0,
            "revision": 1,
            "createdAtMs": 100,
            "updatedAtMs": 100,
            "futureField": true
        }))
        .unwrap();
        assert_eq!(namespace.max_bytes, 1024);

        let stats: StorageRepositoryStats = serde_json::from_value(json!({
            "repositoryId": "repository-1",
            "filesystemTotalBytes": 4096,
            "filesystemAvailableBytes": 2048,
            "storageLimitBytes": 3072,
            "storageUsedBytes": 1024,
            "storageReservedBytes": 0,
            "storageWritableBytes": 1024,
            "warningAtAvailableBytes": 512,
            "stopAtAvailableBytes": 256,
            "writeState": "writable",
            "settingsRevision": 1,
            "namespaceCount": 1,
            "objectCount": 2,
            "scopeNamespaceCount": 1,
            "scopeObjectCount": 2,
            "scopeUsedBytes": 1024,
            "scopeReservedBytes": 0,
            "activeUploadCount": 0,
            "pendingDeletionCount": 1,
            "futureField": true
        }))
        .unwrap();
        assert_eq!(stats.pending_deletion_count, 1);
    }

    #[test]
    fn storage_route_and_strategy_constants_are_canonical() {
        assert_eq!(STORAGE_NODE_TYPE, "storage");
        assert_eq!(STORAGE_PROTOCOL_VERSION, "storage.v2");
        assert_eq!(STORAGE_CREATE_NAMESPACE_ROUTE, "/storage/namespace/create");
        assert_eq!(STORAGE_UPDATE_NAMESPACE_ROUTE, "/storage/namespace/update");
        assert_eq!(STORAGE_UPDATE_SETTINGS_ROUTE, "/storage/settings/update");
        assert_eq!(StorageOverflowStrategy::Reject.as_str(), "reject");
        assert_eq!(
            StorageOverflowStrategy::parse("evictOldest").unwrap(),
            StorageOverflowStrategy::EvictOldest
        );
        assert!(StorageOverflowStrategy::parse("deleteOldest").is_err());
    }
}