use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::str::FromStr;
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::entry::Entry;
use crate::identifier::EntryAddress;
use crate::meta::{MetaFieldNameError, validate_meta_field_name};
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StructuralRippleSettings {
#[serde(skip_serializing_if = "is_false")]
pub lake: bool,
#[serde(skip_serializing_if = "is_false")]
pub anchor: bool,
}
impl StructuralRippleSettings {
pub fn new(lake: bool, anchor: bool) -> Self {
Self { lake, anchor }
}
pub fn is_empty(&self) -> bool {
!self.lake && !self.anchor
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StructuralEdgeSettings {
pub render: bool,
pub ripple: StructuralRippleSettings,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct StructuralEdgeConfig {
#[serde(skip_serializing_if = "is_false")]
render: bool,
}
impl Serialize for StructuralEdgeSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
StructuralEdgeConfig { render: self.render }.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for StructuralEdgeSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let config = StructuralEdgeConfig::deserialize(deserializer)?;
Ok(Self::render_only(config.render))
}
}
impl StructuralEdgeSettings {
pub fn new(render: bool, ripple: StructuralRippleSettings) -> Self {
Self { render, ripple }
}
pub fn render_only(enabled: bool) -> Self {
Self::new(enabled, StructuralRippleSettings::default())
}
pub fn render_and_ripple(render: bool, lake: bool, anchor: bool) -> Self {
Self::new(render, StructuralRippleSettings::new(lake, anchor))
}
}
impl fmt::Display for StructuralEdgeSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if self.render {
parts.push("render=true");
}
if self.ripple.lake {
parts.push("ripple.lake=true");
}
if self.ripple.anchor {
parts.push("ripple.anchor=true");
}
if parts.is_empty() {
write!(formatter, "none")
} else {
write!(formatter, "{}", parts.join(" "))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StructuralEdgeDirection {
To,
From,
Clique,
}
impl StructuralEdgeDirection {
pub const ORDER: [Self; 3] = [Self::To, Self::From, Self::Clique];
pub fn label(self) -> &'static str {
match self {
| Self::To => "to",
| Self::From => "from",
| Self::Clique => "clique",
}
}
}
impl fmt::Display for StructuralEdgeDirection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.label())
}
}
impl FromStr for StructuralEdgeDirection {
type Err = StructuralEdgeDirectionParseError;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
match raw {
| "to" => Ok(Self::To),
| "from" => Ok(Self::From),
| "clique" => Ok(Self::Clique),
| direction => Err(StructuralEdgeDirectionParseError(direction.to_owned())),
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
#[error("unknown structural link direction `{0}`; expected to, from, or clique")]
pub struct StructuralEdgeDirectionParseError(String);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StructuralFieldSettings {
#[serde(skip_serializing_if = "is_default")]
pub to: StructuralEdgeSettings,
#[serde(skip_serializing_if = "is_default")]
pub from: StructuralEdgeSettings,
#[serde(skip_serializing_if = "is_default")]
pub clique: StructuralEdgeSettings,
}
impl StructuralFieldSettings {
pub fn new(
to: StructuralEdgeSettings, from: StructuralEdgeSettings, clique: StructuralEdgeSettings,
) -> Self {
Self { to, from, clique }
}
pub fn render_only(to: bool, from: bool, clique: bool) -> Self {
Self::new(
StructuralEdgeSettings::render_only(to),
StructuralEdgeSettings::render_only(from),
StructuralEdgeSettings::render_only(clique),
)
}
pub fn edge(&self, direction: StructuralEdgeDirection) -> &StructuralEdgeSettings {
match direction {
| StructuralEdgeDirection::To => &self.to,
| StructuralEdgeDirection::From => &self.from,
| StructuralEdgeDirection::Clique => &self.clique,
}
}
pub fn with_tide_policy(mut self, policy: StructuralTideSettings) -> Self {
self.to.ripple = policy.to;
self.from.ripple = policy.from;
self.clique.ripple = policy.clique;
self
}
pub fn without_tide_policy(self) -> Self {
Self::render_only(self.to.render, self.from.render, self.clique.render)
}
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StructuralTideSettings {
#[serde(skip_serializing_if = "StructuralRippleSettings::is_empty")]
pub to: StructuralRippleSettings,
#[serde(skip_serializing_if = "StructuralRippleSettings::is_empty")]
pub from: StructuralRippleSettings,
#[serde(skip_serializing_if = "StructuralRippleSettings::is_empty")]
pub clique: StructuralRippleSettings,
}
impl StructuralTideSettings {
pub fn new(
to: StructuralRippleSettings, from: StructuralRippleSettings,
clique: StructuralRippleSettings,
) -> Self {
Self { to, from, clique }
}
pub fn is_empty(&self) -> bool {
self.to.is_empty() && self.from.is_empty() && self.clique.is_empty()
}
}
pub type StructuralFieldMap = IndexMap<String, StructuralFieldSettings>;
pub type StructuralRelationMap = IndexMap<String, StructuralRelationSettings>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StructuralRelationSettings {
pub entry: EntryAddress,
}
pub type StructuralRenderMap = IndexMap<String, Vec<StructuralEdgeDirection>>;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct StructuralRenderSettings {
fields: StructuralRenderMap,
}
impl StructuralRenderSettings {
pub fn from_fields(
fields: impl IntoIterator<
Item = (impl Into<String>, impl IntoIterator<Item = StructuralEdgeDirection>),
>,
) -> Self {
Self {
fields: fields
.into_iter()
.map(|(field, directions)| (field.into(), directions.into_iter().collect()))
.collect(),
}
}
pub fn fields(&self) -> impl Iterator<Item = (&str, &[StructuralEdgeDirection])> {
self.fields.iter().map(|(field, directions)| (field.as_str(), directions.as_slice()))
}
pub fn directions_for(&self, field: &str) -> Option<&[StructuralEdgeDirection]> {
self.fields.get(field).map(Vec::as_slice)
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StructuralSettings {
fields: StructuralFieldMap,
entries: IndexMap<String, EntryAddress>,
}
impl Serialize for StructuralSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.relation_settings().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for StructuralSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let relations = StructuralRelationMap::deserialize(deserializer)?;
Ok(Self::from_relations(
relations.into_iter().map(|(field, settings)| (field, settings.entry)),
))
}
}
impl StructuralSettings {
pub fn from_entries(entries: &[Entry]) -> Self {
let mut relation_entries = entries
.iter()
.filter(|entry| {
entry.metadata.meta.is_structural_relation()
&& validate_structural_field_name(entry.id.as_str()).is_ok()
})
.map(|entry| entry.id.clone())
.collect::<Vec<_>>();
relation_entries.sort();
Self::from_relations(
relation_entries.into_iter().map(|entry| (entry.as_str().to_owned(), entry)),
)
}
pub fn from_fields(
fields: impl IntoIterator<Item = (impl Into<String>, StructuralFieldSettings)>,
) -> Self {
let mut settings = Self::default();
for (field, field_settings) in fields {
let field = field.into();
let entry = EntryAddress::new(&field)
.expect("structural field can default to its matching entry address");
settings.fields.insert(field.clone(), field_settings);
settings.entries.insert(field, entry);
}
settings
}
pub fn from_relations(
relations: impl IntoIterator<Item = (impl Into<String>, EntryAddress)>,
) -> Self {
let mut settings = Self::default();
for (field, entry) in relations {
settings.set_relation_entry(field, entry);
}
settings
}
pub fn relations(&self) -> impl Iterator<Item = (&str, &EntryAddress)> {
self.entries.iter().map(|(field, entry)| (field.as_str(), entry))
}
pub fn fields(&self) -> impl Iterator<Item = (&str, &StructuralFieldSettings)> {
self.fields.iter().map(|(field, settings)| (field.as_str(), settings))
}
pub fn contains_field(&self, field: &str) -> bool {
self.fields.contains_key(field)
}
pub fn entry_for_field(&self, field: &str) -> Option<&EntryAddress> {
self.entries.get(field)
}
pub fn contains_entry(&self, entry: &EntryAddress) -> bool {
self.entries.values().any(|defined| defined == entry)
}
pub fn set_relation_entry(&mut self, field: impl Into<String>, entry: EntryAddress) -> bool {
let field = field.into();
let changed = self.entries.get(&field) != Some(&entry);
self.fields.entry(field.clone()).or_default();
self.entries.insert(field, entry);
changed
}
pub fn rename_entry_reference(&mut self, old_id: &EntryAddress, new_id: &EntryAddress) -> bool {
let mut changed = false;
for entry in self.entries.values_mut() {
if entry == old_id {
*entry = new_id.clone();
changed = true;
}
}
changed
}
pub fn rename_field(&mut self, old_id: &EntryAddress, new_id: &EntryAddress) -> bool {
let old_field = old_id.as_str();
if !self.fields.contains_key(old_field) {
return false;
}
let mut renamed_fields = StructuralFieldMap::with_capacity(self.fields.len());
let mut renamed_entries = IndexMap::with_capacity(self.entries.len());
for (field, settings) in std::mem::take(&mut self.fields) {
if field == old_field {
renamed_fields.insert(new_id.as_str().to_owned(), settings);
} else {
renamed_fields.insert(field, settings);
}
}
for (field, entry) in std::mem::take(&mut self.entries) {
if field == old_field {
renamed_entries.insert(new_id.as_str().to_owned(), entry);
} else {
renamed_entries.insert(field, entry);
}
}
self.fields = renamed_fields;
self.entries = renamed_entries;
true
}
pub fn with_render_settings(&self, render: &StructuralRenderSettings) -> Self {
let mut fields = StructuralFieldMap::new();
let mut entries = IndexMap::new();
for (field, directions) in render.fields() {
let Some(entry) = self.entries.get(field) else {
continue;
};
fields.insert(
field.to_owned(),
StructuralFieldSettings::render_only(
directions.contains(&StructuralEdgeDirection::To),
directions.contains(&StructuralEdgeDirection::From),
directions.contains(&StructuralEdgeDirection::Clique),
),
);
entries.insert(field.to_owned(), entry.clone());
}
for (field, entry) in &self.entries {
if fields.contains_key(field) {
continue;
}
fields.insert(field.clone(), StructuralFieldSettings::default());
entries.insert(field.clone(), entry.clone());
}
Self { fields, entries }
}
pub fn with_tide_policies_from_entries(&self, entries: &[Entry]) -> Self {
let policies = entries
.iter()
.filter(|entry| entry.metadata.meta.is_structural_relation())
.map(|entry| {
(entry.id.as_str().to_owned(), entry.metadata.meta.tide.unwrap_or_default())
})
.collect::<BTreeMap<_, _>>();
let fields = self
.fields
.iter()
.map(|(field, settings)| {
let settings = settings.without_tide_policy();
let settings = policies
.get(
self.entries
.get(field)
.expect("structural relation has matching entry")
.as_str(),
)
.map(|policy| settings.with_tide_policy(*policy))
.unwrap_or(settings);
(field.clone(), settings)
})
.collect();
Self { fields, entries: self.entries.clone() }
}
fn relation_settings(&self) -> StructuralRelationMap {
self.entries
.iter()
.map(|(field, entry)| {
(field.clone(), StructuralRelationSettings { entry: entry.clone() })
})
.collect()
}
}
pub fn validate_structural_field_name(field: &str) -> Result<(), StructuralFieldNameError> {
if let Err(error) = validate_meta_field_name(field) {
return match error {
| MetaFieldNameError::Invalid(field) => Err(StructuralFieldNameError::Invalid(field)),
| MetaFieldNameError::Reserved(field) => Err(StructuralFieldNameError::Reserved(field)),
};
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum StructuralFieldNameError {
#[error("structural relation name must be a non-empty single-line metadata key: {0}")]
Invalid(String),
#[error("structural relation name is reserved for Sirno metadata: {0}")]
Reserved(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StructuralEdgeIndex {
sources_by_field_target: BTreeMap<String, BTreeMap<EntryAddress, BTreeSet<EntryAddress>>>,
cliques_by_field_target: BTreeMap<String, BTreeMap<EntryAddress, BTreeSet<EntryAddress>>>,
}
impl StructuralEdgeIndex {
pub fn from_entries(entries: &[Entry]) -> Self {
let mut sources_by_field_target =
BTreeMap::<String, BTreeMap<EntryAddress, BTreeSet<EntryAddress>>>::new();
let mut cliques_by_field_target =
BTreeMap::<String, BTreeMap<EntryAddress, BTreeSet<EntryAddress>>>::new();
for entry in entries {
for (field, targets) in entry.metadata.structural_fields() {
Self::insert_sources(
sources_by_field_target.entry(field.to_owned()).or_default(),
&entry.id,
targets,
);
Self::insert_cliques(
cliques_by_field_target.entry(field.to_owned()).or_default(),
&entry.id,
targets,
);
}
}
Self { sources_by_field_target, cliques_by_field_target }
}
fn insert_sources(
sources_by_target: &mut BTreeMap<EntryAddress, BTreeSet<EntryAddress>>,
source: &EntryAddress, targets: &[EntryAddress],
) {
for target in targets {
sources_by_target.entry(target.clone()).or_default().insert(source.clone());
}
}
fn insert_cliques(
cliques_by_target: &mut BTreeMap<EntryAddress, BTreeSet<EntryAddress>>,
source: &EntryAddress, targets: &[EntryAddress],
) {
for target in targets {
let clique = cliques_by_target.entry(target.clone()).or_default();
clique.insert(target.clone());
clique.insert(source.clone());
}
}
pub fn edge_targets(
&self, field: &str, direction: StructuralEdgeDirection, entry: &Entry,
) -> BTreeSet<EntryAddress> {
match direction {
| StructuralEdgeDirection::To => {
entry.metadata.structural_targets_for(field).iter().cloned().collect()
}
| StructuralEdgeDirection::From => self.incoming_targets(field, entry),
| StructuralEdgeDirection::Clique => self.clique_targets(field, entry),
}
}
fn incoming_targets(&self, field: &str, entry: &Entry) -> BTreeSet<EntryAddress> {
self.sources_by_field_target
.get(field)
.and_then(|sources_by_target| sources_by_target.get(&entry.id))
.cloned()
.unwrap_or_default()
}
fn clique_targets(&self, field: &str, entry: &Entry) -> BTreeSet<EntryAddress> {
let mut targets = BTreeSet::new();
let Some(cliques_by_target) = self.cliques_by_field_target.get(field) else {
return targets;
};
for target in entry.metadata.structural_targets_for(field) {
if let Some(clique) = cliques_by_target.get(target) {
targets.extend(clique.iter().filter(|id| *id != &entry.id).cloned());
}
}
if let Some(clique) = cliques_by_target.get(&entry.id) {
targets.extend(clique.iter().filter(|id| *id != &entry.id).cloned());
}
targets
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::{EntryMetaType, EntryMetadata};
#[test]
fn renames_structural_setting_field_in_place() {
let mut settings = StructuralSettings::from_fields([
("category", StructuralFieldSettings::default()),
("refines", StructuralFieldSettings::render_only(true, false, false)),
("belongs", StructuralFieldSettings::default()),
]);
assert!(settings.rename_field(
&EntryAddress::new("refines").unwrap(),
&EntryAddress::new("prerequisite").unwrap()
));
let fields = settings.fields().map(|(field, _)| field).collect::<Vec<_>>();
assert_eq!(fields, ["category", "prerequisite", "belongs"]);
assert_eq!(settings.fields().nth(1).map(|(_, settings)| settings.to.render), Some(true));
assert!(!settings.contains_field("refines"));
assert_eq!(
settings.entry_for_field("prerequisite"),
Some(&EntryAddress::new("refines").unwrap())
);
}
#[test]
fn relations_track_entry_addresses() {
let mut settings = StructuralSettings::from_relations([(
"kind",
EntryAddress::new("metadata.kind").unwrap(),
)]);
assert!(settings.contains_field("kind"));
assert!(settings.contains_entry(&EntryAddress::new("metadata.kind").unwrap()));
assert_eq!(
settings.entry_for_field("kind"),
Some(&EntryAddress::new("metadata.kind").unwrap())
);
assert!(settings.rename_entry_reference(
&EntryAddress::new("metadata.kind").unwrap(),
&EntryAddress::new("metadata.type").unwrap()
));
assert_eq!(
settings.entry_for_field("kind"),
Some(&EntryAddress::new("metadata.type").unwrap())
);
}
#[test]
fn render_settings_apply_to_relations() {
let settings = StructuralSettings::from_relations([
("kind", EntryAddress::new("metadata-kind").unwrap()),
("area", EntryAddress::new("area").unwrap()),
]);
let render = StructuralRenderSettings::from_fields([(
"kind",
[StructuralEdgeDirection::To, StructuralEdgeDirection::From],
)]);
let effective = settings.with_render_settings(&render);
let fields = effective.fields().collect::<Vec<_>>();
assert!(fields[0].1.to.render);
assert!(fields[0].1.from.render);
assert!(!fields[0].1.clique.render);
assert!(!fields[1].1.to.render);
}
#[test]
fn merges_entry_tide_policy_into_render_settings() {
let settings = StructuralSettings::from_fields([(
"belongs",
StructuralFieldSettings::render_only(true, true, false),
)]);
let mut metadata = EntryMetadata::new("Belongs", "A relation.").unwrap();
metadata.meta.entry_type = Some(EntryMetaType::Structural);
metadata.meta.tide = Some(StructuralTideSettings::new(
StructuralRippleSettings::new(true, false),
StructuralRippleSettings::new(true, true),
StructuralRippleSettings::default(),
));
let entry = Entry::new(EntryAddress::new("belongs").unwrap(), metadata, "Body.\n");
let effective = settings.with_tide_policies_from_entries(&[entry]);
let (_, field_settings) = effective.fields().next().unwrap();
assert!(field_settings.to.render);
assert!(field_settings.from.render);
assert!(field_settings.to.ripple.lake);
assert!(!field_settings.to.ripple.anchor);
assert!(field_settings.from.ripple.lake);
assert!(field_settings.from.ripple.anchor);
assert!(!field_settings.clique.ripple.lake);
}
#[test]
fn ignores_render_settings_without_entry_policy() {
let settings = StructuralSettings::from_fields([(
"belongs",
StructuralFieldSettings::new(
StructuralEdgeSettings::render_and_ripple(true, true, true),
StructuralEdgeSettings::render_and_ripple(false, true, true),
StructuralEdgeSettings::render_and_ripple(false, true, true),
),
)]);
let effective = settings.with_tide_policies_from_entries(&[]);
let (_, field_settings) = effective.fields().next().unwrap();
assert!(field_settings.to.render);
assert!(field_settings.to.ripple.is_empty());
assert!(field_settings.from.ripple.is_empty());
assert!(field_settings.clique.ripple.is_empty());
}
}