use std::collections::BTreeMap;
use rskit_codec::value::{ArrayStrategy, merge_with};
use rskit_errors::{AppError, AppResult};
use rskit_util::collections::ensure_unique_by;
use serde_json::Value;
pub trait MergeIdentity: Send + Sync {
fn identity_key(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct IdentityKey(String);
impl IdentityKey {
pub fn new(field: impl Into<String>) -> Self {
Self(field.into())
}
}
impl MergeIdentity for IdentityKey {
fn identity_key(&self) -> &str {
&self.0
}
}
#[derive(Default)]
pub struct IncludeMerge {
identity_sections: BTreeMap<String, Box<dyn MergeIdentity>>,
}
impl std::fmt::Debug for IncludeMerge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IncludeMerge")
.field("identity_sections", &self.identity_sections.keys())
.finish()
}
}
impl IncludeMerge {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_identity(
mut self,
section: impl Into<String>,
identity: impl MergeIdentity + 'static,
) -> Self {
self.identity_sections
.insert(section.into(), Box::new(identity));
self
}
pub fn merge(&self, base: Value, overlay: Value) -> AppResult<Value> {
let identity_sections = &self.identity_sections;
Ok(merge_with(base, overlay, |key| {
if identity_sections.contains_key(key) {
ArrayStrategy::Concat
} else {
ArrayStrategy::Replace
}
}))
}
pub(crate) fn validate(&self, value: &Value) -> AppResult<()> {
match value {
Value::Object(table) => {
for (key, child) in table {
if let (Some(identity), Value::Array(elements)) =
(self.identity_sections.get(key), child)
{
check_unique_identities(key, identity.identity_key(), elements)?;
}
self.validate(child)?;
}
}
Value::Array(elements) => {
for element in elements {
self.validate(element)?;
}
}
_ => {}
}
Ok(())
}
}
fn check_unique_identities(section: &str, identity: &str, elements: &[Value]) -> AppResult<()> {
let identities = elements
.iter()
.filter_map(|element| element.get(identity).and_then(Value::as_str));
ensure_unique_by(identities, |id| *id).map_err(|duplicate| {
AppError::invalid_input(
section,
format!("duplicate '{identity}' value '{duplicate}' in section '{section}'"),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn overlay_scalar_wins_last() {
let merge = IncludeMerge::new();
let base = json!({ "name": "base", "retries": 1 });
let overlay = json!({ "retries": 5 });
let merged = merge.merge(base, overlay).unwrap();
assert_eq!(merged.get("name").unwrap().as_str(), Some("base"));
assert_eq!(merged.get("retries").unwrap().as_i64(), Some(5));
}
#[test]
fn tables_merge_recursively() {
let merge = IncludeMerge::new();
let base = json!({ "server": { "host": "a", "port": 1 } });
let overlay = json!({ "server": { "port": 2 } });
let merged = merge.merge(base, overlay).unwrap();
let server = merged.get("server").unwrap();
assert_eq!(server.get("host").unwrap().as_str(), Some("a"));
assert_eq!(server.get("port").unwrap().as_i64(), Some(2));
}
#[test]
fn identity_sections_concatenate() {
let merge = IncludeMerge::new().with_identity("groups", IdentityKey::new("name"));
let base = json!({ "groups": [{ "name": "a" }] });
let overlay = json!({ "groups": [{ "name": "b" }] });
let merged = merge.merge(base, overlay).unwrap();
merge.validate(&merged).unwrap();
let groups = merged.get("groups").unwrap().as_array().unwrap();
assert_eq!(groups.len(), 2);
}
#[test]
fn duplicate_identity_across_documents_is_rejected() {
let merge = IncludeMerge::new().with_identity("groups", IdentityKey::new("name"));
let base = json!({ "groups": [{ "name": "dup" }] });
let overlay = json!({ "groups": [{ "name": "dup" }] });
let merged = merge.merge(base, overlay).unwrap();
let err = merge.validate(&merged).unwrap_err();
assert!(err.to_string().contains("duplicate"));
assert!(err.to_string().contains("dup"));
}
#[test]
fn duplicate_identity_within_single_document_is_rejected() {
let merge = IncludeMerge::new().with_identity("groups", IdentityKey::new("name"));
let doc = json!({ "groups": [{ "name": "x" }, { "name": "x" }] });
let err = merge.validate(&doc).unwrap_err();
assert!(err.to_string().contains("duplicate"));
}
#[test]
fn duplicate_identity_nested_inside_array_is_rejected() {
let merge = IncludeMerge::new().with_identity("groups", IdentityKey::new("name"));
let doc = json!({
"tenants": [
{ "groups": [{ "name": "x" }, { "name": "x" }] }
]
});
let err = merge.validate(&doc).unwrap_err();
assert!(err.to_string().contains("duplicate"));
}
#[test]
fn non_identity_array_is_replaced() {
let merge = IncludeMerge::new();
let base = json!({ "ports": [1, 2, 3] });
let overlay = json!({ "ports": [9] });
let merged = merge.merge(base, overlay).unwrap();
assert_eq!(merged.get("ports").unwrap().as_array().unwrap().len(), 1);
}
}