1use anyhow::Result;
2use async_trait::async_trait;
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7#[async_trait]
9pub trait UtcpVariablesConfig: Send + Sync {
10 async fn load(&self) -> Result<HashMap<String, String>>;
12 async fn get(&self, key: &str) -> Result<String>;
14}
15
16#[derive(Clone)]
18pub struct UtcpClientConfig {
19 pub variables: HashMap<String, String>,
21 pub providers_file_path: Option<PathBuf>,
23 pub load_variables_from: Vec<Arc<dyn UtcpVariablesConfig>>,
25}
26
27impl Default for UtcpClientConfig {
28 fn default() -> Self {
29 Self {
30 variables: HashMap::new(),
31 providers_file_path: None,
32 load_variables_from: Vec::new(),
33 }
34 }
35}
36
37impl UtcpClientConfig {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn with_providers_file(mut self, path: PathBuf) -> Self {
45 self.providers_file_path = Some(path);
46 self
47 }
48
49 pub fn with_variable(mut self, key: String, value: String) -> Self {
51 self.variables.insert(key, value);
52 self
53 }
54
55 pub fn with_variables(mut self, vars: HashMap<String, String>) -> Self {
57 self.variables.extend(vars);
58 self
59 }
60
61 pub fn with_manual_path(mut self, path: PathBuf) -> Self {
63 self.providers_file_path = Some(path);
64 self
65 }
66
67 pub async fn get_variable(&self, key: &str) -> Option<String> {
69 if let Some(val) = self.variables.get(key) {
71 return Some(val.clone());
72 }
73
74 for loader in &self.load_variables_from {
76 if let Ok(val) = loader.get(key).await {
77 return Some(val);
78 }
79 }
80
81 std::env::var(key).ok()
83 }
84}
85
86pub struct DotEnvLoader {
88 file_path: PathBuf,
89}
90
91impl DotEnvLoader {
92 pub fn new(file_path: PathBuf) -> Self {
94 Self { file_path }
95 }
96}
97
98#[async_trait]
99impl UtcpVariablesConfig for DotEnvLoader {
100 async fn load(&self) -> Result<HashMap<String, String>> {
101 let contents = tokio::fs::read_to_string(&self.file_path).await?;
102 let mut vars = HashMap::new();
103
104 for line in contents.lines() {
105 let line = line.trim();
106 if line.is_empty() || line.starts_with('#') {
107 continue;
108 }
109
110 if let Some((key, value)) = line.split_once('=') {
111 vars.insert(
112 key.trim().to_string(),
113 value.trim().trim_matches('"').to_string(),
114 );
115 }
116 }
117
118 Ok(vars)
119 }
120
121 async fn get(&self, key: &str) -> Result<String> {
122 let vars = self.load().await?;
123 vars.get(key)
124 .cloned()
125 .ok_or_else(|| anyhow::anyhow!("Variable {} not found", key))
126 }
127}