use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use crate::link::Link;
use crate::link_storage::ChangeObserver;
use crate::lino_link::LinoLink;
use crate::named_type_links::{escape_lino_reference, NamedTypeLinks};
use crate::named_types::NamedTypesDecorator;
use crate::parser::Parser;
use crate::query_processor::QueryProcessor;
pub const INTERNAL_NAME_PREFIX: &str = "__persistent_transformation:";
const MISSING_PARTS: &str =
"Persistent transformation query must contain a condition and a substitution.";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PersistentTransformationKind {
Once,
Always,
}
impl fmt::Display for PersistentTransformationKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
Self::Once => "Once",
Self::Always => "Always",
};
formatter.write_str(text)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersistentTransformation {
pub root: u32,
pub kind: PersistentTransformationKind,
pub condition: String,
pub substitution: String,
}
impl PersistentTransformation {
pub fn query(&self) -> String {
format!("({} {})", self.condition, self.substitution)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersistentTransformationQuery {
pub condition: String,
pub substitution: String,
}
impl PersistentTransformationQuery {
pub fn parse(query: &str) -> Result<Self> {
let parsed = Parser::new().parse(query)?;
let outer = parsed.first().ok_or_else(|| anyhow!(MISSING_PARTS))?;
let (condition, substitution) = match outer.values.as_deref() {
Some(values) if values.len() >= 2 => (&values[0], &values[1]),
_ if parsed.len() >= 2 => (&parsed[0], &parsed[1]),
_ => return Err(anyhow!(MISSING_PARTS)),
};
Ok(Self {
condition: format_lino(condition),
substitution: format_lino(substitution),
})
}
pub fn query(&self) -> String {
format!("({} {})", self.condition, self.substitution)
}
}
fn format_lino(link: &LinoLink) -> String {
let values = link.values.as_deref().unwrap_or(&[]);
let id = link.id.as_deref().unwrap_or_default();
if values.is_empty() {
return if id.is_empty() {
"()".to_string()
} else {
escape_lino_reference(id)
};
}
let rendered = values.iter().map(format_lino).collect::<Vec<_>>().join(" ");
if id.is_empty() {
format!("({rendered})")
} else {
format!("({}: {})", escape_lino_reference(id), rendered)
}
}
pub fn make_triggers_database_filename<P: AsRef<Path>>(database_filename: P) -> PathBuf {
let path = database_filename.as_ref();
let stem = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or_default();
let name = format!("{stem}.triggers.links");
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
_ => PathBuf::from(name),
}
}
pub enum TriggerStore {
Embedded,
Sidecar(Box<NamedTypesDecorator>),
}
impl TriggerStore {
pub fn sidecar<P: AsRef<Path>>(path: P, trace: bool) -> Result<Self> {
Ok(Self::Sidecar(Box::new(NamedTypesDecorator::new(
path, trace,
)?)))
}
}
#[derive(Debug, Clone, Copy)]
struct TriggerSchema {
once: u32,
always: u32,
condition: u32,
substitution: u32,
}
const SCHEMA_NAMES: [&str; 6] = [
"Type",
"Trigger",
"Once",
"Always",
"Condition",
"Substitution",
];
fn ensure_schema<L: NamedTypeLinks + ?Sized>(links: &mut L) -> Result<TriggerSchema> {
let r#type = links.get_or_create_named("Type")?;
let trigger = links.get_or_create_named("Trigger")?;
let once = links.get_or_create_named("Once")?;
let always = links.get_or_create_named("Always")?;
let condition = links.get_or_create_named("Condition")?;
let substitution = links.get_or_create_named("Substitution")?;
links.get_or_create(r#type, trigger);
links.get_or_create(trigger, once);
links.get_or_create(trigger, always);
links.get_or_create(r#type, condition);
links.get_or_create(r#type, substitution);
Ok(TriggerSchema {
once,
always,
condition,
substitution,
})
}
fn try_get_schema<L: NamedTypeLinks + ?Sized>(links: &mut L) -> Result<Option<TriggerSchema>> {
let mut ids = [0u32; 6];
for (slot, name) in ids.iter_mut().zip(SCHEMA_NAMES) {
match links.get_by_name(name)? {
Some(id) => *slot = id,
None => return Ok(None),
}
}
Ok(Some(TriggerSchema {
once: ids[2],
always: ids[3],
condition: ids[4],
substitution: ids[5],
}))
}
fn triggers_in<L: NamedTypeLinks + ?Sized>(links: &mut L) -> Result<Vec<PersistentTransformation>> {
let Some(schema) = try_get_schema(links)? else {
return Ok(Vec::new());
};
let mut all = links.all_links();
all.sort_by_key(|link| link.index);
let by_index: HashMap<u32, Link> = all.iter().map(|link| (link.index, *link)).collect();
let mut triggers = Vec::new();
for link in &all {
let kind = if link.source == schema.always {
PersistentTransformationKind::Always
} else if link.source == schema.once {
PersistentTransformationKind::Once
} else {
continue;
};
let Some(payload) = by_index.get(&link.target) else {
continue;
};
let (Some(condition_record), Some(substitution_record)) =
(by_index.get(&payload.source), by_index.get(&payload.target))
else {
continue;
};
if condition_record.source != schema.condition
|| substitution_record.source != schema.substitution
{
continue;
}
let condition = links.get_name(condition_record.target)?;
let substitution = links.get_name(substitution_record.target)?;
let (Some(condition), Some(substitution)) = (
decode_text_name(condition.as_deref(), "condition"),
decode_text_name(substitution.as_deref(), "substitution"),
) else {
continue;
};
triggers.push(PersistentTransformation {
root: link.index,
kind,
condition,
substitution,
});
}
Ok(triggers)
}
fn store_trigger_in<L: NamedTypeLinks + ?Sized>(
links: &mut L,
kind: PersistentTransformationKind,
parsed: &PersistentTransformationQuery,
) -> Result<u32> {
let schema = ensure_schema(links)?;
let condition_text = links.get_or_create_named(&condition_text_name(&parsed.condition))?;
let substitution_text =
links.get_or_create_named(&substitution_text_name(&parsed.substitution))?;
let condition_record = links.get_or_create(schema.condition, condition_text);
let substitution_record = links.get_or_create(schema.substitution, substitution_text);
let payload = links.get_or_create(condition_record, substitution_record);
let trigger_type = match kind {
PersistentTransformationKind::Always => schema.always,
PersistentTransformationKind::Once => schema.once,
};
Ok(links.get_or_create(trigger_type, payload))
}
fn delete_trigger_root<L: NamedTypeLinks + ?Sized>(links: &mut L, root: u32) -> Result<bool> {
if !links.exists(root) {
return Ok(false);
}
links.delete(root)?;
Ok(true)
}
fn condition_text_name(condition: &str) -> String {
format!("{INTERNAL_NAME_PREFIX}condition:{condition}")
}
fn substitution_text_name(substitution: &str) -> String {
format!("{INTERNAL_NAME_PREFIX}substitution:{substitution}")
}
fn decode_text_name(name: Option<&str>, part: &str) -> Option<String> {
let prefix = format!("{INTERNAL_NAME_PREFIX}{part}:");
name?.strip_prefix(&prefix).map(str::to_string)
}
macro_rules! on_trigger_links {
($self:expr, $call:ident($($arg:expr),* $(,)?)) => {
match $self.triggers {
TriggerStore::Sidecar(ref mut store) => $call(store.as_mut() $(, $arg)*),
TriggerStore::Embedded => $call(&mut $self.links $(, $arg)*),
}
};
}
pub struct PersistentTransformationDecorator<L: NamedTypeLinks> {
links: L,
triggers: TriggerStore,
trace: bool,
applying_triggers: bool,
suppress_triggers: bool,
auto_create_missing_references: bool,
pending_error: Option<anyhow::Error>,
}
impl<L: NamedTypeLinks> PersistentTransformationDecorator<L> {
pub fn new(links: L, triggers: TriggerStore, trace: bool) -> Self {
Self {
links,
triggers,
trace,
applying_triggers: false,
suppress_triggers: false,
auto_create_missing_references: false,
pending_error: None,
}
}
pub fn embedded(links: L, trace: bool) -> Self {
Self::new(links, TriggerStore::Embedded, trace)
}
pub fn with_sidecar(links: L, trigger_links: NamedTypesDecorator, trace: bool) -> Self {
Self::new(links, TriggerStore::Sidecar(Box::new(trigger_links)), trace)
}
pub fn with_auto_create_missing_references(mut self, enabled: bool) -> Self {
self.auto_create_missing_references = enabled;
self
}
pub fn auto_create_missing_references(&self) -> bool {
self.auto_create_missing_references
}
pub fn set_auto_create_missing_references(&mut self, enabled: bool) {
self.auto_create_missing_references = enabled;
}
pub fn inner(&self) -> &L {
&self.links
}
pub fn inner_mut(&mut self) -> &mut L {
&mut self.links
}
pub fn trigger_store(&self) -> &TriggerStore {
&self.triggers
}
pub fn trigger_store_mut(&mut self) -> &mut TriggerStore {
&mut self.triggers
}
pub fn into_parts(self) -> (L, TriggerStore) {
(self.links, self.triggers)
}
pub fn store_trigger(
&mut self,
kind: PersistentTransformationKind,
query: &str,
) -> Result<u32> {
let parsed = PersistentTransformationQuery::parse(query)?;
let root = self.without_trigger_application(|this| {
on_trigger_links!(this, store_trigger_in(kind, &parsed))
})?;
self.trace_msg(&format!(
"Stored {kind} trigger #{root}: {}",
parsed.query()
));
Ok(root)
}
pub fn remove_triggers(&mut self, query: &str) -> Result<usize> {
let parsed = PersistentTransformationQuery::parse(query)?;
self.without_trigger_application(|this| {
let matching: Vec<u32> = this
.triggers()?
.into_iter()
.filter(|trigger| {
trigger.condition == parsed.condition
&& trigger.substitution == parsed.substitution
})
.map(|trigger| trigger.root)
.collect();
for root in &matching {
on_trigger_links!(this, delete_trigger_root(*root))?;
this.trace_msg(&format!("Deleted trigger #{root}"));
}
Ok(matching.len())
})
}
pub fn triggers(&mut self) -> Result<Vec<PersistentTransformation>> {
on_trigger_links!(self, triggers_in())
}
fn without_trigger_application<R>(&mut self, action: impl FnOnce(&mut Self) -> R) -> R {
let previous = self.suppress_triggers;
self.suppress_triggers = true;
let result = action(self);
self.suppress_triggers = previous;
result
}
fn after_write(&mut self) {
if let Err(error) = self.apply_triggers_after_operation() {
if self.pending_error.is_none() {
self.pending_error = Some(error);
}
}
}
fn take_pending_error(&mut self) -> Result<()> {
match self.pending_error.take() {
Some(error) => Err(error),
None => Ok(()),
}
}
fn apply_triggers_after_operation(&mut self) -> Result<()> {
if self.suppress_triggers || self.applying_triggers {
return Ok(());
}
let triggers = self.triggers()?;
if triggers.is_empty() {
return Ok(());
}
self.applying_triggers = true;
let outcome = self.apply_triggers(&triggers);
self.applying_triggers = false;
outcome
}
fn apply_triggers(&mut self, triggers: &[PersistentTransformation]) -> Result<()> {
let processor = QueryProcessor::new(self.trace)
.with_auto_create_missing_references(self.auto_create_missing_references);
for trigger in triggers {
let changes = processor.process_query(self, &trigger.query())?;
if changes.is_empty() || trigger.kind != PersistentTransformationKind::Once {
continue;
}
let root = trigger.root;
self.without_trigger_application(|this| {
on_trigger_links!(this, delete_trigger_root(root))
})?;
self.trace_msg(&format!("Deleted trigger #{root}"));
}
Ok(())
}
fn trace_msg(&self, message: &str) {
if self.trace {
println!("[PersistentTransformation] {message}");
}
}
}
impl<L: NamedTypeLinks> NamedTypeLinks for PersistentTransformationDecorator<L> {
fn create(&mut self, source: u32, target: u32) -> u32 {
let index = self.links.create(source, target);
self.after_write();
index
}
fn ensure_created(&mut self, id: u32) -> u32 {
let existed = self.links.exists(id);
let index = self.links.ensure_created(id);
if !existed {
self.after_write();
}
index
}
fn get_link(&mut self, id: u32) -> Option<Link> {
self.links.get_link(id)
}
fn exists(&mut self, id: u32) -> bool {
self.links.exists(id)
}
fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
let link = self.links.update(id, source, target)?;
self.apply_triggers_after_operation()?;
self.take_pending_error()?;
Ok(link)
}
fn delete(&mut self, id: u32) -> Result<Link> {
let link = self.links.delete(id)?;
self.apply_triggers_after_operation()?;
self.take_pending_error()?;
Ok(link)
}
fn delete_observed(&mut self, id: u32, observer: ChangeObserver<'_>) -> Result<Link> {
let link = self.links.delete_observed(id, observer)?;
self.apply_triggers_after_operation()?;
self.take_pending_error()?;
Ok(link)
}
fn all_links(&mut self) -> Vec<Link> {
self.links.all_links()
}
fn search(&mut self, source: u32, target: u32) -> Option<u32> {
self.links.search(source, target)
}
fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
if let Some(index) = self.links.search(source, target) {
return index;
}
let index = self.links.get_or_create(source, target);
self.after_write();
index
}
fn get_name(&mut self, id: u32) -> Result<Option<String>> {
self.links.get_name(id)
}
fn set_name(&mut self, id: u32, name: &str) -> Result<u32> {
self.links.set_name(id, name)
}
fn get_by_name(&mut self, name: &str) -> Result<Option<u32>> {
self.links.get_by_name(name)
}
fn remove_name(&mut self, id: u32) -> Result<()> {
self.links.remove_name(id)
}
fn save(&mut self) -> Result<()> {
self.take_pending_error()?;
self.links.save()?;
if let TriggerStore::Sidecar(store) = &mut self.triggers {
store.save()?;
}
Ok(())
}
}