Skip to main content

greentic_operator/demo/
setup.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fs,
4    path::Path,
5};
6
7use anyhow::{Context, anyhow};
8use serde_json::Value;
9
10use crate::domains::{self, Domain};
11
12/// Providers input describes the domain + provider configuration shipped in `--setup-input`.
13#[derive(Debug)]
14pub struct ProvidersInput {
15    domain_providers: BTreeMap<Domain, BTreeMap<String, Value>>,
16}
17
18impl ProvidersInput {
19    /// Load providers input from JSON or YAML.
20    pub fn load(path: &Path) -> anyhow::Result<Self> {
21        let raw = fs::read_to_string(path)?;
22        let value: Value = serde_json::from_str(&raw)
23            .or_else(|_| serde_yaml_bw::from_str(&raw))
24            .with_context(|| format!("parse providers input {}", path.display()))?;
25        let map = parse_providers_value(&value)?;
26        Ok(Self {
27            domain_providers: map,
28        })
29    }
30
31    /// Returns the configured providers for the selected domain.
32    pub fn providers_for_domain(&self, domain: Domain) -> Option<&BTreeMap<String, Value>> {
33        self.domain_providers.get(&domain)
34    }
35}
36
37fn parse_providers_value(
38    value: &Value,
39) -> anyhow::Result<BTreeMap<Domain, BTreeMap<String, Value>>> {
40    let map = match value.as_object() {
41        Some(map) => map,
42        None => {
43            return Err(anyhow!(
44                "providers input must be an object keyed by domain names"
45            ));
46        }
47    };
48    let mut result = BTreeMap::new();
49    for (domain_key, entry) in map {
50        let domain = match domain_from_str(domain_key) {
51            Some(domain) => domain,
52            None => {
53                return Err(anyhow!(
54                    "unknown domain '{domain_key}' in providers input (expected messaging|events|secrets|oauth)"
55                ));
56            }
57        };
58        let providers = match entry.as_object() {
59            Some(map) => map,
60            None => {
61                return Err(anyhow!(
62                    "providers for domain '{domain_key}' must be an object"
63                ));
64            }
65        };
66        let mut provider_map = BTreeMap::new();
67        for (name, value) in providers {
68            provider_map.insert(name.clone(), value.clone());
69        }
70        result.insert(domain, provider_map);
71    }
72    Ok(result)
73}
74
75fn domain_from_str(value: &str) -> Option<Domain> {
76    match value.to_lowercase().as_str() {
77        "messaging" => Some(Domain::Messaging),
78        "events" => Some(Domain::Events),
79        "secrets" => Some(Domain::Secrets),
80        "oauth" => Some(Domain::OAuth),
81        _ => None,
82    }
83}
84
85/// Discover tenants inside the bundle for the requested domain.
86pub fn discover_tenants(bundle: &Path, domain: Domain) -> anyhow::Result<Vec<String>> {
87    let domain_dir = bundle.join(domains::domain_name(domain)).join("tenants");
88    let general_dir = bundle.join("tenants");
89    if let Some(tenants) = read_tenants(&domain_dir)? {
90        return Ok(tenants);
91    }
92    if let Some(tenants) = read_tenants(&general_dir)? {
93        return Ok(tenants);
94    }
95    Ok(Vec::new())
96}
97
98fn read_tenants(dir: &Path) -> anyhow::Result<Option<Vec<String>>> {
99    if !dir.exists() {
100        return Ok(None);
101    }
102    let mut tenants = BTreeSet::new();
103    for entry in fs::read_dir(dir)? {
104        let entry = entry?;
105        let path = entry.path();
106        if path.is_dir() {
107            if let Some(name) = path.file_name().and_then(|value| value.to_str()) {
108                tenants.insert(name.to_string());
109            }
110            continue;
111        }
112        if path.is_file()
113            && let Some(stem) = path.file_stem().and_then(|value| value.to_str())
114        {
115            tenants.insert(stem.to_string());
116        }
117    }
118    let tenants = tenants.into_iter().collect();
119    Ok(Some(tenants))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use serde_json::json;
126    use tempfile::TempDir;
127
128    #[test]
129    fn parse_providers_input() -> anyhow::Result<()> {
130        let yaml = r#"
131messaging:
132  messaging-telegram:
133    config: value
134"#;
135        let dir = TempDir::new()?;
136        let path = dir.path().join("providers.json");
137        std::fs::write(&path, yaml)?;
138        let input = ProvidersInput::load(&path)?;
139        let providers = input
140            .providers_for_domain(Domain::Messaging)
141            .expect("expected messaging providers");
142        assert_eq!(
143            providers.get("messaging-telegram"),
144            Some(&json!({"config":"value"}))
145        );
146        Ok(())
147    }
148
149    #[test]
150    fn discover_tenants_reads_dirs_and_files() -> anyhow::Result<()> {
151        let bundle = TempDir::new()?;
152        let domain_dir = bundle.path().join("messaging").join("tenants");
153        fs::create_dir_all(&domain_dir)?;
154        fs::create_dir_all(domain_dir.join("alpha"))?;
155        std::fs::write(domain_dir.join("beta.json"), "{}")?;
156        let tenants = discover_tenants(bundle.path(), Domain::Messaging)?;
157        assert!(tenants.contains(&"alpha".to_string()));
158        assert!(tenants.contains(&"beta".to_string()));
159        Ok(())
160    }
161
162    #[test]
163    fn discover_tenants_falls_back_to_general_dir() -> anyhow::Result<()> {
164        let bundle = TempDir::new()?;
165        let tenants_dir = bundle.path().join("tenants");
166        fs::create_dir_all(tenants_dir.join("gamma"))?;
167        let tenants = discover_tenants(bundle.path(), Domain::Events)?;
168        assert_eq!(tenants, vec!["gamma".to_string()]);
169        Ok(())
170    }
171}