katana-markdown-engine 0.1.0

Renderer-neutral Markdown document model for the KatanA ecosystem
Documentation
use super::types::{
    ConflictedTarget, MetadataDocument, MetadataEntry, MetadataReconcileRequest,
    MetadataReconcileResult, TargetResolution, TargetResolutionKind, UnresolvedTarget,
};
use crate::{KmeDocument, KmeNode, TextFingerprint};

pub struct MetadataResolver;

impl MetadataResolver {
    pub fn new() -> Self {
        Self
    }

    pub fn reconcile(
        &self,
        old_document: &KmeDocument,
        new_document: &KmeDocument,
        metadata: &MetadataDocument,
    ) -> Vec<TargetResolution> {
        metadata
            .entries
            .iter()
            .map(|entry| self.resolve_entry(old_document, new_document, entry))
            .collect()
    }

    pub fn reconcile_request(&self, request: MetadataReconcileRequest) -> MetadataReconcileResult {
        let resolutions = self.reconcile(
            &request.old_document,
            &request.new_document,
            &request.metadata,
        );
        MetadataReconcileResult {
            metadata: request.metadata,
            resolutions,
        }
    }

    fn resolve_entry(
        &self,
        old_document: &KmeDocument,
        new_document: &KmeDocument,
        entry: &MetadataEntry,
    ) -> TargetResolution {
        if let Some(node) = new_document.node_by_id(&entry.target.node_id) {
            return Self::resolved(entry, node);
        }
        let candidates = self.find_by_fingerprint(new_document, &entry.target.text_fingerprint);
        match candidates.as_slice() {
            [node] => return Self::moved(entry, node),
            [_, _, ..] => return Self::conflict(entry, &candidates),
            [] => {}
        }
        Self::unresolved(old_document, entry)
    }

    fn find_by_fingerprint<'a>(
        &self,
        document: &'a KmeDocument,
        fingerprint: &TextFingerprint,
    ) -> Vec<&'a KmeNode> {
        document
            .nodes
            .iter()
            .filter(|node| &node.source.raw.fingerprint() == fingerprint)
            .collect()
    }

    fn resolved(entry: &MetadataEntry, node: &KmeNode) -> TargetResolution {
        TargetResolution {
            key: entry.key.clone(),
            kind: TargetResolutionKind::Resolved {
                node_id: node.id.clone(),
            },
        }
    }

    fn moved(entry: &MetadataEntry, node: &KmeNode) -> TargetResolution {
        TargetResolution {
            key: entry.key.clone(),
            kind: TargetResolutionKind::Moved {
                previous_node_id: entry.target.node_id.clone(),
                node_id: node.id.clone(),
            },
        }
    }

    fn conflict(entry: &MetadataEntry, nodes: &[&KmeNode]) -> TargetResolution {
        TargetResolution {
            key: entry.key.clone(),
            kind: TargetResolutionKind::Conflict(ConflictedTarget {
                previous_node_id: entry.target.node_id.clone(),
                candidate_node_ids: nodes.iter().map(|node| node.id.clone()).collect(),
                reason: "multiple fingerprint matches".to_string(),
            }),
        }
    }

    fn unresolved(old_document: &KmeDocument, entry: &MetadataEntry) -> TargetResolution {
        let old_exists = old_document.node_by_id(&entry.target.node_id).is_some();
        let reason = if old_exists {
            "target disappeared"
        } else {
            "target unknown"
        };
        TargetResolution {
            key: entry.key.clone(),
            kind: TargetResolutionKind::Unresolved(UnresolvedTarget {
                node_id: entry.target.node_id.clone(),
                reason: reason.to_string(),
            }),
        }
    }
}

impl Default for MetadataResolver {
    fn default() -> Self {
        Self::new()
    }
}