mecha10_behavior_runtime/config/
loader.rs1use anyhow::{Context as _, Result};
8use serde_json::Value;
9use std::path::{Path, PathBuf};
10
11pub async fn load_behavior_config(behavior_name: &str, project_root: &Path, environment: &str) -> Result<Value> {
35 let env_config_path = project_root
37 .join("configs")
38 .join(environment)
39 .join("behaviors")
40 .join(format!("{}.json", behavior_name));
41
42 let template_path = project_root.join("behaviors").join(format!("{}.json", behavior_name));
43
44 let base_config = load_json_file(&template_path).await.with_context(|| {
46 format!(
47 "Failed to load behavior template '{}' from {}",
48 behavior_name,
49 template_path.display()
50 )
51 })?;
52
53 let final_config = if env_config_path.exists() {
55 let env_config = load_json_file(&env_config_path).await?;
56 merge_configs(&base_config, &env_config)
57 } else {
58 base_config
59 };
60
61 tracing::debug!(
62 "Loaded behavior config '{}' for environment '{}' (template: {}, env: {})",
63 behavior_name,
64 environment,
65 template_path.exists(),
66 env_config_path.exists()
67 );
68
69 Ok(final_config)
70}
71
72async fn load_json_file(path: &Path) -> Result<Value> {
74 let content = tokio::fs::read_to_string(path)
75 .await
76 .with_context(|| format!("Failed to read file: {}", path.display()))?;
77
78 serde_json::from_str(&content).with_context(|| format!("Failed to parse JSON from: {}", path.display()))
79}
80
81fn merge_configs(base: &Value, override_val: &Value) -> Value {
88 match (base, override_val) {
89 (Value::Object(base_map), Value::Object(override_map)) => {
90 let mut result = base_map.clone();
91 for (key, value) in override_map {
92 result.insert(
93 key.clone(),
94 if let Some(base_value) = base_map.get(key) {
95 merge_configs(base_value, value)
96 } else {
97 value.clone()
98 },
99 );
100 }
101 Value::Object(result)
102 }
103 _ => override_val.clone(),
104 }
105}
106
107pub fn get_current_environment() -> String {
111 std::env::var("MECHA10_ENVIRONMENT").unwrap_or_else(|_| "dev".to_string())
112}
113
114pub fn detect_project_root() -> Result<PathBuf> {
119 let mut current = std::env::current_dir()?;
120
121 loop {
122 let mecha10_json = current.join("mecha10.json");
123 if mecha10_json.exists() {
124 return Ok(current);
125 }
126
127 if !current.pop() {
128 anyhow::bail!("Could not find project root (no mecha10.json found in parent directories)");
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use serde_json::json;
137
138 #[test]
139 fn test_merge_configs_objects() {
140 let base = json!({
141 "name": "test",
142 "config": {
143 "speed": 1.0,
144 "angle": 0.5
145 }
146 });
147
148 let override_val = json!({
149 "config": {
150 "speed": 2.0
151 }
152 });
153
154 let result = merge_configs(&base, &override_val);
155
156 assert_eq!(
157 result,
158 json!({
159 "name": "test",
160 "config": {
161 "speed": 2.0,
162 "angle": 0.5
163 }
164 })
165 );
166 }
167
168 #[test]
169 fn test_merge_configs_arrays_replaced() {
170 let base = json!({
171 "items": [1, 2, 3]
172 });
173
174 let override_val = json!({
175 "items": [4, 5]
176 });
177
178 let result = merge_configs(&base, &override_val);
179
180 assert_eq!(
181 result,
182 json!({
183 "items": [4, 5]
184 })
185 );
186 }
187
188 #[test]
189 fn test_get_current_environment_default() {
190 std::env::remove_var("MECHA10_ENVIRONMENT");
191 assert_eq!(get_current_environment(), "dev");
192 }
193}