1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! # SKOS Concept Scheme Mappings / Analyzer
//!
//! Analyzes `skos:ConceptScheme` membership and builds concept trees from
//! the `skos:narrower` hierarchy.
use std::collections::HashSet;
use crate::skos_types::{
ConceptNode, ConceptTree, Graph, NamedNode, HAS_TOP_CONCEPT, IN_SCHEME, NARROWER, PREF_LABEL,
TOP_CONCEPT_OF,
};
// ---------------------------------------------------------------------------
// ConceptSchemeAnalyzer
// ---------------------------------------------------------------------------
/// Analyzes a `skos:ConceptScheme` within a graph.
pub struct ConceptSchemeAnalyzer;
impl ConceptSchemeAnalyzer {
/// Return all concepts declared as top concepts of `scheme` via either
/// `skos:hasTopConcept` or `skos:topConceptOf`.
pub fn top_concepts(graph: &Graph, scheme: &NamedNode) -> Vec<NamedNode> {
let mut result: HashSet<String> = HashSet::new();
// scheme skos:hasTopConcept X
for (_, _, o) in graph
.triples_with_subject(scheme)
.filter(|(_, p, _)| p == HAS_TOP_CONCEPT)
{
result.insert(o.clone());
}
// X skos:topConceptOf scheme
for (s, _, _) in graph
.triples_with_object(scheme)
.filter(|(_, p, _)| p == TOP_CONCEPT_OF)
{
result.insert(s.clone());
}
result.into_iter().collect()
}
/// Return **all** concepts in `scheme` — i.e., every concept `C` for which
/// `C skos:inScheme scheme` is present (after entailment, this includes top
/// concepts via rule S7).
pub fn all_concepts(graph: &Graph, scheme: &NamedNode) -> Vec<NamedNode> {
let mut result: HashSet<String> = HashSet::new();
// Direct inScheme
for (s, _, _) in graph
.triples_with_object(scheme)
.filter(|(_, p, _)| p == IN_SCHEME)
{
result.insert(s.clone());
}
// Also pick up concepts declared via hasTopConcept / topConceptOf
for c in Self::top_concepts(graph, scheme) {
result.insert(c);
}
result.into_iter().collect()
}
/// Build a `ConceptTree` rooted at the top concepts of `scheme`.
///
/// Recursively traverses `skos:narrower` edges. Cycles are broken by
/// tracking visited concept IRIs per branch.
pub fn concept_tree(graph: &Graph, scheme: &NamedNode) -> ConceptTree {
let top = Self::top_concepts(graph, scheme);
let root_concepts = top
.into_iter()
.map(|iri| build_node(graph, &iri, &mut HashSet::new()))
.collect();
ConceptTree { root_concepts }
}
}
/// Recursively build a `ConceptNode` for `iri`, walking `skos:narrower` edges.
fn build_node(graph: &Graph, iri: &str, visited: &mut HashSet<String>) -> ConceptNode {
visited.insert(iri.to_owned());
let pref_label = graph
.triples_with_subject(iri)
.find(|(_, p, _)| p == PREF_LABEL)
.map(|(_, _, o)| {
// Strip @lang suffix for display
if let Some(at) = o.rfind('@') {
o[..at].to_owned()
} else {
o.clone()
}
});
let children: Vec<ConceptNode> = graph
.triples_with_subject(iri)
.filter(|(_, p, _)| p == NARROWER)
.map(|(_, _, child_iri)| child_iri.clone())
.filter(|child| !visited.contains(child.as_str()))
.collect::<Vec<_>>()
.into_iter()
.map(|child_iri| {
let mut vis = visited.clone();
build_node(graph, &child_iri, &mut vis)
})
.collect();
ConceptNode {
iri: iri.to_owned(),
pref_label,
children,
}
}