use r2x_manifest::runtime::RuntimeBindings;
use r2x_manifest::types::PluginType;
use crate::commands::run::pipeline::constants::{
DEFAULT_OUTPUT_ROOT, FOLDER_FIELD_KEYS, PATH_FALLBACK_KEYS, STORE_FIELD_KEYS,
};
use crate::commands::run::RunError;
pub(super) fn build_plugin_config(
bindings: &RuntimeBindings,
package_name: &str,
yaml_config_json: &str,
output_folder: Option<&str>,
inherited_store_path: Option<&str>,
stdin_overrides: Option<&str>,
) -> Result<String, RunError> {
let mut yaml_config: serde_json::Value = serde_json::from_str(yaml_config_json)
.map_err(|e| RunError::Config(format!("Failed to parse YAML config: {}", e)))?;
if let Some(overrides) = stdin_overrides {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(overrides) {
merge_config_values(&mut yaml_config, value);
}
}
let mut final_config = serde_json::Map::new();
let mut store_value_for_folder: Option<serde_json::Value> = None;
if bindings.plugin_type == PluginType::Class {
let mut config_class_params = serde_json::Map::new();
let mut constructor_params = serde_json::Map::new();
if let serde_json::Value::Object(ref yaml_map) = yaml_config {
for (key, value) in yaml_map {
if bindings.parameters.iter().any(|p| p.name.as_ref() == key) {
constructor_params.insert(key.clone(), value.clone());
} else {
config_class_params.insert(key.clone(), value.clone());
}
}
}
final_config.extend(config_class_params);
final_config.extend(constructor_params);
if bindings
.parameters
.iter()
.any(|p| p.name.as_ref() == "path")
&& !final_config.contains_key("path")
&& matches!(yaml_config, serde_json::Value::Object(_))
{
if let serde_json::Value::Object(ref yaml_map) = yaml_config {
if let Some(path_value) = pick_value(yaml_map, PATH_FALLBACK_KEYS) {
final_config.insert("path".to_string(), path_value);
}
}
}
let needs_store = bindings.requires_store
|| bindings
.parameters
.iter()
.any(|p| p.name.as_ref() == "store");
if needs_store {
let store_value = if let serde_json::Value::Object(ref yaml_map) = yaml_config {
pick_value(yaml_map, STORE_FIELD_KEYS)
.or_else(|| {
inherited_store_path.map(|p| serde_json::Value::String(p.to_string()))
})
.map_or_else(|| fallback_store_value(package_name, output_folder), Ok)?
} else if let Some(inherited) = inherited_store_path {
serde_json::Value::String(inherited.to_string())
} else {
fallback_store_value(package_name, output_folder)?
};
store_value_for_folder = Some(store_value.clone());
final_config.insert("store".to_string(), store_value);
}
if bindings
.parameters
.iter()
.any(|p| p.name.as_ref() == "folder_path")
&& !final_config.contains_key("folder_path")
{
let explicit_folder = if let serde_json::Value::Object(ref yaml_map) = yaml_config {
pick_value(yaml_map, FOLDER_FIELD_KEYS)
} else {
None
};
let folder_value = explicit_folder
.or_else(|| store_value_for_folder.as_ref().and_then(value_string_clone))
.or_else(|| {
inherited_store_path.map(|path| serde_json::Value::String(path.to_string()))
});
if let Some(value) = folder_value {
final_config.insert("folder_path".to_string(), value);
}
}
} else if let serde_json::Value::Object(ref yaml_map) = yaml_config {
final_config.extend(yaml_map.clone());
}
serde_json::to_string(&serde_json::Value::Object(final_config))
.map_err(|e| RunError::Config(format!("Failed to serialize final config: {}", e)))
}
fn pick_value(
map: &serde_json::Map<String, serde_json::Value>,
keys: &[&str],
) -> Option<serde_json::Value> {
keys.iter().find_map(|key| map.get(*key).cloned())
}
fn value_string_clone(value: &serde_json::Value) -> Option<serde_json::Value> {
match value {
serde_json::Value::String(s) => Some(serde_json::Value::String(s.clone())),
_ => None,
}
}
fn merge_config_values(target: &mut serde_json::Value, overrides: serde_json::Value) {
match (target, overrides) {
(serde_json::Value::Object(target_map), serde_json::Value::Object(override_map)) => {
for (key, value) in override_map {
match target_map.get_mut(&key) {
Some(existing) => merge_config_values(existing, value),
None => {
target_map.insert(key, value);
}
}
}
}
(target_value, override_value) => {
*target_value = override_value;
}
}
}
fn fallback_store_value(
_package_name: &str,
output_folder: Option<&str>,
) -> Result<serde_json::Value, RunError> {
let output_folder = output_folder.unwrap_or(DEFAULT_OUTPUT_ROOT);
let store_path = format!("{}/store", output_folder);
std::fs::create_dir_all(&store_path)
.map_err(|e| RunError::Config(format!("Failed to create store directory: {}", e)))?;
Ok(serde_json::Value::String(store_path))
}
#[cfg(test)]
mod tests {
use crate::commands::run::pipeline::builder::merge_config_values;
use serde_json::json;
#[test]
fn merge_config_values_replaces_existing() {
let mut target = json!({
"system_base_power": 100,
"system_name": "TestSystem"
});
let overrides = json!({
"system_base_power": 200,
"new_field": "value"
});
merge_config_values(&mut target, overrides);
assert_eq!(target["system_base_power"], json!(200));
assert_eq!(target["system_name"], json!("TestSystem"));
assert_eq!(target["new_field"], json!("value"));
}
#[test]
fn merge_config_values_adds_new_fields() {
let mut target = json!({
"system_name": "TestSystem"
});
let overrides = json!({
"optional_field": "new_value",
"another": 42
});
merge_config_values(&mut target, overrides);
assert_eq!(target["system_name"], json!("TestSystem"));
assert_eq!(target["optional_field"], json!("new_value"));
assert_eq!(target["another"], json!(42));
}
#[test]
fn merge_config_values_nested_merge() {
let mut target = json!({
"config": {
"base_power": 100,
"name": "Test"
}
});
let overrides = json!({
"config": {
"base_power": 200,
"extra": "new"
}
});
merge_config_values(&mut target, overrides);
assert_eq!(target["config"]["base_power"], json!(200));
assert_eq!(target["config"]["name"], json!("Test"));
assert_eq!(target["config"]["extra"], json!("new"));
}
}