use super::{EntityDedupMergePolicy, HashSet, RuntimeError, RuntimeResult, Value};
pub(super) fn reject_pack_managed_schedule_mutation(
note: &khive_storage::note::Note,
operation: &str,
) -> RuntimeResult<()> {
if note.kind == "scheduled_event" {
return Err(RuntimeError::InvalidInput(format!(
"cannot {operation} a schedule-managed `scheduled_event` note through generic KG \
mutation; use schedule.cancel or create a replacement schedule"
)));
}
Ok(())
}
pub(crate) fn merge_string_field(
into: &str,
from: &str,
strategy: EntityDedupMergePolicy,
) -> String {
match strategy {
EntityDedupMergePolicy::PreferInto | EntityDedupMergePolicy::Union => into.to_string(),
EntityDedupMergePolicy::PreferFrom => from.to_string(),
}
}
pub(crate) const OWNER_ESTABLISHED_PROPERTIES: &[&str] = &[
"from_actor",
"to_actor",
"direction",
"sent_at",
"outbound_ref",
"thread_id",
"subject",
"wire_message_id",
"external_id",
];
const KIND_OWNED_PROPERTIES: &[(&str, &[&str])] = &[
(
"message",
&[
"quarantined",
"channel_kind",
"channel_slug",
"delivery_hold",
"delivery_hold_reason",
"delivery_hold_at",
"external_id_diagnostic_note_id",
],
),
("channel_health", &["channel_kind", "channel_slug"]),
];
pub(crate) fn kind_owned_properties(kind: &str) -> &'static [&'static str] {
KIND_OWNED_PROPERTIES
.iter()
.find_map(|(owned_kind, keys)| (*owned_kind == kind).then_some(*keys))
.unwrap_or(&[])
}
pub(super) fn message_is_quarantined(note: &khive_storage::note::Note) -> bool {
let Some(Value::Object(map)) = note.properties.as_ref() else {
return false;
};
match map.get("quarantined") {
None => false,
Some(Value::Bool(value)) => *value,
Some(Value::String(value)) => value != "false",
Some(_) => true,
}
}
pub(crate) fn owner_established_property_named_in(patch: &Value) -> Option<&'static str> {
let Value::Object(map) = patch else {
return None;
};
OWNER_ESTABLISHED_PROPERTIES
.iter()
.copied()
.find(|key| map.contains_key(*key))
}
pub(crate) fn preserve_owner_established_properties(
into: &Option<Value>,
merged: &mut Option<Value>,
) {
preserve_property_keys(OWNER_ESTABLISHED_PROPERTIES, into, merged);
}
pub(super) fn preserve_property_keys(
keys: &[&str],
into: &Option<Value>,
merged: &mut Option<Value>,
) {
if !matches!(merged, Some(Value::Object(_))) {
let Some(Value::Object(into_map)) = into else {
return;
};
let owned_on_into = keys.iter().any(|key| into_map.contains_key(*key));
if owned_on_into {
*merged = into.clone();
}
return;
}
let Some(Value::Object(merged_map)) = merged.as_mut() else {
return;
};
let into_map = match into {
Some(Value::Object(m)) => Some(m),
_ => None,
};
for key in keys {
match into_map.and_then(|m| m.get(*key)) {
Some(value) => {
merged_map.insert((*key).to_string(), value.clone());
}
None => {
merged_map.remove(*key);
}
}
}
}
pub(crate) fn count_new_property_keys(
original: Option<&Value>,
final_value: Option<&Value>,
strategy: EntityDedupMergePolicy,
) -> usize {
match (original, final_value) {
(_, None) => 0,
(None, Some(Value::Object(map))) => map.len(),
(None, Some(_)) => 1,
(Some(Value::Object(orig_map)), Some(Value::Object(final_map))) => {
count_new_keys_within_object(orig_map, final_map, strategy)
}
(Some(_), Some(Value::Object(final_map))) => usize::from(!final_map.is_empty()),
(Some(orig), Some(final_val)) => usize::from(orig != final_val),
}
}
fn count_new_keys_within_object(
orig_map: &serde_json::Map<String, Value>,
final_map: &serde_json::Map<String, Value>,
strategy: EntityDedupMergePolicy,
) -> usize {
final_map
.iter()
.map(|(key, value)| match orig_map.get(key) {
None => 1,
Some(Value::Object(nested_orig))
if matches!(strategy, EntityDedupMergePolicy::Union) =>
{
match value {
Value::Object(nested_final) => {
count_new_keys_within_object(nested_orig, nested_final, strategy)
}
_ => 0,
}
}
Some(_) => 0,
})
.sum()
}
pub(crate) fn merge_properties(
into: &Option<Value>,
from: &Option<Value>,
strategy: EntityDedupMergePolicy,
) -> (Option<Value>, usize) {
match (into, from) {
(None, None) => (None, 0),
(Some(a), None) => (Some(a.clone()), 0),
(None, Some(b)) => {
let count = if let Value::Object(m) = b { m.len() } else { 1 };
(Some(b.clone()), count)
}
(Some(into_val), Some(from_val)) => {
let (merged, added) = merge_json(into_val, from_val, strategy);
(Some(merged), added)
}
}
}
pub(super) fn note_update_values_equal(left: &Option<Value>, right: &Option<Value>) -> bool {
fn equal(left: &Value, right: &Value, is_tags_field: bool, is_properties_object: bool) -> bool {
match (left, right) {
(Value::Object(a), Value::Object(b)) => {
a.len() == b.len()
&& a.iter().all(|(key, value)| {
b.get(key).is_some_and(|other| {
equal(value, other, is_properties_object && key == "tags", false)
})
})
}
(Value::Array(a), Value::Array(b)) if is_tags_field => {
if a.len() != b.len() {
return false;
}
let mut left = a.iter().map(Value::to_string).collect::<Vec<_>>();
let mut right = b.iter().map(Value::to_string).collect::<Vec<_>>();
left.sort_unstable();
right.sort_unstable();
left == right
}
(Value::Array(a), Value::Array(b)) => {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(left, right)| equal(left, right, false, false))
}
_ => left == right,
}
}
match (left, right) {
(None, None) => true,
(Some(left), Some(right)) => equal(left, right, false, true),
_ => false,
}
}
pub(super) fn merge_json(
into: &Value,
from: &Value,
strategy: EntityDedupMergePolicy,
) -> (Value, usize) {
match (into, from, strategy) {
(Value::Object(a), Value::Object(b), EntityDedupMergePolicy::Union) => {
let mut result = a.clone();
let mut added = 0usize;
for (k, v_from) in b {
if let Some(v_into) = a.get(k) {
let (merged, sub_added) =
merge_json(v_into, v_from, EntityDedupMergePolicy::Union);
result.insert(k.clone(), merged);
added += sub_added;
} else {
result.insert(k.clone(), v_from.clone());
added += 1;
}
}
(Value::Object(result), added)
}
(Value::Object(a), Value::Object(b), EntityDedupMergePolicy::PreferInto) => {
let mut result = a.clone();
let mut added = 0usize;
for (k, v) in b {
if !a.contains_key(k) {
result.insert(k.clone(), v.clone());
added += 1;
}
}
(Value::Object(result), added)
}
(Value::Object(a), Value::Object(b), EntityDedupMergePolicy::PreferFrom) => {
let mut result = a.clone();
let mut added = 0usize;
for (k, v) in b {
result.insert(k.clone(), v.clone());
if !a.contains_key(k) {
added += 1;
}
}
(Value::Object(result), added)
}
(_into_val, from_val, EntityDedupMergePolicy::PreferFrom) => (from_val.clone(), 1),
_ => (into.clone(), 0),
}
}
pub(crate) fn union_tags(into: &[String], from: &[String]) -> (Vec<String>, usize) {
let mut seen: HashSet<&str> = into.iter().map(|s| s.as_str()).collect();
let mut result: Vec<String> = into.to_vec();
let mut added = 0usize;
for tag in from {
if seen.insert(tag.as_str()) {
result.push(tag.clone());
added += 1;
}
}
(result, added)
}