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