use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::str::FromStr;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::entry::Entry;
use crate::identifier::EntryAddress;
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 frost: bool,
}
impl StructuralRippleSettings {
pub fn new(lake: bool, frost: bool) -> Self {
Self { lake, frost }
}
pub fn is_empty(&self) -> bool {
!self.lake && !self.frost
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StructuralEdgeSettings {
#[serde(skip_serializing_if = "is_false")]
pub render: bool,
#[serde(skip_serializing_if = "StructuralRippleSettings::is_empty")]
pub ripple: StructuralRippleSettings,
}
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, frost: bool) -> Self {
Self::new(render, StructuralRippleSettings::new(lake, frost))
}
}
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.frost {
parts.push("ripple.frost=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 edge 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,
}
}
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}
pub type StructuralFieldMap = IndexMap<String, StructuralFieldSettings>;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct StructuralSettings {
fields: StructuralFieldMap,
}
impl StructuralSettings {
pub fn from_fields(
fields: impl IntoIterator<Item = (impl Into<String>, StructuralFieldSettings)>,
) -> Self {
Self {
fields: fields.into_iter().map(|(field, settings)| (field.into(), settings)).collect(),
}
}
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 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 = StructuralFieldMap::with_capacity(self.fields.len());
for (field, settings) in std::mem::take(&mut self.fields) {
if field == old_field {
renamed.insert(new_id.as_str().to_owned(), settings);
} else {
renamed.insert(field, settings);
}
}
self.fields = renamed;
true
}
}
#[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::*;
#[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"));
}
}