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