greentic_operator/demo/
setup.rs1use 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#[derive(Debug)]
14pub struct ProvidersInput {
15 domain_providers: BTreeMap<Domain, BTreeMap<String, Value>>,
16}
17
18impl ProvidersInput {
19 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 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
85pub 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}