Skip to main content

systemprompt_cloud/
context.rs

1use systemprompt_models::Profile;
2use systemprompt_models::profile_bootstrap::ProfileBootstrap;
3
4use crate::api_client::CloudApiClient;
5use crate::credentials::CloudCredentials;
6use crate::error::{CloudError, CloudResult};
7use crate::paths::{CloudPath, get_cloud_paths};
8use crate::tenants::{StoredTenant, TenantStore};
9
10#[derive(Debug, Clone)]
11pub struct ResolvedTenant {
12    // JSON: external vendor identifier
13    pub id: String,
14    pub name: String,
15    pub app_id: Option<String>,
16    pub hostname: Option<String>,
17    pub region: Option<String>,
18}
19
20impl From<StoredTenant> for ResolvedTenant {
21    fn from(tenant: StoredTenant) -> Self {
22        Self {
23            id: tenant.id,
24            name: tenant.name,
25            app_id: tenant.app_id,
26            hostname: tenant.hostname,
27            region: tenant.region,
28        }
29    }
30}
31
32#[derive(Debug)]
33pub struct CloudContext {
34    pub credentials: CloudCredentials,
35    pub profile: Option<&'static Profile>,
36    pub tenant: Option<ResolvedTenant>,
37    pub api_client: CloudApiClient,
38}
39
40impl CloudContext {
41    pub fn new_authenticated() -> CloudResult<Self> {
42        let cloud_paths = get_cloud_paths();
43        let creds_path = cloud_paths.resolve(CloudPath::Credentials);
44        let credentials = CloudCredentials::load_and_validate_from_path(&creds_path)
45            .map_err(|_| CloudError::NotAuthenticated)?;
46
47        let api_client = CloudApiClient::new(&credentials.api_url, &credentials.api_token)
48            .map_err(CloudError::Network)?;
49
50        Ok(Self {
51            credentials,
52            profile: None,
53            tenant: None,
54            api_client,
55        })
56    }
57
58    pub fn with_profile(mut self) -> CloudResult<Self> {
59        let profile = ProfileBootstrap::get().map_err(|e| CloudError::ProfileRequired {
60            message: e.to_string(),
61        })?;
62
63        self.profile = Some(profile);
64
65        if let Some(ref cloud_config) = profile.cloud {
66            if let Some(ref tenant_id) = cloud_config.tenant_id {
67                self.tenant = Self::resolve_tenant(tenant_id)?;
68            }
69        }
70
71        Ok(self)
72    }
73
74    fn resolve_tenant(tenant_id: &str) -> CloudResult<Option<ResolvedTenant>> {
75        let cloud_paths = get_cloud_paths();
76        let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
77
78        if !tenants_path.exists() {
79            return Ok(None);
80        }
81
82        let store =
83            TenantStore::load_from_path(&tenants_path).map_err(|_| CloudError::TenantsNotSynced)?;
84
85        store.find_tenant(tenant_id).map_or_else(
86            || {
87                Err(CloudError::TenantNotFound {
88                    tenant_id: tenant_id.to_string(),
89                })
90            },
91            |tenant| Ok(Some(ResolvedTenant::from(tenant.clone()))),
92        )
93    }
94
95    pub fn tenant_id(&self) -> CloudResult<&str> {
96        self.tenant
97            .as_ref()
98            .map(|t| t.id.as_str())
99            .ok_or(CloudError::TenantNotConfigured)
100    }
101
102    pub fn app_id(&self) -> CloudResult<&str> {
103        self.tenant
104            .as_ref()
105            .and_then(|t| t.app_id.as_deref())
106            .ok_or(CloudError::AppNotConfigured)
107    }
108
109    #[must_use]
110    pub fn tenant_name(&self) -> &str {
111        self.tenant.as_ref().map_or("unknown", |t| t.name.as_str())
112    }
113
114    #[must_use]
115    pub fn hostname(&self) -> Option<&str> {
116        self.tenant.as_ref().and_then(|t| t.hostname.as_deref())
117    }
118
119    pub fn profile(&self) -> CloudResult<&'static Profile> {
120        self.profile.ok_or_else(|| CloudError::ProfileRequired {
121            message: "Profile not loaded in context".into(),
122        })
123    }
124
125    #[must_use]
126    pub const fn has_tenant(&self) -> bool {
127        self.tenant.is_some()
128    }
129}