use crate::content::node::NodeState;
use crate::index::status::{DefinitionDifference, DifferenceKind, definition_drift};
use crate::index::{IndexError, IndexResult};
pub const REWRITTEN_PROPERTY_NAMES: [&str; 4] = ["refresh", "seed", "corrupt", "indexImportState"];
pub const FACETS_CHILD_NAME: &str = "facets";
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DriftVerdict {
pub differences: Vec<DefinitionDifference>,
}
impl DriftVerdict {
#[must_use]
pub fn is_clean(&self) -> bool {
self.differences.is_empty()
}
#[must_use]
pub fn first(&self) -> Option<&DefinitionDifference> {
self.differences.first()
}
}
pub fn compare(file: &NodeState<'_>, stored: &NodeState<'_>) -> IndexResult<DriftVerdict> {
let ignored: Vec<String> = REWRITTEN_PROPERTY_NAMES
.iter()
.map(|name| (*name).to_owned())
.collect();
let store_has_facets = stored.child_node(FACETS_CHILD_NAME)?.is_some();
let mut differences = definition_drift(file, stored, &ignored)?;
differences.retain(|difference| {
!(!store_has_facets
&& difference.kind == DifferenceKind::Added
&& is_facets_path(&difference.path))
});
differences.extend(directional_property_differences(file, stored)?);
differences.sort();
differences.dedup();
Ok(DriftVerdict { differences })
}
fn is_facets_path(path: &str) -> bool {
path == format!("/{FACETS_CHILD_NAME}") || path.starts_with(&format!("/{FACETS_CHILD_NAME}/"))
}
fn directional_property_differences(
file: &NodeState<'_>,
stored: &NodeState<'_>,
) -> IndexResult<Vec<DefinitionDifference>> {
let mut differences = Vec::new();
for name in REWRITTEN_PROPERTY_NAMES {
let in_file = rendered_property(file, name)?;
let in_store = rendered_property(stored, name)?;
let tolerated = match name {
"refresh" => in_store.is_none(),
"seed" => in_store.is_none() || in_file == in_store,
"corrupt" | "indexImportState" => in_file.is_none(),
_ => true,
};
if tolerated || in_file == in_store {
continue;
}
differences.push(DefinitionDifference {
path: format!("/{name}"),
kind: match (in_file.is_some(), in_store.is_some()) {
(true, false) => DifferenceKind::Added,
(false, true) => DifferenceKind::Removed,
_ => DifferenceKind::Changed,
},
});
}
Ok(differences)
}
fn rendered_property(node: &NodeState<'_>, name: &str) -> IndexResult<Option<String>> {
let Some(property) = node.property(name)? else {
return Ok(None);
};
let rendered = match &property.values {
crate::content::node::PropertyValues::Single(value) => value.as_text().unwrap_or_default(),
crate::content::node::PropertyValues::Multiple(values) => values
.iter()
.map(|value| value.as_text().unwrap_or_default())
.collect::<Vec<_>>()
.join("\u{1}"),
};
Ok(Some(format!("{:?}:{rendered}", property.property_type)))
}
#[must_use]
pub fn refusal(index_path: &str, verdict: &DriftVerdict) -> IndexError {
let detail = verdict.first().map_or_else(
|| "the definitions file does not describe this definition".to_owned(),
|difference| {
let what = match difference.kind {
DifferenceKind::Added => "the file has",
DifferenceKind::Removed => "the store has",
DifferenceKind::Changed => "the two disagree on",
};
format!("{what} {}", difference.path)
},
);
IndexError::Record(crate::Error::InvalidFormat {
details: format!(
"the definitions file does not describe {index_path} as the store holds it: \
{detail}. froe imports index *data*, never a definition change — make \
definition changes through oak-run or AEM first."
),
})
}