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