use serde_json::Value;
use std::fmt;
const NAME_KEY: &str = "name";
const REPLACE_KEY: &str = "$replace";
const CONFIGURATIONS_KEY: &str = "configurations";
const DEFAULT_CONFIGURATION_KEY: &str = "default_configuration";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigResolveError {
NoSelection,
UnknownConfiguration { name: String, available: Vec<String> },
}
impl fmt::Display for ConfigResolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigResolveError::NoSelection => write!(
f,
"configurations are defined but no active configuration was selected \
and no default_configuration is set"
),
ConfigResolveError::UnknownConfiguration { name, available } => write!(
f,
"unknown configuration '{}' (available: {})",
name,
available.join(", ")
),
}
}
}
impl std::error::Error for ConfigResolveError {}
pub fn apply_configuration_overlay(base: &mut Value, overlay: &Value) {
if has_replace_sentinel(overlay) {
*base = cloned_without_sentinel(overlay);
return;
}
match (base, overlay) {
(Value::Object(b), Value::Object(o)) => {
for (k, ov) in o {
match b.get_mut(k) {
Some(bv) => apply_configuration_overlay(bv, ov),
None => {
b.insert(k.clone(), cloned_without_sentinel(ov));
}
}
}
}
(Value::Array(b), Value::Array(o)) if is_named_array(b) && is_named_array(o) => {
merge_named_array(b, o);
}
(b, o) => {
*b = o.clone();
}
}
}
pub fn resolve_active_config(
config: &Value,
active: Option<&str>,
) -> Result<Value, ConfigResolveError> {
let configs = match config.get(CONFIGURATIONS_KEY) {
Some(Value::Object(m)) if !m.is_empty() => m,
_ => return Ok(strip_config_keys(config)),
};
let default = config.get(DEFAULT_CONFIGURATION_KEY).and_then(Value::as_str);
let name = active
.or(default)
.ok_or(ConfigResolveError::NoSelection)?;
let overlay = configs.get(name).ok_or_else(|| {
let mut available: Vec<String> = configs.keys().cloned().collect();
available.sort();
ConfigResolveError::UnknownConfiguration {
name: name.to_string(),
available,
}
})?;
let mut resolved = strip_config_keys(config);
apply_configuration_overlay(&mut resolved, overlay);
Ok(strip_config_keys(&resolved))
}
fn has_replace_sentinel(v: &Value) -> bool {
v.get(REPLACE_KEY) == Some(&Value::Bool(true))
}
fn cloned_without_sentinel(v: &Value) -> Value {
let mut v = v.clone();
if let Value::Object(m) = &mut v {
m.remove(REPLACE_KEY);
}
v
}
fn is_named_array(arr: &[Value]) -> bool {
!arr.is_empty()
&& arr
.iter()
.all(|e| e.get(NAME_KEY).and_then(Value::as_str).is_some())
}
fn merge_named_array(base: &mut Vec<Value>, overlay: &[Value]) {
for ov in overlay {
let name = ov.get(NAME_KEY).and_then(Value::as_str).unwrap();
match base
.iter_mut()
.find(|e| e.get(NAME_KEY).and_then(Value::as_str) == Some(name))
{
Some(existing) => apply_configuration_overlay(existing, ov),
None => base.push(cloned_without_sentinel(ov)),
}
}
}
fn strip_config_keys(config: &Value) -> Value {
let mut c = config.clone();
if let Value::Object(m) = &mut c {
m.remove(CONFIGURATIONS_KEY);
m.remove(DEFAULT_CONFIGURATION_KEY);
}
c
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn deep_merge_objects() {
let mut base = json!({ "axes": { "scaling": 1.0, "fcml": { "enabled": true, "kp": 2.0 } } });
let overlay = json!({ "axes": { "fcml": { "enabled": false } } });
apply_configuration_overlay(&mut base, &overlay);
assert_eq!(
base,
json!({ "axes": { "scaling": 1.0, "fcml": { "enabled": false, "kp": 2.0 } } })
);
}
#[test]
fn merge_named_array_patches_matching_element() {
let mut base = json!({
"slaves": [
{ "name": "main_drive", "position": 0, "watchdog": 100 },
{ "name": "load_encoder", "position": 1, "enabled": true }
]
});
let overlay = json!({ "slaves": [ { "name": "load_encoder", "enabled": false } ] });
apply_configuration_overlay(&mut base, &overlay);
assert_eq!(
base,
json!({
"slaves": [
{ "name": "main_drive", "position": 0, "watchdog": 100 },
{ "name": "load_encoder", "position": 1, "enabled": false }
]
})
);
}
#[test]
fn replace_sentinel_swaps_whole_element() {
let mut base = json!({
"slaves": [
{ "name": "main_drive", "device_id": { "vendor": "teknic" }, "startup_sdo": [1, 2, 3] }
]
});
let overlay = json!({
"slaves": [
{ "name": "main_drive", "$replace": true,
"device_id": { "vendor": "akd" }, "startup_sdo": [9] }
]
});
apply_configuration_overlay(&mut base, &overlay);
assert_eq!(
base,
json!({
"slaves": [
{ "name": "main_drive", "device_id": { "vendor": "akd" }, "startup_sdo": [9] }
]
})
);
}
#[test]
fn named_array_appends_unknown_element() {
let mut base = json!({ "slaves": [ { "name": "a", "x": 1 } ] });
let overlay = json!({ "slaves": [ { "name": "b", "y": 2, "$replace": true } ] });
apply_configuration_overlay(&mut base, &overlay);
assert_eq!(
base,
json!({ "slaves": [ { "name": "a", "x": 1 }, { "name": "b", "y": 2 } ] })
);
}
#[test]
fn resolve_passthrough_when_no_configurations() {
let config = json!({ "interface": "eth0", "slaves": [] });
assert_eq!(
resolve_active_config(&config, Some("anything")).unwrap(),
config
);
}
#[test]
fn resolve_strips_selection_keys() {
let config = json!({
"interface": "eth0",
"default_configuration": "a",
"configurations": { "a": { "interface": "eth1" } }
});
let resolved = resolve_active_config(&config, None).unwrap();
assert_eq!(resolved, json!({ "interface": "eth1" }));
}
#[test]
fn resolve_override_beats_default() {
let config = json!({
"v": "base",
"default_configuration": "a",
"configurations": { "a": { "v": "A" }, "b": { "v": "B" } }
});
assert_eq!(
resolve_active_config(&config, Some("b")).unwrap(),
json!({ "v": "B" })
);
}
#[test]
fn resolve_unknown_name_errors() {
let config = json!({ "configurations": { "a": {} } });
let err = resolve_active_config(&config, Some("zzz")).unwrap_err();
match err {
ConfigResolveError::UnknownConfiguration { name, available } => {
assert_eq!(name, "zzz");
assert_eq!(available, vec!["a".to_string()]);
}
_ => panic!("expected UnknownConfiguration, got {err:?}"),
}
}
#[test]
fn resolve_no_selection_errors() {
let config = json!({ "configurations": { "a": {} } });
assert_eq!(
resolve_active_config(&config, None).unwrap_err(),
ConfigResolveError::NoSelection
);
}
}