kmp-application 0.1.9

Application services of the KMP kernel: the use cases behind ingest, wake, ask, near, rewind and trace
Documentation
use std::cmp::Ordering;

use kmp_domain::{NodeNeighborhood, NodeProjection, NodeRelationProjection};

pub(crate) fn ordered_neighborhood(mut neighborhood: NodeNeighborhood) -> NodeNeighborhood {
    neighborhood.neighbors.sort_by(compare_nodes);
    neighborhood.relations.sort_by(compare_relations);
    neighborhood
}

fn compare_nodes(left: &NodeProjection, right: &NodeProjection) -> Ordering {
    (&left.node_id, &left.node_kind, &left.title).cmp(&(
        &right.node_id,
        &right.node_kind,
        &right.title,
    ))
}

fn compare_relations(left: &NodeRelationProjection, right: &NodeRelationProjection) -> Ordering {
    left.explanation
        .sequence()
        .unwrap_or(u32::MAX)
        .cmp(&right.explanation.sequence().unwrap_or(u32::MAX))
        .then_with(|| {
            (
                &left.source_node_id,
                &left.target_node_id,
                &left.relation_type,
            )
                .cmp(&(
                    &right.source_node_id,
                    &right.target_node_id,
                    &right.relation_type,
                ))
        })
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use kmp_domain::{
        NodeNeighborhood, NodeProjection, NodeRelationProjection, RelationExplanation,
        RelationSemanticClass,
    };

    use super::ordered_neighborhood;

    #[test]
    fn ordered_neighborhood_sorts_neighbors_and_relationships() {
        let ordered = ordered_neighborhood(NodeNeighborhood {
            root: node("story-123", "story", "Root"),
            neighbors: vec![
                node("task-2", "task", "Task B"),
                node("decision-1", "decision", "Decision"),
                node("task-1", "task", "Task A"),
            ],
            relations: vec![
                relation("story-123", "task-2", "HAS_TASK"),
                relation("story-123", "decision-1", "RECORDS"),
                relation("story-123", "task-1", "HAS_TASK"),
            ],
        });

        assert_eq!(ordered.neighbors[0].node_id, "decision-1");
        assert_eq!(ordered.neighbors[1].node_id, "task-1");
        assert_eq!(ordered.neighbors[2].node_id, "task-2");
        assert_eq!(ordered.relations[0].target_node_id, "decision-1");
        assert_eq!(ordered.relations[1].target_node_id, "task-1");
        assert_eq!(ordered.relations[2].target_node_id, "task-2");
    }

    #[test]
    fn ordered_neighborhood_prefers_relation_sequence_when_present() {
        let ordered = ordered_neighborhood(NodeNeighborhood {
            root: node("story-123", "story", "Root"),
            neighbors: vec![
                node("task-1", "task", "Task"),
                node("task-2", "task", "Task"),
            ],
            relations: vec![
                relation_with_properties("story-123", "task-2", "HAS_TASK", [("sequence", "2")]),
                relation_with_properties("story-123", "task-1", "HAS_TASK", [("sequence", "1")]),
            ],
        });

        assert_eq!(ordered.relations[0].target_node_id, "task-1");
        assert_eq!(ordered.relations[1].target_node_id, "task-2");
    }

    fn node(node_id: &str, node_kind: &str, title: &str) -> NodeProjection {
        NodeProjection {
            node_id: node_id.to_string(),
            node_kind: node_kind.to_string(),
            title: title.to_string(),
            summary: String::new(),
            status: "ACTIVE".to_string(),
            labels: Vec::new(),
            properties: BTreeMap::new(),
            provenance: None,
        }
    }

    fn relation(
        source_node_id: &str,
        target_node_id: &str,
        relation_type: &str,
    ) -> NodeRelationProjection {
        NodeRelationProjection {
            source_node_id: source_node_id.to_string(),
            target_node_id: target_node_id.to_string(),
            relation_type: relation_type.to_string(),
            explanation: RelationExplanation::new(RelationSemanticClass::Structural),
        }
    }

    fn relation_with_properties<const N: usize>(
        source_node_id: &str,
        target_node_id: &str,
        relation_type: &str,
        properties: [(&str, &str); N],
    ) -> NodeRelationProjection {
        let mut explanation = RelationExplanation::new(RelationSemanticClass::Structural);

        for (key, value) in properties {
            if key == "sequence" {
                explanation = explanation
                    .with_sequence(value.parse::<u32>().expect("sequence should parse as u32"));
            }
        }

        NodeRelationProjection {
            source_node_id: source_node_id.to_string(),
            target_node_id: target_node_id.to_string(),
            relation_type: relation_type.to_string(),
            explanation,
        }
    }
}