Skip to main content

systemprompt_cloud/tenants/
tenant_store.rs

1//! Persistent map of [`super::StoredTenant`] records.
2
3use std::fs;
4use std::path::Path;
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use validator::Validate;
9
10use super::StoredTenant;
11use crate::api_client::TenantInfo;
12use crate::error::{CloudError, CloudResult};
13
14#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
15pub struct TenantStore {
16    #[validate(nested)]
17    pub tenants: Vec<StoredTenant>,
18
19    pub synced_at: DateTime<Utc>,
20}
21
22impl TenantStore {
23    #[must_use]
24    pub fn new(tenants: Vec<StoredTenant>) -> Self {
25        Self {
26            tenants,
27            synced_at: Utc::now(),
28        }
29    }
30
31    #[must_use]
32    pub fn from_tenant_infos(infos: &[TenantInfo]) -> Self {
33        let tenants = infos.iter().map(StoredTenant::from_tenant_info).collect();
34        Self::new(tenants)
35    }
36
37    pub fn load_from_path(path: &Path) -> CloudResult<Self> {
38        if !path.exists() {
39            return Err(CloudError::TenantsNotSynced);
40        }
41
42        let content = fs::read_to_string(path)?;
43
44        let store: Self = serde_json::from_str(&content)
45            .map_err(|e| CloudError::TenantsStoreCorrupted { source: e })?;
46
47        store
48            .validate()
49            .map_err(|e| CloudError::TenantsStoreInvalid {
50                message: e.to_string(),
51            })?;
52
53        Ok(store)
54    }
55
56    pub fn save_to_path(&self, path: &Path) -> CloudResult<()> {
57        self.validate()
58            .map_err(|e| CloudError::TenantsStoreInvalid {
59                message: e.to_string(),
60            })?;
61
62        if let Some(dir) = path.parent() {
63            fs::create_dir_all(dir)?;
64
65            let gitignore_path = dir.join(".gitignore");
66            if !gitignore_path.exists() {
67                fs::write(&gitignore_path, "*\n")?;
68            }
69        }
70
71        let content = serde_json::to_string_pretty(self)?;
72        fs::write(path, content)?;
73
74        #[cfg(unix)]
75        {
76            use std::os::unix::fs::PermissionsExt;
77            let mut perms = fs::metadata(path)?.permissions();
78            perms.set_mode(0o600);
79            fs::set_permissions(path, perms)?;
80        }
81
82        Ok(())
83    }
84
85    #[must_use]
86    pub fn find_tenant(&self, id: &str) -> Option<&StoredTenant> {
87        self.tenants.iter().find(|t| t.id == id)
88    }
89
90    #[must_use]
91    pub fn is_empty(&self) -> bool {
92        self.tenants.is_empty()
93    }
94
95    #[must_use]
96    pub fn len(&self) -> usize {
97        self.tenants.len()
98    }
99
100    #[must_use]
101    pub fn is_stale(&self, max_age: chrono::Duration) -> bool {
102        let age = Utc::now() - self.synced_at;
103        age > max_age
104    }
105}
106
107impl Default for TenantStore {
108    fn default() -> Self {
109        Self {
110            tenants: Vec::new(),
111            synced_at: Utc::now(),
112        }
113    }
114}