From dc8ec0b991b2931bfd8e9616bbc43082cce250dc Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:43:07 +0100 Subject: [PATCH 01/14] Add BTree Node Repository --- src/tree/btree_node_repository.rs | 322 ++++++++++++++++++++++++++++++ src/tree/error.rs | 7 + 2 files changed, 329 insertions(+) create mode 100644 src/tree/btree_node_repository.rs create mode 100644 src/tree/error.rs diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs new file mode 100644 index 0000000..d9a9fe6 --- /dev/null +++ b/src/tree/btree_node_repository.rs @@ -0,0 +1,322 @@ +use crate::tree::error::NodeRepositoryError; +use crate::tree::node::MaterializedNode; +use crate::tree::pointer::Pointer; +use crate::tree::traits::{LocatableNode, NodeRepository}; +use phenopackets::schema::v2::{Cohort, Family, Phenopacket}; +use std::any::{Any, TypeId}; +use std::cell::Cell; +use std::collections::{BTreeMap, HashMap}; +use std::ops::Range; + +pub(crate) struct ScopeMappings { + scope_by_type_id: HashMap, + max_scope: Cell, +} + +impl ScopeMappings { + pub(crate) fn new() -> Self { + let mut type_id_by_scope: HashMap = HashMap::new(); + type_id_by_scope.insert(TypeId::of::(), 0u8); + type_id_by_scope.insert(TypeId::of::(), 1u8); + type_id_by_scope.insert(TypeId::of::(), 1u8); + + Self { + scope_by_type_id: type_id_by_scope, + max_scope: Cell::from(0u8), + } + } + + pub fn get_scope(&self, type_id: &TypeId) -> Option { + self.scope_by_type_id.get(type_id).copied() + } + + pub fn get_type_id(&self, scope: &u8) -> Option<&TypeId> { + self.scope_by_type_id + .iter() + .find_map(|(type_id, v)| if v == scope { Some(type_id) } else { None }) + } + + pub fn is_scope_boundary(&self, type_id: &TypeId) -> bool { + self.scope_by_type_id.contains_key(type_id) + } + + pub fn derive_scope(&self, path: &str, type_id: &TypeId) -> u8 { + if let Some(scope) = self.get_scope(type_id) { + let current_max = self.max_scope.get(); + self.max_scope.set(current_max.max(scope)); + } + + let phenopacket_type_id = TypeId::of::(); + let case_scope = self.scope_by_type_id.get(&phenopacket_type_id).unwrap(); + + if &phenopacket_type_id == type_id { + return *case_scope; + } + + if path.contains("members") + || path.contains("relatives") + || path.contains("proband") + // This is needed to know, when we only look at a single phenopacket. + // Since, we are iterating the phenopacket tree from top to bottom, we will always find top level structures + // that are above the phenopacket, if not we can assume, that we are only looking at a single one + || self.max_scope.get() == *case_scope + { + self.get_scope(&TypeId::of::()) + .expect("Should always exist") + } else { + self.get_scope(&TypeId::of::()) + .expect("Should always exist") + } + } +} + +struct NodeEntry { + type_id: TypeId, + scope: u8, + is_scope_boundary: bool, + inner: Box, +} + +pub(crate) struct BTreeNodeRepository { + node_store: BTreeMap, + span_store: BTreeMap>, + scope_mappings: ScopeMappings, +} + +impl BTreeNodeRepository { + pub(crate) fn new() -> Self { + Self { + node_store: BTreeMap::new(), + span_store: BTreeMap::new(), + scope_mappings: ScopeMappings::new(), + } + } + + fn get_subtree_spans(&self, root_path: &str) -> HashMap> { + self.span_store + .range::(root_path.to_string()..) + .take_while(|(k, _)| k.starts_with(root_path)) + .map(|(p, r)| (Pointer::new(p.as_str()), r.clone())) + .collect() + } + + fn cast_entry( + &self, + path: &str, + entry: &NodeEntry, + ) -> Result, NodeRepositoryError> + where + T: Clone + 'static, + { + let content_ref = entry.inner.downcast_ref::().ok_or_else(|| { + NodeRepositoryError::CantReinstantiateNode( + path.to_string(), + std::any::type_name::().to_string(), + ) + })?; + + let content = content_ref.clone(); + let spans = self.get_subtree_spans(path); + + Ok(MaterializedNode::new(content, spans, Pointer::new(path))) + } +} + +impl NodeRepository for BTreeNodeRepository { + fn insert(&mut self, node: MaterializedNode) -> Result<(), NodeRepositoryError> { + let type_id = TypeId::of::(); + let node_path = node.pointer().position().to_string(); + + let scope = self + .scope_mappings + .derive_scope(node_path.as_str(), &type_id); + let is_scope_boundary = self.scope_mappings.is_scope_boundary(&type_id); + + for (ptr, span) in node.spans() { + self.span_store + .entry(ptr.position().to_string()) + .or_insert_with(|| span.clone()); + } + + let entry = NodeEntry { + type_id, + scope, + is_scope_boundary, + inner: Box::new(node.inner), + }; + + self.node_store.insert(node_path.to_string(), entry); + + Ok(()) + } + + fn get_all(&self) -> Result>, NodeRepositoryError> + where + T: Clone + 'static, + { + let target_type = TypeId::of::(); + + let nodes = self + .node_store + .iter() + .filter(|(_, entry)| entry.type_id == target_type) + .map(|(path, entry)| self.cast_entry::(path.as_str(), entry)) + .collect::>, NodeRepositoryError>>()?; + + Ok(nodes) + } + + fn get_nodes_in_scope( + &self, + scope: u8, + ) -> Result>, NodeRepositoryError> + where + T: Clone + 'static, + { + let target_type = TypeId::of::(); + + let nodes = self + .node_store + .iter() + .filter(|(_, entry)| entry.type_id == target_type && entry.scope == scope) + .map(|(path, entry)| self.cast_entry::(path, entry)) + .collect::>, NodeRepositoryError>>()?; + + Ok(nodes) + } + + fn get_nodes_for_scope_per_top_level_element( + &self, + scope: u8, + ) -> Result>>, NodeRepositoryError> + where + T: Clone + 'static, + { + let target_type = TypeId::of::(); + + let top_levels: Vec<&String> = self + .node_store + .iter() + .filter(|(_, entry)| entry.is_scope_boundary && entry.scope == scope) + .map(|(path, _)| path) + .collect(); + + let mut output = Vec::new(); + + for tl_path in top_levels { + let children = self + .node_store + .range::(tl_path.to_string()..) + .take_while(|(k, _)| k.starts_with(tl_path)) + .filter(|(_, entry)| entry.type_id == target_type && entry.scope == scope) + .map(|(path, entry)| self.cast_entry::(path, entry)) + .collect::>, NodeRepositoryError>>()?; + + if !children.is_empty() { + output.push(children); + } + } + + Ok(output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::materializer::NodeMaterializer; + use crate::tree::abstract_pheno_tree::AbstractTreeTraversal; + use crate::tree::pointer::Pointer; + use phenopackets::schema::v2::core::{MetaData, OntologyClass, Resource}; + use phenopackets::schema::v2::{Cohort, Phenopacket}; + use std::collections::HashMap; + use std::fs; + use std::path::PathBuf; + + fn test_cohort() -> Cohort { + let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets"); + + let json_phenopacket_path = assets_dir.join("phenopacket.json"); + let phenostr = fs::read_to_string(json_phenopacket_path).unwrap(); + + let pp: Phenopacket = serde_json::from_str(&phenostr).unwrap(); + + Cohort { + id: "Some".to_string(), + description: "".to_string(), + members: vec![pp.clone(), pp.clone()], + files: vec![], + meta_data: Some(MetaData { + created: None, + created_by: "Patrick".to_string(), + submitted_by: "Patrick".to_string(), + resources: vec![Resource { + id: "1".to_string(), + name: "HP".to_string(), + url: "www.example.com".to_string(), + version: "2020-10-10".to_string(), + namespace_prefix: "hp".to_string(), + iri_prefix: "".to_string(), + }], + updates: vec![], + phenopacket_schema_version: "2".to_string(), + external_references: vec![], + }), + } + } + fn cohort_board() -> BTreeNodeRepository { + let cohort = test_cohort(); + let value = serde_json::to_value(&cohort).unwrap(); + + let tree = AbstractTreeTraversal::new(value, HashMap::new()); + let mut repo = BTreeNodeRepository::new(); + + let mat = NodeMaterializer; + for node in tree.traverse() { + mat.materialize_nodes(&node, &mut repo); + } + repo + } + + #[test] + fn test_insert() { + let mut repo = BTreeNodeRepository::new(); + + let node = MaterializedNode::new( + OntologyClass { + id: "HP:0000001".to_string(), + label: "All".to_string(), + }, + HashMap::new(), + Pointer::at_phenotypes().down("0/type").clone(), + ); + repo.insert(node).unwrap(); + } + + #[test] + fn test_get_nodes_for_scope_per_top_level_element() { + let repo = cohort_board(); + let retrieved = repo + .get_nodes_for_scope_per_top_level_element::(0u8) + .unwrap(); + + assert_eq!(retrieved.len(), 2); + } + + #[test] + fn test_get_all_nodes() { + let repo = cohort_board(); + let test_cohort = test_cohort(); + let retrieved = repo.get_all::().unwrap(); + + let mut n_resources = test_cohort.meta_data.unwrap().resources.len(); + + for pp in test_cohort.members { + n_resources += pp.meta_data.unwrap().resources.len() + } + + assert_eq!(retrieved.len(), n_resources); + } +} diff --git a/src/tree/error.rs b/src/tree/error.rs new file mode 100644 index 0000000..a9285d8 --- /dev/null +++ b/src/tree/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NodeRepositoryError { + #[error("Cant Reinstantiate Node at '{0}' and type '{1}'.")] + CantReinstantiateNode(String, String), +} From 36982c460d890b3500c078929bf412638ee29dc1 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:43:17 +0100 Subject: [PATCH 02/14] Parse Cohort Node --- src/parsing/parseable_nodes.rs | 16 +++++++++++++++- src/tree/mod.rs | 2 ++ src/tree/node.rs | 4 ++++ src/tree/traits.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index e212c0f..54a970f 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -1,12 +1,26 @@ use crate::parsing::traits::ParsableNode; use crate::tree::node::DynamicNode; use crate::tree::traits::LocatableNode; -use phenopackets::schema::v2::Phenopacket; use phenopackets::schema::v2::core::{ Diagnosis, Disease, OntologyClass, PhenotypicFeature, Resource, VitalStatus, }; +use phenopackets::schema::v2::{Cohort, Phenopacket}; use serde_json::Value; +impl ParsableNode for Cohort { + fn parse(node: &DynamicNode) -> Option { + if let Value::Object(map) = &node.inner + && map.contains_key("id") + && map.contains_key("members") + && let Ok(cohort) = serde_json::from_value::(node.inner.clone()) + { + Some(cohort) + } else { + None + } + } +} + impl ParsableNode for OntologyClass { fn parse(node: &DynamicNode) -> Option { if let Value::Object(map) = &node.inner diff --git a/src/tree/mod.rs b/src/tree/mod.rs index 2912779..3705fe6 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -1,4 +1,6 @@ pub(crate) mod abstract_pheno_tree; +pub(crate) mod btree_node_repository; +mod error; pub mod node; pub mod node_repository; pub mod pointer; diff --git a/src/tree/node.rs b/src/tree/node.rs index cbf1431..832a99e 100644 --- a/src/tree/node.rs +++ b/src/tree/node.rs @@ -64,6 +64,10 @@ impl MaterializedNode { dyn_node.pointer().clone(), ) } + + pub(crate) fn spans(&self) -> &HashMap> { + &self.spans + } } impl RetrievableNode for MaterializedNode { diff --git a/src/tree/traits.rs b/src/tree/traits.rs index 5701813..f3363aa 100644 --- a/src/tree/traits.rs +++ b/src/tree/traits.rs @@ -1,3 +1,5 @@ +use crate::tree::error::NodeRepositoryError; +use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; use serde_json::Value; use std::borrow::Cow; @@ -15,3 +17,28 @@ pub trait LocatableNode { pub trait RetrievableNode { fn value_at(&self, ptr: &Pointer) -> Option>; } + +pub(crate) trait NodeRepository { + fn insert( + &mut self, + node: MaterializedNode, + ) -> Result<(), NodeRepositoryError>; + + // Gets all nodes of a type + // Example: Check if all CURIE id's are formatted correctly + fn get_all(&self) -> Result>, NodeRepositoryError>; + + // Gets all nodes of a type in a scope + // Example: Get all nodes of the phenopacket scope + all resources of the cohort. Check if pp resources are in cohort + fn get_nodes_in_scope( + &self, + scope: u8, + ) -> Result>, NodeRepositoryError>; + + // All nodes of a type for cases per case + // Example: Check if all curie id's are represented in the resources in a phenopacket + fn get_nodes_for_scope_per_top_level_element( + &self, + scope: u8, + ) -> Result>>, NodeRepositoryError>; +} From db2d4ade26f39dd9f6a8fd5dd70ce4cdcc997668 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:45:50 +0100 Subject: [PATCH 03/14] NodeMaterializer: Materialize Cohort --- src/materializer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/materializer.rs b/src/materializer.rs index 4434caf..eaeadd2 100644 --- a/src/materializer.rs +++ b/src/materializer.rs @@ -3,16 +3,18 @@ use crate::tree::node::{DynamicNode, MaterializedNode}; use crate::tree::node_repository::NodeRepository; use crate::tree::traits::LocatableNode; use log::error; -use phenopackets::schema::v2::Phenopacket; use phenopackets::schema::v2::core::{ Diagnosis, Disease, OntologyClass, PhenotypicFeature, Resource, VitalStatus, }; +use phenopackets::schema::v2::{Cohort, Phenopacket}; pub(crate) struct NodeMaterializer; impl NodeMaterializer { pub fn materialize_nodes(&mut self, dyn_node: &DynamicNode, repo: &mut NodeRepository) { - if let Some(oc) = OntologyClass::parse(dyn_node) { + if let Some(cohort) = Cohort::parse(dyn_node) { + Self::push_to_repo(cohort, dyn_node, repo); + } else if let Some(oc) = OntologyClass::parse(dyn_node) { Self::push_to_repo(oc, dyn_node, repo); } else if let Some(pf) = PhenotypicFeature::parse(dyn_node) { Self::push_to_repo(pf, dyn_node, repo); From 1a52d95013f75d2b8ae83880f22e11fbd65d3f8f Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:48:16 +0100 Subject: [PATCH 04/14] Better message --- src/tree/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree/error.rs b/src/tree/error.rs index a9285d8..2644f96 100644 --- a/src/tree/error.rs +++ b/src/tree/error.rs @@ -2,6 +2,6 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum NodeRepositoryError { - #[error("Cant Reinstantiate Node at '{0}' and type '{1}'.")] + #[error("Cant Reinstantiate Node at '{0}' with type '{1}'.")] CantReinstantiateNode(String, String), } From 65ea9f850661505507707c61780d410e27368c77 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:49:35 +0100 Subject: [PATCH 05/14] Better message --- src/tree/btree_node_repository.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index d9a9fe6..1e586a4 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -274,9 +274,10 @@ mod tests { let mut repo = BTreeNodeRepository::new(); let mat = NodeMaterializer; - for node in tree.traverse() { + // TODO: Change interface of materialize_nodes to take an impl NodeRepository trait + /*for node in tree.traverse() { mat.materialize_nodes(&node, &mut repo); - } + }*/ repo } From 967d4c652efd913e3a063b7ce9c64190befa3786 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 12:52:43 +0100 Subject: [PATCH 06/14] Linting --- src/tree/btree_node_repository.rs | 21 ++++++++------------- src/tree/node.rs | 1 + src/tree/traits.rs | 1 + 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index 1e586a4..a0b7f35 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use crate::tree::error::NodeRepositoryError; use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; @@ -30,12 +31,6 @@ impl ScopeMappings { self.scope_by_type_id.get(type_id).copied() } - pub fn get_type_id(&self, scope: &u8) -> Option<&TypeId> { - self.scope_by_type_id - .iter() - .find_map(|(type_id, v)| if v == scope { Some(type_id) } else { None }) - } - pub fn is_scope_boundary(&self, type_id: &TypeId) -> bool { self.scope_by_type_id.contains_key(type_id) } @@ -224,8 +219,6 @@ impl NodeRepository for BTreeNodeRepository { #[cfg(test)] mod tests { use super::*; - use crate::materializer::NodeMaterializer; - use crate::tree::abstract_pheno_tree::AbstractTreeTraversal; use crate::tree::pointer::Pointer; use phenopackets::schema::v2::core::{MetaData, OntologyClass, Resource}; use phenopackets::schema::v2::{Cohort, Phenopacket}; @@ -267,18 +260,20 @@ mod tests { } } fn cohort_board() -> BTreeNodeRepository { - let cohort = test_cohort(); + /*let cohort = test_cohort(); let value = serde_json::to_value(&cohort).unwrap(); - let tree = AbstractTreeTraversal::new(value, HashMap::new()); - let mut repo = BTreeNodeRepository::new(); + let tree = AbstractTreeTraversal::new(value, HashMap::new()); + let repo = BTreeNodeRepository::new(); let mat = NodeMaterializer; // TODO: Change interface of materialize_nodes to take an impl NodeRepository trait - /*for node in tree.traverse() { + for node in tree.traverse() { mat.materialize_nodes(&node, &mut repo); - }*/ + } repo + */ + BTreeNodeRepository::new() } #[test] diff --git a/src/tree/node.rs b/src/tree/node.rs index 832a99e..5bbcfc8 100644 --- a/src/tree/node.rs +++ b/src/tree/node.rs @@ -65,6 +65,7 @@ impl MaterializedNode { ) } + #[allow(dead_code)] pub(crate) fn spans(&self) -> &HashMap> { &self.spans } diff --git a/src/tree/traits.rs b/src/tree/traits.rs index f3363aa..c0111fb 100644 --- a/src/tree/traits.rs +++ b/src/tree/traits.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use crate::tree::error::NodeRepositoryError; use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; From 494b04a5d1041a043e3373dff3f60a31c058d215 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Mon, 8 Dec 2025 13:22:00 +0100 Subject: [PATCH 07/14] Stuff --- src/tree/btree_node_repository.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index a0b7f35..72a5919 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -22,8 +22,13 @@ impl ScopeMappings { type_id_by_scope.insert(TypeId::of::(), 1u8); Self { + max_scope: Cell::from( + *type_id_by_scope + .values() + .min() + .expect("Value was just assigned"), + ), scope_by_type_id: type_id_by_scope, - max_scope: Cell::from(0u8), } } @@ -42,19 +47,15 @@ impl ScopeMappings { } let phenopacket_type_id = TypeId::of::(); - let case_scope = self.scope_by_type_id.get(&phenopacket_type_id).unwrap(); - - if &phenopacket_type_id == type_id { - return *case_scope; - } if path.contains("members") || path.contains("relatives") || path.contains("proband") // This is needed to know, when we only look at a single phenopacket. // Since, we are iterating the phenopacket tree from top to bottom, we will always find top level structures - // that are above the phenopacket, if not we can assume, that we are only looking at a single one - || self.max_scope.get() == *case_scope + // that are above the phenopacket, if not we can assume, that we are only looking at a single one. + || self.max_scope.get() == *self.scope_by_type_id.get(&phenopacket_type_id).unwrap() + || type_id == &phenopacket_type_id { self.get_scope(&TypeId::of::()) .expect("Should always exist") From 6a68a875978d437203305a83168633b8caa197cd Mon Sep 17 00:00:00 2001 From: Rouven Reuter <49242091+SmartMonkey-git@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:39:55 +0100 Subject: [PATCH 08/14] Scopes (#77) * Introduce scope concepts * typo * Retrieving data (#80) * Stable * Deref * Better inputs * Clean up * Rename generic T to NodeType * Linting * Clean up * Fix bug * Implement rules (#81) --- src/materializer.rs | 14 +- src/phenolint.rs | 4 +- src/rules/curies/curie_format_rule.rs | 7 +- .../disease_consistency_rule.rs | 56 +++--- src/rules/resources.rs | 49 ++--- src/rules/rule_registry.rs | 7 +- src/rules/traits.rs | 26 ++- src/tree/btree_node_repository.rs | 125 ++++--------- src/tree/mod.rs | 3 +- src/tree/node.rs | 15 +- src/tree/node_repository.rs | 98 ---------- src/tree/querying/mod.rs | 3 + src/tree/querying/presentation.rs | 71 ++++++++ src/tree/querying/queries.rs | 128 +++++++++++++ src/tree/querying/traits.rs | 55 ++++++ src/tree/scopes.rs | 172 ++++++++++++++++++ src/tree/traits.rs | 7 +- tests/test_custom_rule.rs | 7 +- 18 files changed, 567 insertions(+), 280 deletions(-) delete mode 100644 src/tree/node_repository.rs create mode 100644 src/tree/querying/mod.rs create mode 100644 src/tree/querying/presentation.rs create mode 100644 src/tree/querying/queries.rs create mode 100644 src/tree/querying/traits.rs create mode 100644 src/tree/scopes.rs diff --git a/src/materializer.rs b/src/materializer.rs index eaeadd2..1828bb6 100644 --- a/src/materializer.rs +++ b/src/materializer.rs @@ -1,7 +1,6 @@ use crate::parsing::traits::ParsableNode; use crate::tree::node::{DynamicNode, MaterializedNode}; -use crate::tree::node_repository::NodeRepository; -use crate::tree::traits::LocatableNode; +use crate::tree::traits::{LocatableNode, NodeRepository}; use log::error; use phenopackets::schema::v2::core::{ Diagnosis, Disease, OntologyClass, PhenotypicFeature, Resource, VitalStatus, @@ -11,7 +10,7 @@ use phenopackets::schema::v2::{Cohort, Phenopacket}; pub(crate) struct NodeMaterializer; impl NodeMaterializer { - pub fn materialize_nodes(&mut self, dyn_node: &DynamicNode, repo: &mut NodeRepository) { + pub fn materialize_nodes(&mut self, dyn_node: &DynamicNode, repo: &mut impl NodeRepository) { if let Some(cohort) = Cohort::parse(dyn_node) { Self::push_to_repo(cohort, dyn_node, repo); } else if let Some(oc) = OntologyClass::parse(dyn_node) { @@ -33,12 +32,13 @@ impl NodeMaterializer { }; } - fn push_to_repo( - materialized: T, + fn push_to_repo( + materialized: NodeType, dyn_node: &DynamicNode, - board: &mut NodeRepository, + board: &mut impl NodeRepository, ) { let node = MaterializedNode::from_dynamic(materialized, dyn_node); - board.insert(node); + // TODO: Error throwing + board.insert(node).expect("Unable to insert node"); } } diff --git a/src/phenolint.rs b/src/phenolint.rs index 1a374d3..cf1d1f2 100644 --- a/src/phenolint.rs +++ b/src/phenolint.rs @@ -14,13 +14,13 @@ use crate::schema_validation::validator::PhenopacketSchemaValidator; use crate::traits::Lint; use crate::tree::abstract_pheno_tree::AbstractTreeTraversal; use crate::tree::node::DynamicNode; -use crate::tree::node_repository::NodeRepository; use crate::tree::pointer::Pointer; use log::{error, warn}; use phenopackets::schema::v2::Phenopacket; use prost::Message; use serde_json::Value; +use crate::tree::btree_node_repository::BTreeNodeRepository; use std::fs; use std::path::PathBuf; @@ -71,7 +71,7 @@ impl Lint for Phenolint { let root_node = DynamicNode::new(&values, &spans, Pointer::at_root()); let apt = AbstractTreeTraversal::new(values, spans); - let mut node_repo: NodeRepository = NodeRepository::new(); + let mut node_repo = BTreeNodeRepository::new(); for node in apt.traverse() { self.node_materializer diff --git a/src/rules/curies/curie_format_rule.rs b/src/rules/curies/curie_format_rule.rs index a5e3a3c..41bd08d 100644 --- a/src/rules/curies/curie_format_rule.rs +++ b/src/rules/curies/curie_format_rule.rs @@ -10,7 +10,8 @@ use crate::report::traits::{CompileReport, RegisterableReport, ReportFromContext use crate::rules::rule_registration::RuleRegistration; use crate::rules::traits::RuleMetaData; use crate::rules::traits::{LintRule, RuleCheck, RuleFromContext}; -use crate::tree::node_repository::List; +use crate::tree::querying::presentation::Flattened; +use crate::tree::querying::queries::convenience::All; use crate::tree::traits::{LocatableNode, Node}; use phenolint_macros::{register_report, register_rule}; use phenopackets::schema::v2::core::OntologyClass; @@ -38,9 +39,9 @@ impl RuleFromContext for CurieFormatRule { } impl RuleCheck for CurieFormatRule { - type Data<'a> = List<'a, OntologyClass>; + type Query = All; - fn check(&self, data: Self::Data<'_>) -> Vec { + fn check(&self, data: Flattened) -> Vec { let mut violations = vec![]; for node in data.0.iter() { diff --git a/src/rules/interpretation/disease_consistency_rule.rs b/src/rules/interpretation/disease_consistency_rule.rs index cd2eafe..5a6a763 100644 --- a/src/rules/interpretation/disease_consistency_rule.rs +++ b/src/rules/interpretation/disease_consistency_rule.rs @@ -14,8 +14,9 @@ use crate::report::traits::{CompileReport, RegisterableReport, ReportFromContext use crate::rules::rule_registration::RuleRegistration; use crate::rules::traits::RuleMetaData; use crate::rules::traits::{LintRule, RuleCheck, RuleFromContext}; -use crate::tree::node_repository::List; use crate::tree::pointer::Pointer; +use crate::tree::querying::presentation::Grouped; +use crate::tree::querying::queries::convenience::GroupedIndividuals; use crate::tree::traits::{LocatableNode, Node}; use phenolint_macros::{register_patch, register_report, register_rule}; use phenopackets::schema::v2::core::{Diagnosis, Disease, OntologyClass}; @@ -38,34 +39,37 @@ impl RuleFromContext for DiseaseConsistencyRule { } impl RuleCheck for DiseaseConsistencyRule { - type Data<'a> = (List<'a, Diagnosis>, List<'a, Disease>); + type Query = (GroupedIndividuals, GroupedIndividuals); - fn check(&self, data: Self::Data<'_>) -> Vec { + fn check(&self, data: (Grouped, Grouped)) -> Vec { let mut violations = vec![]; - let disease_terms: Vec<(&str, &str)> = data - .1 - .iter() - .filter_map(|disease| { - disease - .inner - .term - .as_ref() - .map(|oc| (oc.id.as_str(), oc.label.as_str())) - }) - .collect(); - - for diagnosis in data.0.iter() { - if let Some(oc) = &diagnosis.inner.disease - && !disease_terms.contains(&(oc.id.as_str(), oc.label.as_str())) - { - violations.push(LintViolation::new( - ViolationSeverity::Warning, - LintRule::rule_id(self), - NonEmptyVec::with_single_entry( - diagnosis.pointer().clone().down("disease").clone(), - ), - )) + let (diagnosis_groups, diseases_group) = data; + + for (diagnosis, diseases) in diagnosis_groups.0.iter().zip(diseases_group.0) { + let disease_terms: Vec<(&str, &str)> = diseases + .iter() + .filter_map(|disease| { + disease + .inner + .term + .as_ref() + .map(|oc| (oc.id.as_str(), oc.label.as_str())) + }) + .collect(); + + for diagnosis in diagnosis.iter() { + if let Some(oc) = &diagnosis.inner.disease + && !disease_terms.contains(&(oc.id.as_str(), oc.label.as_str())) + { + violations.push(LintViolation::new( + ViolationSeverity::Warning, + LintRule::rule_id(self), + NonEmptyVec::with_single_entry( + diagnosis.pointer().clone().down("disease").clone(), + ), + )) + } } } diff --git a/src/rules/resources.rs b/src/rules/resources.rs index 315498a..a7a3d46 100644 --- a/src/rules/resources.rs +++ b/src/rules/resources.rs @@ -8,8 +8,9 @@ use crate::report::traits::RuleReport; use crate::report::traits::{CompileReport, RegisterableReport, ReportFromContext}; use crate::rules::rule_registration::RuleRegistration; use crate::rules::traits::{LintRule, RuleCheck, RuleFromContext, RuleMetaData}; -use crate::tree::node_repository::List; use crate::tree::pointer::Pointer; +use crate::tree::querying::presentation::Grouped; +use crate::tree::querying::queries::convenience::GroupedIndividuals; use crate::tree::traits::{LocatableNode, Node}; use phenolint_macros::{register_report, register_rule}; use phenopackets::schema::v2::core::{OntologyClass, Resource}; @@ -35,28 +36,34 @@ impl RuleFromContext for CuriesHaveResourcesRule { } impl RuleCheck for CuriesHaveResourcesRule { - type Data<'a> = (List<'a, OntologyClass>, List<'a, Resource>); - - fn check(&self, data: Self::Data<'_>) -> Vec { - let known_prefixes: HashSet<_> = data - .1 - .iter() - .map(|r| r.inner.namespace_prefix.as_str()) - .collect(); + type Query = ( + GroupedIndividuals, + GroupedIndividuals, + ); + fn check(&self, data: (Grouped, Grouped)) -> Vec { + let (ontology_classes, resources) = data; let mut violations = vec![]; - for node in data.0.iter() { - if let Some(prefix) = find_prefix(node.inner.id.as_str()) - && !known_prefixes.contains(prefix) - { - violations.push(LintViolation::new( - ViolationSeverity::Error, - LintRule::rule_id(self), - node.pointer().clone().into(), // <- warns about the ontology class itself - )); + for (o, r) in ontology_classes.0.iter().zip(resources.0) { + let known_prefixes: HashSet<_> = r + .iter() + .map(|r| r.inner.namespace_prefix.as_str()) + .collect(); + + for node in o.iter() { + if let Some(prefix) = find_prefix(node.inner.id.as_str()) + && !known_prefixes.contains(prefix) + { + violations.push(LintViolation::new( + ViolationSeverity::Error, + LintRule::rule_id(self), + node.pointer().clone().into(), // <- warns about the ontology class itself + )); + } } } + violations } } @@ -66,8 +73,8 @@ mod test_curies_have_resources { use crate::rules::resources::CuriesHaveResourcesRule; use crate::rules::traits::{RuleCheck, RuleMetaData}; use crate::tree::node::MaterializedNode; - use crate::tree::node_repository::List; use crate::tree::pointer::Pointer; + use crate::tree::querying::presentation::Grouped; use phenopackets::schema::v2::core::OntologyClass; #[test] @@ -82,8 +89,8 @@ mod test_curies_have_resources { Default::default(), Pointer::new("/phenotypicFeatures/0/type"), )]; - let resources = []; - let data = (List(&ocs), List(&resources)); + let resources = vec![vec![]]; + let data = (Grouped(vec![ocs.to_vec()]), Grouped(resources)); let violations = rule.check(data); diff --git a/src/rules/rule_registry.rs b/src/rules/rule_registry.rs index 9bd3534..aa1296f 100644 --- a/src/rules/rule_registry.rs +++ b/src/rules/rule_registry.rs @@ -80,7 +80,8 @@ mod tests { use crate::rules::traits::RuleCheck; use crate::rules::traits::RuleFromContext; use crate::rules::traits::RuleMetaData; - use crate::tree::node_repository::List; + use crate::tree::querying::presentation::Flattened; + use crate::tree::querying::queries::convenience::All; use phenolint_macros::register_rule; use phenopackets::schema::v2::core::OntologyClass; use rstest::rstest; @@ -100,9 +101,9 @@ mod tests { } } impl RuleCheck for TestRule { - type Data<'a> = List<'a, OntologyClass>; + type Query = All; - fn check(&self, _: Self::Data<'_>) -> Vec { + fn check(&self, _: Flattened) -> Vec { todo!() } } diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 97173cb..656729b 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,15 +1,17 @@ use crate::LinterContext; use crate::diagnostics::LintViolation; use crate::error::FromContextError; -use crate::tree::node_repository::NodeRepository; +use crate::tree::btree_node_repository::BTreeNodeRepository; +use crate::tree::querying::traits::QueryStrategy; -pub trait LintRule: RuleFromContext + Send + Sync { +pub trait LintRule: Send + Sync { fn rule_id(&self) -> &str; - fn check_erased(&self, board: &NodeRepository) -> Vec; + // Needs to be concrete type, because NodeRepository trait is not dyn compatible :( + fn check_erased(&self, board: &BTreeNodeRepository) -> Vec; } -pub trait RuleMetaData: Send + Sync { +pub trait RuleMetaData { fn rule_id(&self) -> &str; } @@ -20,28 +22,22 @@ pub trait RuleFromContext { } pub trait RuleCheck: Send + Sync + 'static { - type Data<'a>: LintData<'a> + ?Sized; - fn check(&self, data: Self::Data<'_>) -> Vec; + type Query: QueryStrategy; + fn check(&self, data: ::Output) -> Vec; } impl LintRule for T where T: RuleCheck + RuleFromContext + RuleMetaData, - for<'a> ::Data<'a>: Sized, + for<'a> ::Query: Sized, { fn rule_id(&self) -> &str { self.rule_id() } - fn check_erased(&self, board: &NodeRepository) -> Vec { - let data = ::Data::fetch(board); + fn check_erased(&self, board: &BTreeNodeRepository) -> Vec { + let data = ::Query::query(board); self.check(data) } } - -pub trait LintData<'a> { - fn fetch(board: &'a NodeRepository) -> Self - where - Self: Sized; -} diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index 72a5919..dcf31a2 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -2,78 +2,20 @@ use crate::tree::error::NodeRepositoryError; use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; +use crate::tree::scopes::{ScopeLayer, ScopeMappings}; use crate::tree::traits::{LocatableNode, NodeRepository}; -use phenopackets::schema::v2::{Cohort, Family, Phenopacket}; use std::any::{Any, TypeId}; -use std::cell::Cell; use std::collections::{BTreeMap, HashMap}; use std::ops::Range; -pub(crate) struct ScopeMappings { - scope_by_type_id: HashMap, - max_scope: Cell, -} - -impl ScopeMappings { - pub(crate) fn new() -> Self { - let mut type_id_by_scope: HashMap = HashMap::new(); - type_id_by_scope.insert(TypeId::of::(), 0u8); - type_id_by_scope.insert(TypeId::of::(), 1u8); - type_id_by_scope.insert(TypeId::of::(), 1u8); - - Self { - max_scope: Cell::from( - *type_id_by_scope - .values() - .min() - .expect("Value was just assigned"), - ), - scope_by_type_id: type_id_by_scope, - } - } - - pub fn get_scope(&self, type_id: &TypeId) -> Option { - self.scope_by_type_id.get(type_id).copied() - } - - pub fn is_scope_boundary(&self, type_id: &TypeId) -> bool { - self.scope_by_type_id.contains_key(type_id) - } - - pub fn derive_scope(&self, path: &str, type_id: &TypeId) -> u8 { - if let Some(scope) = self.get_scope(type_id) { - let current_max = self.max_scope.get(); - self.max_scope.set(current_max.max(scope)); - } - - let phenopacket_type_id = TypeId::of::(); - - if path.contains("members") - || path.contains("relatives") - || path.contains("proband") - // This is needed to know, when we only look at a single phenopacket. - // Since, we are iterating the phenopacket tree from top to bottom, we will always find top level structures - // that are above the phenopacket, if not we can assume, that we are only looking at a single one. - || self.max_scope.get() == *self.scope_by_type_id.get(&phenopacket_type_id).unwrap() - || type_id == &phenopacket_type_id - { - self.get_scope(&TypeId::of::()) - .expect("Should always exist") - } else { - self.get_scope(&TypeId::of::()) - .expect("Should always exist") - } - } -} - struct NodeEntry { type_id: TypeId, - scope: u8, + scope: ScopeLayer, is_scope_boundary: bool, inner: Box, } -pub(crate) struct BTreeNodeRepository { +pub struct BTreeNodeRepository { node_store: BTreeMap, span_store: BTreeMap>, scope_mappings: ScopeMappings, @@ -96,18 +38,18 @@ impl BTreeNodeRepository { .collect() } - fn cast_entry( + fn cast_entry( &self, path: &str, entry: &NodeEntry, - ) -> Result, NodeRepositoryError> + ) -> Result, NodeRepositoryError> where - T: Clone + 'static, + NodeType: Clone + 'static, { - let content_ref = entry.inner.downcast_ref::().ok_or_else(|| { + let content_ref = entry.inner.downcast_ref::().ok_or_else(|| { NodeRepositoryError::CantReinstantiateNode( path.to_string(), - std::any::type_name::().to_string(), + std::any::type_name::().to_string(), ) })?; @@ -119,8 +61,11 @@ impl BTreeNodeRepository { } impl NodeRepository for BTreeNodeRepository { - fn insert(&mut self, node: MaterializedNode) -> Result<(), NodeRepositoryError> { - let type_id = TypeId::of::(); + fn insert( + &mut self, + node: MaterializedNode, + ) -> Result<(), NodeRepositoryError> { + let type_id = TypeId::of::(); let node_path = node.pointer().position().to_string(); let scope = self @@ -146,49 +91,49 @@ impl NodeRepository for BTreeNodeRepository { Ok(()) } - fn get_all(&self) -> Result>, NodeRepositoryError> + fn get_all(&self) -> Result>, NodeRepositoryError> where - T: Clone + 'static, + NodeType: Clone + 'static, { - let target_type = TypeId::of::(); + let target_type = TypeId::of::(); let nodes = self .node_store .iter() .filter(|(_, entry)| entry.type_id == target_type) - .map(|(path, entry)| self.cast_entry::(path.as_str(), entry)) - .collect::>, NodeRepositoryError>>()?; + .map(|(path, entry)| self.cast_entry::(path.as_str(), entry)) + .collect::>, NodeRepositoryError>>()?; Ok(nodes) } - fn get_nodes_in_scope( + fn get_nodes_in_scope( &self, - scope: u8, - ) -> Result>, NodeRepositoryError> + scope: ScopeLayer, + ) -> Result>, NodeRepositoryError> where - T: Clone + 'static, + NodeType: Clone + 'static, { - let target_type = TypeId::of::(); + let target_type = TypeId::of::(); let nodes = self .node_store .iter() .filter(|(_, entry)| entry.type_id == target_type && entry.scope == scope) - .map(|(path, entry)| self.cast_entry::(path, entry)) - .collect::>, NodeRepositoryError>>()?; + .map(|(path, entry)| self.cast_entry::(path, entry)) + .collect::>, NodeRepositoryError>>()?; Ok(nodes) } - fn get_nodes_for_scope_per_top_level_element( + fn get_nodes_for_scope_per_top_level_element( &self, - scope: u8, - ) -> Result>>, NodeRepositoryError> + scope: ScopeLayer, + ) -> Result>>, NodeRepositoryError> where - T: Clone + 'static, + NodeType: Clone + 'static, { - let target_type = TypeId::of::(); + let target_type = TypeId::of::(); let top_levels: Vec<&String> = self .node_store @@ -205,12 +150,10 @@ impl NodeRepository for BTreeNodeRepository { .range::(tl_path.to_string()..) .take_while(|(k, _)| k.starts_with(tl_path)) .filter(|(_, entry)| entry.type_id == target_type && entry.scope == scope) - .map(|(path, entry)| self.cast_entry::(path, entry)) - .collect::>, NodeRepositoryError>>()?; + .map(|(path, entry)| self.cast_entry::(path, entry)) + .collect::>, NodeRepositoryError>>()?; - if !children.is_empty() { - output.push(children); - } + output.push(children); } Ok(output) @@ -296,7 +239,7 @@ mod tests { fn test_get_nodes_for_scope_per_top_level_element() { let repo = cohort_board(); let retrieved = repo - .get_nodes_for_scope_per_top_level_element::(0u8) + .get_nodes_for_scope_per_top_level_element::(ScopeLayer::Individual) .unwrap(); assert_eq!(retrieved.len(), 2); diff --git a/src/tree/mod.rs b/src/tree/mod.rs index 3705fe6..7b8dd6f 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -2,7 +2,8 @@ pub(crate) mod abstract_pheno_tree; pub(crate) mod btree_node_repository; mod error; pub mod node; -pub mod node_repository; pub mod pointer; +pub mod querying; +mod scopes; pub mod traits; pub(crate) mod utils; diff --git a/src/tree/node.rs b/src/tree/node.rs index 5bbcfc8..1f4e1ec 100644 --- a/src/tree/node.rs +++ b/src/tree/node.rs @@ -38,15 +38,16 @@ impl LocatableNode for DynamicNode { } } -pub struct MaterializedNode { - pub inner: T, +#[derive(Clone, Debug)] +pub struct MaterializedNode { + pub inner: NodeType, spans: HashMap>, pointer: Pointer, } -impl MaterializedNode { +impl MaterializedNode { pub fn new( - materialized_node: T, + materialized_node: NodeType, spans: HashMap>, pointer: Pointer, ) -> Self { @@ -57,7 +58,7 @@ impl MaterializedNode { } } - pub(crate) fn from_dynamic(materialized: T, dyn_node: &DynamicNode) -> Self { + pub(crate) fn from_dynamic(materialized: NodeType, dyn_node: &DynamicNode) -> Self { Self::new( materialized, dyn_node.spans.clone(), @@ -71,7 +72,7 @@ impl MaterializedNode { } } -impl RetrievableNode for MaterializedNode { +impl RetrievableNode for MaterializedNode { fn value_at(&self, ptr: &Pointer) -> Option> { let node_opt = serde_json::to_value(&self.inner).ok()?; let value = node_opt.pointer(ptr.position())?.clone(); @@ -79,7 +80,7 @@ impl RetrievableNode for MaterializedNode { } } -impl LocatableNode for MaterializedNode { +impl LocatableNode for MaterializedNode { fn span_at(&self, ptr: &Pointer) -> Option<&Range> { self.spans.get(ptr) } diff --git a/src/tree/node_repository.rs b/src/tree/node_repository.rs deleted file mode 100644 index d1eeac2..0000000 --- a/src/tree/node_repository.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::rules::traits::LintData; -use crate::tree::node::MaterializedNode; -use crate::tree::pointer::Pointer; -use crate::tree::traits::LocatableNode; - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::ops::Deref; - -#[derive(Default)] -pub struct NodeRepository { - board: HashMap>, -} - -impl NodeRepository { - pub fn new() -> NodeRepository { - NodeRepository { - board: HashMap::new(), - } - } - - fn get_raw(&self) -> &[MaterializedNode] { - self.board - .get(&TypeId::of::()) - .and_then(|b| b.downcast_ref::>>()) - .map(|v| v.as_slice()) - .unwrap_or(&[]) - } - - pub fn insert(&mut self, node: MaterializedNode) { - self.board - .entry(TypeId::of::()) - .or_insert_with(|| Box::new(Vec::>::new())) - .downcast_mut::>>() - .unwrap() - .push(node); - } - - pub fn node_by_pointer(&self, ptr: &Pointer) -> Option<&MaterializedNode> { - for nodes in self.board.values() { - let casted_node = nodes - .downcast_ref::>>() - .expect("Should be downcastable"); - - for node in casted_node.iter() { - if node.pointer() == ptr { - return Some(node); - } - } - } - None - } -} - -pub struct Single<'a, T: 'static>(pub Option<&'a MaterializedNode>); - -impl<'a, T> LintData<'a> for Single<'a, T> { - fn fetch(board: &'a NodeRepository) -> Self { - Single(board.get_raw::().first()) - } -} - -pub struct List<'a, T: 'static>(pub &'a [MaterializedNode]); - -impl<'a, T> Deref for List<'a, T> { - type Target = &'a [MaterializedNode]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, T> LintData<'a> for List<'a, T> { - fn fetch(board: &'a NodeRepository) -> Self { - List(board.get_raw()) - } -} - -impl<'a, A, B> LintData<'a> for (A, B) -where - A: LintData<'a>, - B: LintData<'a>, -{ - fn fetch(board: &'a NodeRepository) -> Self { - (A::fetch(board), B::fetch(board)) - } -} - -impl<'a, A, B, C> LintData<'a> for (A, B, C) -where - A: LintData<'a>, - B: LintData<'a>, - C: LintData<'a>, -{ - fn fetch(board: &'a NodeRepository) -> Self { - (A::fetch(board), B::fetch(board), C::fetch(board)) - } -} diff --git a/src/tree/querying/mod.rs b/src/tree/querying/mod.rs new file mode 100644 index 0000000..6bf3045 --- /dev/null +++ b/src/tree/querying/mod.rs @@ -0,0 +1,3 @@ +pub mod presentation; +pub mod queries; +pub mod traits; diff --git a/src/tree/querying/presentation.rs b/src/tree/querying/presentation.rs new file mode 100644 index 0000000..53f3528 --- /dev/null +++ b/src/tree/querying/presentation.rs @@ -0,0 +1,71 @@ +use crate::tree::node::MaterializedNode; +use crate::tree::querying::traits::QueryPresentation; +use std::ops::Deref; + +pub struct Flattened(pub Vec>); + +impl Deref for Flattened { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl QueryPresentation>> for Flattened { + fn present(query_res: Vec>) -> Self { + Flattened(query_res) + } +} + +impl QueryPresentation>>> for Flattened { + fn present(query_res: Vec>>) -> Self { + Flattened(query_res.into_iter().flatten().collect()) + } +} + +pub struct First(pub Option>); + +impl Deref for First { + type Target = Option>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl QueryPresentation>> for First { + fn present(query_res: Vec>) -> Self { + First(query_res.first().cloned()) + } +} + +impl QueryPresentation>>> for First { + fn present(query_res: Vec>>) -> Self { + for i in query_res { + if let Some(j) = i.into_iter().next() { + return First(Some(j)); + } + } + + First(None) + } +} + +pub struct Grouped(pub Vec>>); + +impl Deref for Grouped { + type Target = Vec>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl QueryPresentation>>> + for Grouped +{ + fn present(query_res: Vec>>) -> Self { + Grouped(query_res) + } +} diff --git a/src/tree/querying/queries.rs b/src/tree/querying/queries.rs new file mode 100644 index 0000000..b7e91b0 --- /dev/null +++ b/src/tree/querying/queries.rs @@ -0,0 +1,128 @@ +use crate::tree::node::MaterializedNode; + +use crate::tree::traits::NodeRepository; + +use crate::tree::querying::traits::{QueryPresentation, QueryStrategy, ScopeDefinition}; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct QueryAllNodes(PhantomData<(Presentation, NodeType)>); + +impl>>> + QueryStrategy for QueryAllNodes +{ + type Output = Presentation; + fn query(node_repo: &impl NodeRepository) -> Self::Output { + Presentation::present(node_repo.get_all::().unwrap_or_default()) + } +} +#[derive(Debug)] +pub struct QueryNodesInScope( + PhantomData<(Scope, Presentation, NodeType)>, +); + +impl< + Scope: ScopeDefinition, + NodeType: Clone + 'static, + Presentation: QueryPresentation>>, +> QueryStrategy for QueryNodesInScope +{ + type Output = Presentation; + fn query(node_repo: &impl NodeRepository) -> Self::Output { + let query_result = node_repo + .get_nodes_in_scope::(Scope::layer()) + .unwrap_or_default(); + + Presentation::present(query_result) + } +} + +#[derive(Debug)] +pub struct QueryGroupedNodes( + PhantomData<(Scope, Presentation, NodeType)>, +); + +impl< + Scope: ScopeDefinition, + NodeType: Clone + 'static, + Presentation: QueryPresentation>>>, +> QueryStrategy for QueryGroupedNodes +{ + type Output = Presentation; + fn query(node_repo: &impl NodeRepository) -> Self::Output { + let query_result = node_repo + .get_nodes_for_scope_per_top_level_element::(Scope::layer()) + .unwrap_or_default(); + + Presentation::present(query_result) + } +} + +pub mod convenience { + use crate::tree::querying::presentation::{First, Flattened, Grouped}; + use crate::tree::querying::queries::{QueryAllNodes, QueryGroupedNodes, QueryNodesInScope}; + use phenopackets::schema::v2::Phenopacket; + + // More to be added. + pub type All = QueryAllNodes>; + pub type GroupedIndividuals = + QueryGroupedNodes>; + pub type SingleInScope = QueryNodesInScope>; +} + +mod temp_test { + #![allow(dead_code)] + #![allow(unused)] + // Testing and see how it would work from here: + + use crate::tree::querying::presentation::{First, Flattened}; + use crate::tree::querying::queries::convenience::{All, GroupedIndividuals, SingleInScope}; + use crate::tree::querying::queries::{QueryAllNodes, QueryNodesInScope}; + use crate::tree::querying::traits::QueryStrategy; + use phenopackets::schema::v2::Phenopacket; + use phenopackets::schema::v2::core::{OntologyClass, PhenotypicFeature}; + + trait TheRuleTrait { + type Query: QueryStrategy; + + fn check_erased(&'_ self, board: ::Output) -> bool; + } + + struct __RuleImplementation1; + + impl TheRuleTrait for __RuleImplementation1 { + type Query = ( + QueryNodesInScope>, + QueryAllNodes>, + ); + + fn check_erased(&'_ self, board: ::Output) -> bool { + todo!() + } + } + + struct __RuleImplementation2; + + impl TheRuleTrait for __RuleImplementation2 { + type Query = ( + All, + SingleInScope, + GroupedIndividuals, + ); + + fn check_erased(&self, board: ::Output) -> bool { + let a = board; + todo!() + } + } + + struct __RuleImplementation3; + + impl TheRuleTrait for __RuleImplementation3 { + type Query = GroupedIndividuals; + + fn check_erased(&self, board: ::Output) -> bool { + todo!() + } + } +} diff --git a/src/tree/querying/traits.rs b/src/tree/querying/traits.rs new file mode 100644 index 0000000..3bfbc14 --- /dev/null +++ b/src/tree/querying/traits.rs @@ -0,0 +1,55 @@ +use crate::tree::scopes::ScopeLayer; +use crate::tree::traits::NodeRepository; + +pub trait QueryStrategy { + type Output; + #[allow(unused)] + fn query(node_repo: &impl NodeRepository) -> Self::Output; +} + +macro_rules! impl_query_strategy_tuple { + () => { + impl QueryStrategy for () { + type Output = (); + fn query(_node_repo: &impl NodeRepository) -> Self::Output { + + } + } + }; + + ($head:ident $(, $tail:ident)*) => { + impl_query_strategy_tuple!($($tail),*); + + impl<$head, $($tail),*> QueryStrategy for ($head, $($tail,)*) + where + $head: QueryStrategy, + $($tail: QueryStrategy),* + { + type Output = ($head::Output, $($tail::Output,)*); + + fn query(node_repo: &impl NodeRepository) -> Self::Output { + ( + $head::query(node_repo), + $($tail::query(node_repo),)* + ) + } + } + }; +} + +impl_query_strategy_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); + +#[allow(unused)] +pub(crate) trait QueryPresentation { + fn present(query_res: Input) -> Self + where + Self: Sized; +} + +pub trait ScopeDefinition { + fn layer() -> ScopeLayer; + + fn partitioning_fields() -> &'static [&'static str] { + &[] + } +} diff --git a/src/tree/scopes.rs b/src/tree/scopes.rs new file mode 100644 index 0000000..663215d --- /dev/null +++ b/src/tree/scopes.rs @@ -0,0 +1,172 @@ +use crate::tree::pointer::Pointer; +use crate::tree::querying::traits::ScopeDefinition; +use phenopackets::schema::v2::{Cohort, Family, Phenopacket}; +use std::any::TypeId; +use std::cell::Cell; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ScopeLayer { + Individual = 0, + Aggregated = 1, +} + +impl ScopeDefinition for Phenopacket { + fn layer() -> ScopeLayer { + ScopeLayer::Individual + } + + fn partitioning_fields() -> &'static [&'static str] { + &["members", "relatives", "proband"] + } +} + +impl ScopeDefinition for Family { + fn layer() -> ScopeLayer { + ScopeLayer::Aggregated + } +} + +impl ScopeDefinition for Cohort { + fn layer() -> ScopeLayer { + ScopeLayer::Aggregated + } +} + +pub(crate) struct ScopeMappings { + scope_by_type_id: HashMap, + boundaries: HashMap<&'static str, TypeId>, + max_seen_scope: Cell, +} + +impl ScopeMappings { + pub(crate) fn new() -> Self { + let mut scope_map = Self { + max_seen_scope: Cell::from(ScopeLayer::Individual), + scope_by_type_id: HashMap::new(), + boundaries: HashMap::new(), + }; + + scope_map.register::(); + scope_map.register::(); + scope_map.register::(); + + scope_map + } + + fn register(&mut self) { + let type_id = TypeId::of::(); + self.scope_by_type_id.insert(type_id, NodeType::layer()); + + for b_field in NodeType::partitioning_fields() { + self.boundaries.insert(b_field, type_id); + } + } + + pub fn get_scope(&self, type_id: &TypeId) -> Option { + self.scope_by_type_id.get(type_id).copied() + } + + pub fn is_scope_boundary(&self, type_id: &TypeId) -> bool { + self.scope_by_type_id.contains_key(type_id) + } + + pub fn derive_scope(&self, path: &str, type_id: &TypeId) -> ScopeLayer { + if let Some(scope) = self.get_scope(type_id) { + let current_max = self.max_seen_scope.get(); + self.max_seen_scope.set(current_max.max(scope)); + } + + let segments: Vec<_> = Pointer::new(path).segments().collect(); + for segment in segments.iter().rev() { + if let Some(boundary_type_id) = self.boundaries.get(segment.as_str()) { + return self + .scope_by_type_id + .get(boundary_type_id) + .copied() + .expect("Configuration error: Boundary points to unknown scope"); + } + } + + self.max_seen_scope.get() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use phenopackets::schema::v2::core::Resource; + + #[test] + fn test_derive_scope_single_pp() { + let scope_map = ScopeMappings::new(); + + let type_id = TypeId::of::(); + + assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Individual); + } + + #[test] + fn test_derive_scope_cohort() { + let scope_map = ScopeMappings::new(); + + let type_id = TypeId::of::(); + + assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + } + + #[test] + fn test_derive_scope_family() { + let scope_map = ScopeMappings::new(); + + let type_id = TypeId::of::(); + + assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + } + + #[test] + fn test_derive_scope_cohort_with_pp() { + let scope_map = ScopeMappings::new(); + + let type_id = TypeId::of::(); + + assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + + let type_id = TypeId::of::(); + + assert_eq!( + scope_map.derive_scope( + &format!("/{}", Phenopacket::partitioning_fields().first().unwrap()), + &type_id + ), + ScopeLayer::Individual + ); + } + + #[test] + fn test_derive_scope_cohort_with_random() { + let scope_map = ScopeMappings::new(); + + let type_id = TypeId::of::(); + + assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + + let type_id = TypeId::of::(); + + assert_eq!( + scope_map.derive_scope( + &format!( + "/{}/metaData/resources", + Phenopacket::partitioning_fields().first().unwrap() + ), + &type_id + ), + ScopeLayer::Individual + ); + + assert_eq!( + scope_map.derive_scope("/resources", &type_id), + ScopeLayer::Aggregated + ); + } +} diff --git a/src/tree/traits.rs b/src/tree/traits.rs index c0111fb..8d9e203 100644 --- a/src/tree/traits.rs +++ b/src/tree/traits.rs @@ -2,6 +2,7 @@ use crate::tree::error::NodeRepositoryError; use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; +use crate::tree::scopes::ScopeLayer; use serde_json::Value; use std::borrow::Cow; use std::ops::Range; @@ -19,7 +20,7 @@ pub trait RetrievableNode { fn value_at(&self, ptr: &Pointer) -> Option>; } -pub(crate) trait NodeRepository { +pub trait NodeRepository { fn insert( &mut self, node: MaterializedNode, @@ -33,13 +34,13 @@ pub(crate) trait NodeRepository { // Example: Get all nodes of the phenopacket scope + all resources of the cohort. Check if pp resources are in cohort fn get_nodes_in_scope( &self, - scope: u8, + scope: ScopeLayer, ) -> Result>, NodeRepositoryError>; // All nodes of a type for cases per case // Example: Check if all curie id's are represented in the resources in a phenopacket fn get_nodes_for_scope_per_top_level_element( &self, - scope: u8, + scope: ScopeLayer, ) -> Result>>, NodeRepositoryError>; } diff --git a/tests/test_custom_rule.rs b/tests/test_custom_rule.rs index 297f386..d7fc003 100644 --- a/tests/test_custom_rule.rs +++ b/tests/test_custom_rule.rs @@ -18,8 +18,9 @@ use phenolint::report::specs::{LabelSpecs, ReportSpecs}; use phenolint::report::traits::{CompileReport, RegisterableReport, ReportFromContext, RuleReport}; use phenolint::rules::traits::LintRule; use phenolint::rules::traits::{RuleCheck, RuleFromContext}; -use phenolint::tree::node_repository::List; use phenolint::tree::pointer::Pointer; +use phenolint::tree::querying::presentation::Flattened; +use phenolint::tree::querying::queries::convenience::All; use phenolint::tree::traits::Node; use phenolint_macros::{register_patch, register_report, register_rule}; use phenopackets::schema::v2::Phenopacket; @@ -43,9 +44,9 @@ impl RuleFromContext for CustomRule { } impl RuleCheck for CustomRule { - type Data<'a> = List<'a, OntologyClass>; + type Query = All; - fn check(&self, _: Self::Data<'_>) -> Vec { + fn check(&self, _: Flattened) -> Vec { vec![LintViolation::new( ViolationSeverity::Info, LintRule::rule_id(self), From 84b7d8358b28070becd4cf9fafa8b585caebf256 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Wed, 10 Dec 2025 15:42:34 +0100 Subject: [PATCH 09/14] Fix pointer --- src/tree/btree_node_repository.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index 72a5919..e6b6137 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -92,7 +92,7 @@ impl BTreeNodeRepository { self.span_store .range::(root_path.to_string()..) .take_while(|(k, _)| k.starts_with(root_path)) - .map(|(p, r)| (Pointer::new(p.as_str()), r.clone())) + .map(|(p, r)| (Pointer::from(p.as_str()), r.clone())) .collect() } @@ -114,7 +114,7 @@ impl BTreeNodeRepository { let content = content_ref.clone(); let spans = self.get_subtree_spans(path); - Ok(MaterializedNode::new(content, spans, Pointer::new(path))) + Ok(MaterializedNode::new(content, spans, Pointer::from(path))) } } @@ -287,7 +287,7 @@ mod tests { label: "All".to_string(), }, HashMap::new(), - Pointer::at_phenotypes().down("0/type").clone(), + Pointer::from("phenotypicFeatures/0/type").clone(), ); repo.insert(node).unwrap(); } From 98c71b9094b9521827dd2684a7f84324aac1e573 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Wed, 10 Dec 2025 17:06:56 +0100 Subject: [PATCH 10/14] Add builder test --- src/parsing/parseable_nodes.rs | 5 +- src/phenolint.rs | 11 +-- src/test_utils.rs | 5 ++ src/tree/btree_node_repository.rs | 121 +++++++++++++++++++++++------- src/tree/scopes.rs | 48 ++++++++---- src/tree/traits.rs | 7 ++ 6 files changed, 149 insertions(+), 48 deletions(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index a2a4812..f78d7f4 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -55,7 +55,6 @@ impl ParsableNode for Phenopacket { if let Value::Object(map) = &node.inner && map.contains_key("id") && map.contains_key("metaData") - && node.pointer().is_root() && let Ok(pp) = serde_json::from_value::(node.inner.clone()) { Some(pp) @@ -71,8 +70,12 @@ impl ParsableNode for Resource { && map.contains_key("id") && map.contains_key("name") && map.contains_key("url") + && map.contains_key("namespacePrefix") + && map.contains_key("version") + && map.contains_key("iriPrefix") && let Ok(resource) = serde_json::from_value::(node.inner.clone()) { + print!("Parsed Resource"); Some(resource) } else { None diff --git a/src/phenolint.rs b/src/phenolint.rs index cf1d1f2..db12367 100644 --- a/src/phenolint.rs +++ b/src/phenolint.rs @@ -20,7 +20,8 @@ use phenopackets::schema::v2::Phenopacket; use prost::Message; use serde_json::Value; -use crate::tree::btree_node_repository::BTreeNodeRepository; +use crate::tree::btree_node_repository::{BTreeNodeRepository, BTreeNodeRepositoryBuilder}; +use crate::tree::traits::NodeRepositoryBuilder; use std::fs; use std::path::PathBuf; @@ -70,13 +71,7 @@ impl Lint for Phenolint { let root_node = DynamicNode::new(&values, &spans, Pointer::at_root()); - let apt = AbstractTreeTraversal::new(values, spans); - let mut node_repo = BTreeNodeRepository::new(); - - for node in apt.traverse() { - self.node_materializer - .materialize_nodes(&node, &mut node_repo) - } + let node_repo = BTreeNodeRepositoryBuilder::build(values, spans); let mut findings = vec![]; for rule in self.rule_registry.rules() { diff --git a/src/test_utils.rs b/src/test_utils.rs index b707610..ccc9e19 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -2,6 +2,7 @@ use crate::diagnostics::LintFinding; use once_cell::sync::Lazy; use ontolius::io::OntologyLoaderBuilder; use ontolius::ontology::csr::FullCsrOntology; +use phenopackets::schema::v2::Phenopacket; use std::path::PathBuf; use std::sync::Arc; @@ -14,6 +15,10 @@ pub(crate) fn assets_dir() -> PathBuf { pub(crate) fn json_phenopacket_dir() -> PathBuf { assets_dir().join("phenopacket.json") } +pub(crate) fn test_phenopacket() -> Phenopacket { + let pp_dir = json_phenopacket_dir(); + serde_json::from_reader(std::fs::File::open(pp_dir).unwrap()).unwrap() +} pub(crate) static HPO: Lazy> = Lazy::new(|| init_ontolius(assets_dir().join("hp.toy.json"))); diff --git a/src/tree/btree_node_repository.rs b/src/tree/btree_node_repository.rs index 94b5d89..08fc582 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/btree_node_repository.rs @@ -1,13 +1,17 @@ #![allow(dead_code)] +use crate::materializer::NodeMaterializer; +use crate::tree::abstract_pheno_tree::AbstractTreeTraversal; use crate::tree::error::NodeRepositoryError; use crate::tree::node::MaterializedNode; use crate::tree::pointer::Pointer; use crate::tree::scopes::{ScopeLayer, ScopeMappings}; -use crate::tree::traits::{LocatableNode, NodeRepository}; +use crate::tree::traits::{LocatableNode, NodeRepository, NodeRepositoryBuilder}; +use serde_json::Value; use std::any::{Any, TypeId}; use std::collections::{BTreeMap, HashMap}; use std::ops::Range; +#[derive(Debug)] struct NodeEntry { type_id: TypeId, scope: ScopeLayer, @@ -66,11 +70,8 @@ impl NodeRepository for BTreeNodeRepository { node: MaterializedNode, ) -> Result<(), NodeRepositoryError> { let type_id = TypeId::of::(); - let node_path = node.pointer().position().to_string(); - let scope = self - .scope_mappings - .derive_scope(node_path.as_str(), &type_id); + let scope = self.scope_mappings.derive_scope(node.pointer(), &type_id); let is_scope_boundary = self.scope_mappings.is_scope_boundary(&type_id); for (ptr, span) in node.spans() { @@ -79,6 +80,8 @@ impl NodeRepository for BTreeNodeRepository { .or_insert_with(|| span.clone()); } + let ptr = node.pointer().clone(); + let entry = NodeEntry { type_id, scope, @@ -86,7 +89,7 @@ impl NodeRepository for BTreeNodeRepository { inner: Box::new(node.inner), }; - self.node_store.insert(node_path.to_string(), entry); + self.node_store.insert(ptr.to_string(), entry); Ok(()) } @@ -134,7 +137,6 @@ impl NodeRepository for BTreeNodeRepository { NodeType: Clone + 'static, { let target_type = TypeId::of::(); - let top_levels: Vec<&String> = self .node_store .iter() @@ -160,17 +162,80 @@ impl NodeRepository for BTreeNodeRepository { } } +pub(crate) struct BTreeNodeRepositoryBuilder; + +impl NodeRepositoryBuilder for BTreeNodeRepositoryBuilder { + fn build(tree: Value, spans: HashMap>) -> BTreeNodeRepository { + let mut repo = BTreeNodeRepository::new(); + let mut materialized = NodeMaterializer; + for node in AbstractTreeTraversal::new(tree, spans).traverse() { + materialized.materialize_nodes(&node, &mut repo); + } + repo + } +} + +#[cfg(test)] +mod test_builder { + use super::*; + use crate::test_utils::test_phenopacket; + use phenopackets::schema::v2::Phenopacket; + use phenopackets::schema::v2::core::{MetaData, Resource}; + use std::thread::Scope; + + #[test] + fn test_builder_single_phenopacket() { + let test_pp = Phenopacket { + id: "some_id".to_string(), + meta_data: Some(MetaData { + created: Some(Default::default()), + created_by: "Daniel The Man".to_string(), + submitted_by: "Peter Hobbitson".to_string(), + resources: vec![Resource { + id: "HP".to_string(), + name: "HPO".to_string(), + url: "www.hpo.com".to_string(), + version: "2.0".to_string(), + namespace_prefix: "hp".to_string(), + iri_prefix: "prefix".to_string(), + }], + phenopacket_schema_version: "2".to_string(), + ..Default::default() + }), + ..Default::default() + }; + + let values = serde_json::to_value(&test_pp).unwrap(); + let repo = BTreeNodeRepositoryBuilder::build(values, HashMap::new()); + + assert_eq!(repo.node_store.len(), 2); + + let boundries: Vec<_> = repo + .node_store + .values() + .filter(|node| node.is_scope_boundary) + .collect(); + + assert_eq!(boundries.len(), 1); + + let pp_node = boundries.first().unwrap(); + assert_eq!(pp_node.type_id, TypeId::of::()); + assert_eq!(pp_node.scope, ScopeLayer::Individual); + } +} + #[cfg(test)] mod tests { use super::*; use crate::tree::pointer::Pointer; + use crate::tree::traits::NodeRepositoryBuilder; use phenopackets::schema::v2::core::{MetaData, OntologyClass, Resource}; use phenopackets::schema::v2::{Cohort, Phenopacket}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; - fn test_cohort() -> Cohort { + fn generate_test_cohort() -> Cohort { let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("assets"); @@ -203,41 +268,45 @@ mod tests { }), } } - fn cohort_board() -> BTreeNodeRepository { - /*let cohort = test_cohort(); + fn cohort_repository() -> BTreeNodeRepository { + let cohort = generate_test_cohort(); let value = serde_json::to_value(&cohort).unwrap(); - let tree = AbstractTreeTraversal::new(value, HashMap::new()); - let repo = BTreeNodeRepository::new(); - - let mat = NodeMaterializer; - // TODO: Change interface of materialize_nodes to take an impl NodeRepository trait - for node in tree.traverse() { - mat.materialize_nodes(&node, &mut repo); - } - repo - */ - BTreeNodeRepository::new() + BTreeNodeRepositoryBuilder::build(value, HashMap::new()) } #[test] fn test_insert() { let mut repo = BTreeNodeRepository::new(); + let node_pointer = Pointer::from("phenotypicFeatures/0/type"); + let mut spans = BTreeMap::new(); + spans.insert(node_pointer.to_string().clone(), 0usize..50usize); + let node = MaterializedNode::new( OntologyClass { id: "HP:0000001".to_string(), label: "All".to_string(), }, - HashMap::new(), - Pointer::from("phenotypicFeatures/0/type").clone(), + spans + .iter() + .map(|(key, val)| (Pointer::from(key.as_str()), val.clone())) + .collect(), + node_pointer.clone(), ); repo.insert(node).unwrap(); + + let node_entry = repo.node_store.get(&node_pointer.to_string()).unwrap(); + + assert_eq!(repo.span_store, spans); + assert_eq!(node_entry.type_id, TypeId::of::()); + assert_eq!(node_entry.scope, ScopeLayer::Individual); + assert!(!node_entry.is_scope_boundary); } #[test] fn test_get_nodes_for_scope_per_top_level_element() { - let repo = cohort_board(); + let repo = cohort_repository(); let retrieved = repo .get_nodes_for_scope_per_top_level_element::(ScopeLayer::Individual) .unwrap(); @@ -247,8 +316,8 @@ mod tests { #[test] fn test_get_all_nodes() { - let repo = cohort_board(); - let test_cohort = test_cohort(); + let repo = cohort_repository(); + let test_cohort = generate_test_cohort(); let retrieved = repo.get_all::().unwrap(); let mut n_resources = test_cohort.meta_data.unwrap().resources.len(); diff --git a/src/tree/scopes.rs b/src/tree/scopes.rs index 663215d..887fc72 100644 --- a/src/tree/scopes.rs +++ b/src/tree/scopes.rs @@ -2,6 +2,7 @@ use crate::tree::pointer::Pointer; use crate::tree::querying::traits::ScopeDefinition; use phenopackets::schema::v2::{Cohort, Family, Phenopacket}; use std::any::TypeId; +use std::borrow::Cow; use std::cell::Cell; use std::collections::HashMap; @@ -71,15 +72,16 @@ impl ScopeMappings { self.scope_by_type_id.contains_key(type_id) } - pub fn derive_scope(&self, path: &str, type_id: &TypeId) -> ScopeLayer { + pub fn derive_scope(&self, path: &Pointer, type_id: &TypeId) -> ScopeLayer { if let Some(scope) = self.get_scope(type_id) { let current_max = self.max_seen_scope.get(); self.max_seen_scope.set(current_max.max(scope)); } - let segments: Vec<_> = Pointer::new(path).segments().collect(); + let segments: Vec> = path.iter_segments().collect(); + for segment in segments.iter().rev() { - if let Some(boundary_type_id) = self.boundaries.get(segment.as_str()) { + if let Some(boundary_type_id) = self.boundaries.get(segment.as_ref()) { return self .scope_by_type_id .get(boundary_type_id) @@ -103,7 +105,10 @@ mod tests { let type_id = TypeId::of::(); - assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Individual); + assert_eq!( + scope_map.derive_scope(&Pointer::at_root(), &type_id), + ScopeLayer::Individual + ); } #[test] @@ -112,7 +117,10 @@ mod tests { let type_id = TypeId::of::(); - assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + assert_eq!( + scope_map.derive_scope(&Pointer::at_root(), &type_id), + ScopeLayer::Aggregated + ); } #[test] @@ -121,7 +129,10 @@ mod tests { let type_id = TypeId::of::(); - assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + assert_eq!( + scope_map.derive_scope(&Pointer::at_root(), &type_id), + ScopeLayer::Aggregated + ); } #[test] @@ -130,13 +141,18 @@ mod tests { let type_id = TypeId::of::(); - assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + assert_eq!( + scope_map.derive_scope(&Pointer::at_root(), &type_id), + ScopeLayer::Aggregated + ); let type_id = TypeId::of::(); assert_eq!( scope_map.derive_scope( - &format!("/{}", Phenopacket::partitioning_fields().first().unwrap()), + &Pointer::from( + format!("/{}", Phenopacket::partitioning_fields().first().unwrap()).as_str() + ), &type_id ), ScopeLayer::Individual @@ -149,15 +165,21 @@ mod tests { let type_id = TypeId::of::(); - assert_eq!(scope_map.derive_scope("", &type_id), ScopeLayer::Aggregated); + assert_eq!( + scope_map.derive_scope(&Pointer::at_root(), &type_id), + ScopeLayer::Aggregated + ); let type_id = TypeId::of::(); assert_eq!( scope_map.derive_scope( - &format!( - "/{}/metaData/resources", - Phenopacket::partitioning_fields().first().unwrap() + &Pointer::from( + format!( + "/{}/metaData/resources", + Phenopacket::partitioning_fields().first().unwrap() + ) + .as_str() ), &type_id ), @@ -165,7 +187,7 @@ mod tests { ); assert_eq!( - scope_map.derive_scope("/resources", &type_id), + scope_map.derive_scope(&Pointer::from("/resources"), &type_id), ScopeLayer::Aggregated ); } diff --git a/src/tree/traits.rs b/src/tree/traits.rs index 8d9e203..63fed1f 100644 --- a/src/tree/traits.rs +++ b/src/tree/traits.rs @@ -5,6 +5,7 @@ use crate::tree::pointer::Pointer; use crate::tree::scopes::ScopeLayer; use serde_json::Value; use std::borrow::Cow; +use std::collections::HashMap; use std::ops::Range; pub trait Node: LocatableNode + RetrievableNode {} @@ -44,3 +45,9 @@ pub trait NodeRepository { scope: ScopeLayer, ) -> Result>>, NodeRepositoryError>; } + +pub(crate) trait NodeRepositoryBuilder { + fn build(tree: Value, spans: HashMap>) -> T + where + Self: Sized; +} From d34f162e9dbfbd0214b672e53654810b2b6e00f0 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Tue, 6 Jan 2026 10:15:40 +0100 Subject: [PATCH 11/14] Rename --- src/phenolint.rs | 5 ++-- src/rules/traits.rs | 6 ++--- ..._repository.rs => flat_node_repository.rs} | 24 +++++++++---------- src/tree/mod.rs | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) rename src/tree/{btree_node_repository.rs => flat_node_repository.rs} (94%) diff --git a/src/phenolint.rs b/src/phenolint.rs index db12367..764effb 100644 --- a/src/phenolint.rs +++ b/src/phenolint.rs @@ -12,7 +12,6 @@ use crate::report::report_registry::ReportRegistry; use crate::rules::rule_registry::{RuleRegistry, check_duplicate_rule_ids}; use crate::schema_validation::validator::PhenopacketSchemaValidator; use crate::traits::Lint; -use crate::tree::abstract_pheno_tree::AbstractTreeTraversal; use crate::tree::node::DynamicNode; use crate::tree::pointer::Pointer; use log::{error, warn}; @@ -20,7 +19,7 @@ use phenopackets::schema::v2::Phenopacket; use prost::Message; use serde_json::Value; -use crate::tree::btree_node_repository::{BTreeNodeRepository, BTreeNodeRepositoryBuilder}; +use crate::tree::flat_node_repository::FlatNodeRepositoryBuilder; use crate::tree::traits::NodeRepositoryBuilder; use std::fs; use std::path::PathBuf; @@ -71,7 +70,7 @@ impl Lint for Phenolint { let root_node = DynamicNode::new(&values, &spans, Pointer::at_root()); - let node_repo = BTreeNodeRepositoryBuilder::build(values, spans); + let node_repo = FlatNodeRepositoryBuilder::build(values, spans); let mut findings = vec![]; for rule in self.rule_registry.rules() { diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 656729b..7a8349e 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,14 +1,14 @@ use crate::LinterContext; use crate::diagnostics::LintViolation; use crate::error::FromContextError; -use crate::tree::btree_node_repository::BTreeNodeRepository; +use crate::tree::flat_node_repository::FlatNodeRepository; use crate::tree::querying::traits::QueryStrategy; pub trait LintRule: Send + Sync { fn rule_id(&self) -> &str; // Needs to be concrete type, because NodeRepository trait is not dyn compatible :( - fn check_erased(&self, board: &BTreeNodeRepository) -> Vec; + fn check_erased(&self, board: &FlatNodeRepository) -> Vec; } pub trait RuleMetaData { @@ -35,7 +35,7 @@ where self.rule_id() } - fn check_erased(&self, board: &BTreeNodeRepository) -> Vec { + fn check_erased(&self, board: &FlatNodeRepository) -> Vec { let data = ::Query::query(board); self.check(data) diff --git a/src/tree/btree_node_repository.rs b/src/tree/flat_node_repository.rs similarity index 94% rename from src/tree/btree_node_repository.rs rename to src/tree/flat_node_repository.rs index 08fc582..a1a7483 100644 --- a/src/tree/btree_node_repository.rs +++ b/src/tree/flat_node_repository.rs @@ -19,13 +19,13 @@ struct NodeEntry { inner: Box, } -pub struct BTreeNodeRepository { +pub struct FlatNodeRepository { node_store: BTreeMap, span_store: BTreeMap>, scope_mappings: ScopeMappings, } -impl BTreeNodeRepository { +impl FlatNodeRepository { pub(crate) fn new() -> Self { Self { node_store: BTreeMap::new(), @@ -64,7 +64,7 @@ impl BTreeNodeRepository { } } -impl NodeRepository for BTreeNodeRepository { +impl NodeRepository for FlatNodeRepository { fn insert( &mut self, node: MaterializedNode, @@ -162,11 +162,11 @@ impl NodeRepository for BTreeNodeRepository { } } -pub(crate) struct BTreeNodeRepositoryBuilder; +pub(crate) struct FlatNodeRepositoryBuilder; -impl NodeRepositoryBuilder for BTreeNodeRepositoryBuilder { - fn build(tree: Value, spans: HashMap>) -> BTreeNodeRepository { - let mut repo = BTreeNodeRepository::new(); +impl NodeRepositoryBuilder for FlatNodeRepositoryBuilder { + fn build(tree: Value, spans: HashMap>) -> FlatNodeRepository { + let mut repo = FlatNodeRepository::new(); let mut materialized = NodeMaterializer; for node in AbstractTreeTraversal::new(tree, spans).traverse() { materialized.materialize_nodes(&node, &mut repo); @@ -178,10 +178,8 @@ impl NodeRepositoryBuilder for BTreeNodeRepositoryBuilder { #[cfg(test)] mod test_builder { use super::*; - use crate::test_utils::test_phenopacket; use phenopackets::schema::v2::Phenopacket; use phenopackets::schema::v2::core::{MetaData, Resource}; - use std::thread::Scope; #[test] fn test_builder_single_phenopacket() { @@ -206,7 +204,7 @@ mod test_builder { }; let values = serde_json::to_value(&test_pp).unwrap(); - let repo = BTreeNodeRepositoryBuilder::build(values, HashMap::new()); + let repo = FlatNodeRepositoryBuilder::build(values, HashMap::new()); assert_eq!(repo.node_store.len(), 2); @@ -268,16 +266,16 @@ mod tests { }), } } - fn cohort_repository() -> BTreeNodeRepository { + fn cohort_repository() -> FlatNodeRepository { let cohort = generate_test_cohort(); let value = serde_json::to_value(&cohort).unwrap(); - BTreeNodeRepositoryBuilder::build(value, HashMap::new()) + FlatNodeRepositoryBuilder::build(value, HashMap::new()) } #[test] fn test_insert() { - let mut repo = BTreeNodeRepository::new(); + let mut repo = FlatNodeRepository::new(); let node_pointer = Pointer::from("phenotypicFeatures/0/type"); let mut spans = BTreeMap::new(); diff --git a/src/tree/mod.rs b/src/tree/mod.rs index b70c706..e801c0d 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -1,6 +1,6 @@ pub(crate) mod abstract_pheno_tree; -pub(crate) mod btree_node_repository; mod error; +pub(crate) mod flat_node_repository; pub mod node; pub mod pointer; pub mod querying; From 0560417199c877bb21f03686511015dfc9f32efe Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Tue, 6 Jan 2026 12:11:54 +0100 Subject: [PATCH 12/14] Test get_nodes_for_scope_per_top_level_element --- src/parsing/parseable_nodes.rs | 1 - src/tree/flat_node_repository.rs | 90 ++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index f78d7f4..feb7713 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -75,7 +75,6 @@ impl ParsableNode for Resource { && map.contains_key("iriPrefix") && let Ok(resource) = serde_json::from_value::(node.inner.clone()) { - print!("Parsed Resource"); Some(resource) } else { None diff --git a/src/tree/flat_node_repository.rs b/src/tree/flat_node_repository.rs index a1a7483..6df3876 100644 --- a/src/tree/flat_node_repository.rs +++ b/src/tree/flat_node_repository.rs @@ -223,7 +223,7 @@ mod test_builder { } #[cfg(test)] -mod tests { +mod tests_repository { use super::*; use crate::tree::pointer::Pointer; use crate::tree::traits::NodeRepositoryBuilder; @@ -266,13 +266,74 @@ mod tests { }), } } - fn cohort_repository() -> FlatNodeRepository { + + struct NodeRepoUnitTester; + + impl NodeRepoUnitTester { + pub fn test(cohort: Cohort) + where + R: NodeRepository, + B: NodeRepositoryBuilder, + { + let node_repo: R = Self::build_test_repo::(&cohort); + Self::test_get_nodes_for_scope_per_top_level_element(node_repo, &cohort); + } + + fn build_test_repo(cohort: &Cohort) -> R + where + R: NodeRepository, + B: NodeRepositoryBuilder, + { + let value = serde_json::to_value(cohort).unwrap(); + B::build(value, HashMap::new()) + } + + fn test_get_nodes_for_scope_per_top_level_element(repo: R, cohort: &Cohort) + where + R: NodeRepository, + { + let retrieved = repo + .get_nodes_for_scope_per_top_level_element::(ScopeLayer::Individual) + .unwrap(); + + for (member, nodes) in cohort.members.iter().zip(&retrieved) { + let resources = &member.meta_data.as_ref().unwrap().resources; + for (resource, node) in resources.iter().zip(nodes) { + assert_eq!(&node.inner, resource); + } + } + } + + fn test_get_all_nodes(repo: R, cohort: &Cohort) + where + R: NodeRepository, + { + let retrieved = repo.get_all::().unwrap(); + + let mut n_resources = cohort.meta_data.clone().unwrap().resources.len(); + + for pp in cohort.members.clone() { + n_resources += pp.meta_data.unwrap().resources.len() + } + + assert_eq!(retrieved.len(), n_resources); + } + } + + fn build_test_repo() -> FlatNodeRepository { let cohort = generate_test_cohort(); let value = serde_json::to_value(&cohort).unwrap(); FlatNodeRepositoryBuilder::build(value, HashMap::new()) } + #[test] + fn test_node_repository() { + let cohort = generate_test_cohort(); + + NodeRepoUnitTester::test::(cohort); + } + #[test] fn test_insert() { let mut repo = FlatNodeRepository::new(); @@ -301,29 +362,4 @@ mod tests { assert_eq!(node_entry.scope, ScopeLayer::Individual); assert!(!node_entry.is_scope_boundary); } - - #[test] - fn test_get_nodes_for_scope_per_top_level_element() { - let repo = cohort_repository(); - let retrieved = repo - .get_nodes_for_scope_per_top_level_element::(ScopeLayer::Individual) - .unwrap(); - - assert_eq!(retrieved.len(), 2); - } - - #[test] - fn test_get_all_nodes() { - let repo = cohort_repository(); - let test_cohort = generate_test_cohort(); - let retrieved = repo.get_all::().unwrap(); - - let mut n_resources = test_cohort.meta_data.unwrap().resources.len(); - - for pp in test_cohort.members { - n_resources += pp.meta_data.unwrap().resources.len() - } - - assert_eq!(retrieved.len(), n_resources); - } } From 2840c27e180fa619bd4286fb3ab9f16f8f44d42a Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Tue, 6 Jan 2026 13:17:53 +0100 Subject: [PATCH 13/14] Add NodeRepoUnitTester --- src/parsing/parseable_nodes.rs | 1 + src/tree/flat_node_repository.rs | 67 +++++++++++++++++++++++++------- src/tree/scopes.rs | 1 + 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index feb7713..d2af0f4 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -1,5 +1,6 @@ use crate::parsing::traits::ParsableNode; use crate::tree::node::DynamicNode; +use crate::tree::pointer::Pointer; use crate::tree::traits::LocatableNode; use phenopackets::schema::v2::core::{ Diagnosis, Disease, OntologyClass, PhenotypicFeature, Resource, VitalStatus, diff --git a/src/tree/flat_node_repository.rs b/src/tree/flat_node_repository.rs index 6df3876..de58239 100644 --- a/src/tree/flat_node_repository.rs +++ b/src/tree/flat_node_repository.rs @@ -258,7 +258,7 @@ mod tests_repository { url: "www.example.com".to_string(), version: "2020-10-10".to_string(), namespace_prefix: "hp".to_string(), - iri_prefix: "".to_string(), + iri_prefix: "hp".to_string(), }], updates: vec![], phenopacket_schema_version: "2".to_string(), @@ -276,7 +276,9 @@ mod tests_repository { B: NodeRepositoryBuilder, { let node_repo: R = Self::build_test_repo::(&cohort); - Self::test_get_nodes_for_scope_per_top_level_element(node_repo, &cohort); + Self::test_get_nodes_for_scope_per_top_level_element(&node_repo, &cohort); + Self::test_get_all_nodes(&node_repo, &cohort); + Self::test_get_nodes_in_scope(&node_repo, &cohort); } fn build_test_repo(cohort: &Cohort) -> R @@ -288,7 +290,47 @@ mod tests_repository { B::build(value, HashMap::new()) } - fn test_get_nodes_for_scope_per_top_level_element(repo: R, cohort: &Cohort) + fn test_get_nodes_in_scope(repo: &R, cohort: &Cohort) + where + R: NodeRepository, + { + let cohort_level_resources = repo + .get_nodes_in_scope::(ScopeLayer::Aggregated) + .unwrap(); + + for node_resource in cohort_level_resources.iter() { + assert!( + cohort + .meta_data + .clone() + .unwrap() + .resources + .contains(&node_resource.inner) + ); + } + assert_eq!( + cohort.meta_data.clone().unwrap().resources.len(), + cohort_level_resources.len() + ); + + let pp_level_resources = repo + .get_nodes_in_scope::(ScopeLayer::Individual) + .unwrap(); + + let all_pp_resources: Vec = cohort + .members + .clone() + .iter() + .flat_map(|pp| pp.meta_data.clone().unwrap().resources) + .collect(); + + for node_resource in pp_level_resources.iter() { + assert!(all_pp_resources.contains(&node_resource.inner)); + } + assert_eq!(pp_level_resources.len(), all_pp_resources.len()); + } + + fn test_get_nodes_for_scope_per_top_level_element(repo: &R, cohort: &Cohort) where R: NodeRepository, { @@ -304,33 +346,30 @@ mod tests_repository { } } - fn test_get_all_nodes(repo: R, cohort: &Cohort) + fn test_get_all_nodes(repo: &R, cohort: &Cohort) where R: NodeRepository, { let retrieved = repo.get_all::().unwrap(); let mut n_resources = cohort.meta_data.clone().unwrap().resources.len(); - for pp in cohort.members.clone() { n_resources += pp.meta_data.unwrap().resources.len() } - assert_eq!(retrieved.len(), n_resources); + assert_eq!( + retrieved.len(), + n_resources, + "Got {} resources from repo, but expected {}", + retrieved.len(), + n_resources + ); } } - fn build_test_repo() -> FlatNodeRepository { - let cohort = generate_test_cohort(); - let value = serde_json::to_value(&cohort).unwrap(); - - FlatNodeRepositoryBuilder::build(value, HashMap::new()) - } - #[test] fn test_node_repository() { let cohort = generate_test_cohort(); - NodeRepoUnitTester::test::(cohort); } diff --git a/src/tree/scopes.rs b/src/tree/scopes.rs index 887fc72..430513a 100644 --- a/src/tree/scopes.rs +++ b/src/tree/scopes.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ScopeLayer { Individual = 0, + // TODO: Find better name than Aggregated Aggregated = 1, } From 589fd7b732641325390668cc9e821088c4d2eecf Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Tue, 6 Jan 2026 13:23:55 +0100 Subject: [PATCH 14/14] Linting --- src/parsing/parseable_nodes.rs | 1 - src/phenolint.rs | 3 --- src/schema_validation/validator.rs | 13 ++++--------- src/test_utils.rs | 8 ++++++++ 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index d2af0f4..feb7713 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -1,6 +1,5 @@ use crate::parsing::traits::ParsableNode; use crate::tree::node::DynamicNode; -use crate::tree::pointer::Pointer; use crate::tree::traits::LocatableNode; use phenopackets::schema::v2::core::{ Diagnosis, Disease, OntologyClass, PhenotypicFeature, Resource, VitalStatus, diff --git a/src/phenolint.rs b/src/phenolint.rs index 764effb..25867ee 100644 --- a/src/phenolint.rs +++ b/src/phenolint.rs @@ -3,7 +3,6 @@ use crate::diagnostics::enums::PhenopacketData; use crate::diagnostics::{LintFinding, LintReport}; use crate::enums::InputTypes; use crate::error::{InitError, LintResult, LinterError, ParsingError, validation_error_to_string}; -use crate::materializer::NodeMaterializer; use crate::parsing::phenopacket_parser::PhenopacketParser; use crate::patches::patch_engine::PatchEngine; use crate::patches::patch_registry::PatchRegistry; @@ -28,7 +27,6 @@ pub struct Phenolint { rule_registry: RuleRegistry, patch_registry: PatchRegistry, report_registry: ReportRegistry, - node_materializer: NodeMaterializer, patch_engine: PatchEngine, validator: PhenopacketSchemaValidator, } @@ -45,7 +43,6 @@ impl Phenolint { rule_registry, report_registry, patch_registry, - node_materializer: NodeMaterializer, patch_engine: PatchEngine, validator: PhenopacketSchemaValidator::default(), } diff --git a/src/schema_validation/validator.rs b/src/schema_validation/validator.rs index 13dfa40..d0c8016 100644 --- a/src/schema_validation/validator.rs +++ b/src/schema_validation/validator.rs @@ -131,10 +131,9 @@ impl Default for PhenopacketSchemaValidator { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::json_phenopacket_dir; + use crate::test_utils::test_phenopacket_as_value; use rstest::{fixture, rstest}; use serde_json::json; - use std::fs; #[fixture] #[once] @@ -144,9 +143,7 @@ mod tests { #[fixture] fn base_phenopacket() -> Value { - let phenostr = - fs::read_to_string(json_phenopacket_dir()).expect("Could not read test file"); - serde_json::from_str(&phenostr).expect("Invalid JSON in test file") + test_phenopacket_as_value() } #[rstest] @@ -231,16 +228,14 @@ mod tests { } #[rstest] - fn test_validator_thread_safety() { + fn test_validator_thread_safety(base_phenopacket: Value) { let validator = std::sync::Arc::new(PhenopacketSchemaValidator::default()); - let phenostr = fs::read_to_string(json_phenopacket_dir()).unwrap(); - let pp: Value = serde_json::from_str(&phenostr).unwrap(); let mut handles = vec![]; for _ in 0..5 { let v_clone = validator.clone(); - let pp_clone = pp.clone(); + let pp_clone = base_phenopacket.clone(); handles.push(std::thread::spawn(move || { let res = v_clone.validate_phenopacket(&pp_clone); assert!(res.is_ok()); diff --git a/src/test_utils.rs b/src/test_utils.rs index ccc9e19..0353815 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -3,6 +3,8 @@ use once_cell::sync::Lazy; use ontolius::io::OntologyLoaderBuilder; use ontolius::ontology::csr::FullCsrOntology; use phenopackets::schema::v2::Phenopacket; +use serde_json::Value; +use std::fs; use std::path::PathBuf; use std::sync::Arc; @@ -15,6 +17,12 @@ pub(crate) fn assets_dir() -> PathBuf { pub(crate) fn json_phenopacket_dir() -> PathBuf { assets_dir().join("phenopacket.json") } + +pub(crate) fn test_phenopacket_as_value() -> Value { + let phenostr = fs::read_to_string(json_phenopacket_dir()).expect("Could not read test file"); + serde_json::from_str(&phenostr).expect("Invalid JSON in test file") +} +#[allow(dead_code)] pub(crate) fn test_phenopacket() -> Phenopacket { let pp_dir = json_phenopacket_dir(); serde_json::from_reader(std::fs::File::open(pp_dir).unwrap()).unwrap()