systemprompt_cloud/tenants/
tenant_store.rs1use 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}