use std::collections::BTreeSet;
use crate::content::node::{NodeState, PropertyState};
use crate::content::property::PropertyValue;
use crate::index::{IndexResult, strict_string, values_of};
pub const STATUS_NODE_NAME: &str = ":status";
pub const STORED_DEFINITION_NODE_NAME: &str = ":index-definition";
pub const DRIFT_IGNORED_PROPERTY_NAMES: [&str; 2] = ["reindex", "reindexCount"];
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct StatusNode {
pub unique_identifier: Option<String>,
pub last_updated: Option<String>,
pub indexed_nodes: Option<i64>,
pub reindex_completion_timestamp: Option<String>,
}
impl StatusNode {
pub fn read(definition: &NodeState<'_>) -> IndexResult<Option<Self>> {
let Some(node) = definition.child_node(STATUS_NODE_NAME)? else {
return Ok(None);
};
Ok(Some(Self {
unique_identifier: strict_string(node.property("uid")?.as_ref()).map(str::to_owned),
last_updated: first_text(node.property("lastUpdated")?.as_ref()),
indexed_nodes: crate::index::converting_long(node.property("indexedNodes")?.as_ref()),
reindex_completion_timestamp: first_text(
node.property("reindexCompletionTimestamp")?.as_ref(),
),
}))
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct StoredDefinition {
pub creation_timestamp: Option<String>,
pub seed: Option<i64>,
}
impl StoredDefinition {
pub fn read(definition: &NodeState<'_>) -> IndexResult<Option<Self>> {
let Some(node) = definition.child_node(STORED_DEFINITION_NODE_NAME)? else {
return Ok(None);
};
Ok(Some(Self {
creation_timestamp: first_text(node.property("creationTimestamp")?.as_ref()),
seed: crate::index::converting_long(node.property("seed")?.as_ref()),
}))
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct DefinitionDifference {
pub path: String,
pub kind: DifferenceKind,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum DifferenceKind {
Added,
Removed,
Changed,
}
pub fn definition_drift(
definition: &NodeState<'_>,
stored: &NodeState<'_>,
extra_ignored_property_names: &[String],
) -> IndexResult<Vec<DefinitionDifference>> {
let mut ignored: BTreeSet<&str> = DRIFT_IGNORED_PROPERTY_NAMES.into_iter().collect();
ignored.extend(extra_ignored_property_names.iter().map(String::as_str));
let mut differences = Vec::new();
compare_nodes(stored, definition, "", &ignored, &mut differences)?;
differences.sort();
Ok(differences)
}
fn compare_nodes(
before: &NodeState<'_>,
after: &NodeState<'_>,
path: &str,
ignored: &BTreeSet<&str>,
differences: &mut Vec<DefinitionDifference>,
) -> IndexResult<()> {
compare_properties(before, after, path, ignored, differences)?;
compare_children(before, after, path, ignored, differences)
}
fn compare_properties(
before: &NodeState<'_>,
after: &NodeState<'_>,
path: &str,
ignored: &BTreeSet<&str>,
differences: &mut Vec<DefinitionDifference>,
) -> IndexResult<()> {
let before_properties = visible_properties(before, ignored)?;
let after_properties = visible_properties(after, ignored)?;
for property in &after_properties {
match before_properties
.iter()
.find(|candidate| candidate.name == property.name)
{
None => differences.push(DefinitionDifference {
path: format!("{path}/{}", property.name),
kind: DifferenceKind::Added,
}),
Some(previous) if previous != property => differences.push(DefinitionDifference {
path: format!("{path}/{}", property.name),
kind: DifferenceKind::Changed,
}),
Some(_) => {}
}
}
for property in &before_properties {
if !after_properties
.iter()
.any(|candidate| candidate.name == property.name)
{
differences.push(DefinitionDifference {
path: format!("{path}/{}", property.name),
kind: DifferenceKind::Removed,
});
}
}
Ok(())
}
fn compare_children(
before: &NodeState<'_>,
after: &NodeState<'_>,
path: &str,
ignored: &BTreeSet<&str>,
differences: &mut Vec<DefinitionDifference>,
) -> IndexResult<()> {
let before_children = visible_children(before)?;
let after_children = visible_children(after)?;
for (name, child) in &after_children {
match before_children
.iter()
.find(|(candidate, _)| candidate == name)
{
None => differences.push(DefinitionDifference {
path: format!("{path}/{name}"),
kind: DifferenceKind::Added,
}),
Some((_, previous)) => {
compare_nodes(
previous,
child,
&format!("{path}/{name}"),
ignored,
differences,
)?;
}
}
}
for (name, _) in &before_children {
if !after_children
.iter()
.any(|(candidate, _)| candidate == name)
{
differences.push(DefinitionDifference {
path: format!("{path}/{name}"),
kind: DifferenceKind::Removed,
});
}
}
Ok(())
}
fn visible_properties(
node: &NodeState<'_>,
ignored: &BTreeSet<&str>,
) -> IndexResult<Vec<PropertyState>> {
Ok(node
.properties()?
.into_iter()
.filter(|property| {
!property.name.starts_with(':') && !ignored.contains(property.name.as_str())
})
.collect())
}
fn visible_children<'provider>(
node: &NodeState<'provider>,
) -> IndexResult<Vec<(String, NodeState<'provider>)>> {
Ok(node
.child_node_entries()?
.into_iter()
.filter(|(name, _)| !name.starts_with(':'))
.collect())
}
fn first_text(property: Option<&PropertyState>) -> Option<String> {
property.and_then(|property| values_of(property).first().and_then(PropertyValue::as_text))
}