Skip to main content

systemprompt_cloud/tenants/
tenant_store.rs

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