1use std::collections::{HashMap, HashSet, VecDeque};
2
3use oxigraph::io::{RdfFormat, RdfParser};
4use oxigraph::model::*;
5
6use crate::{Error, GraphRead, Result};
7
8#[cfg(test)]
9#[path = "graph_tests.rs"]
10mod tests;
11
12pub const SH: &str = "http://www.w3.org/ns/shacl#";
13pub const RDF: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
14pub const RDFS: &str = "http://www.w3.org/2000/01/rdf-schema#";
15pub const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
16
17pub fn iri(namespace: &str, name: &str) -> NamedNode {
18 NamedNode::new_unchecked(format!("{namespace}{name}"))
19}
20
21pub fn subject(term: &Term) -> Result<NamedOrBlankNode> {
22 match term {
23 Term::NamedNode(n) => Ok(n.clone().into()),
24 Term::BlankNode(n) => Ok(n.clone().into()),
25 _ => Err(Error::Invalid(format!(
26 "Expected a shape IRI or blank node, got {term}"
27 ))),
28 }
29}
30
31pub fn parse(input: &str, format: RdfFormat, base_iri: Option<&str>) -> Result<Dataset> {
34 parse_with_graphs(input, format, base_iri, false, false)
35}
36
37pub fn parse_dataset(input: &str, format: RdfFormat, base_iri: Option<&str>) -> Result<Dataset> {
38 parse_with_graphs(input, format, base_iri, true, false)
39}
40
41pub fn parse_store_snapshot(input: &str) -> Result<Dataset> {
45 parse_with_graphs(input, RdfFormat::NQuads, None, true, true)
46}
47
48pub fn store_quad(mut quad: Quad) -> Quad {
49 if let Term::Literal(literal) = &quad.object
50 && literal.datatype() == iri(XSD, "string").as_ref()
51 && !literal.has_explicit_datatype()
52 {
53 quad.object = Literal::new_typed_literal(literal.value(), iri(XSD, "string")).into();
54 }
55 quad
56}
57
58fn parse_with_graphs(
59 input: &str,
60 format: RdfFormat,
61 base_iri: Option<&str>,
62 named: bool,
63 store_strings: bool,
64) -> Result<Dataset> {
65 let mut store = Dataset::new();
66 let mut parser = RdfParser::from_format(format);
67 if !named {
68 parser = parser.without_named_graphs();
69 }
70 if let Some(base) = base_iri {
71 parser = parser
72 .with_base_iri(base)
73 .map_err(|e| Error::Invalid(format!("Invalid base IRI: {e}")))?;
74 }
75 let quads = parser
76 .for_slice(input.as_bytes())
77 .map(|q| q.map(|q| if store_strings { store_quad(q) } else { q }))
78 .collect::<std::result::Result<Vec<_>, _>>()
79 .map_err(|e| Error::Invalid(format!("RDF parse error: {e}")))?;
80 store.extend(quads);
81 Ok(store)
82}
83
84pub fn serialize(store: &Dataset) -> Result<String> {
85 let mut writer =
86 oxigraph::io::RdfSerializer::from_format(RdfFormat::NQuads).for_writer(Vec::new());
87 for quad in store.iter() {
88 writer
89 .serialize_quad(quad)
90 .map_err(|e| Error::Runtime(e.to_string()))?;
91 }
92 String::from_utf8(writer.finish().map_err(|e| Error::Runtime(e.to_string()))?)
93 .map_err(|e| Error::Runtime(e.to_string()))
94}
95
96#[cfg(test)]
97thread_local! {
98 pub(crate) static UNION_BUILDS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
99}
100
101pub fn with_union(data: &Dataset) -> Dataset {
103 #[cfg(test)]
104 UNION_BUILDS.with(|count| count.set(count.get() + 1));
105 let mut union = data.clone();
106 union.extend(
107 data.iter()
108 .map(|q| Quad::new(q.subject, q.predicate, q.object, GraphName::DefaultGraph)),
109 );
110 union
111}
112
113pub fn commit_additions(data: &mut Dataset, before: &Dataset, after: &Dataset) {
114 data.extend(
115 after
116 .iter()
117 .filter(|q| !before.contains(*q))
118 .map(|q| q.into_owned()),
119 );
120}
121
122pub fn objects(
123 store: &(impl GraphRead + ?Sized),
124 node: &Term,
125 predicate: &NamedNode,
126) -> Result<Vec<Term>> {
127 let Ok(node) = subject(node) else {
128 return Ok(Vec::new());
129 };
130 store
131 .matching_quads(
132 Some(node.as_ref()),
133 Some(predicate.as_ref()),
134 None,
135 Some(GraphNameRef::DefaultGraph),
136 )
137 .map(|q| q.map(|q| q.object))
138 .collect()
139}
140
141pub fn values(store: &(impl GraphRead + ?Sized), node: &Term, name: &str) -> Result<Vec<Term>> {
142 objects(store, node, &iri(SH, name))
143}
144
145pub fn one(store: &(impl GraphRead + ?Sized), node: &Term, name: &str) -> Result<Option<Term>> {
146 let mut terms = values(store, node, name)?;
147 if terms.len() > 1 {
148 return Err(Error::Invalid(format!(
149 "{node} has multiple sh:{name} values"
150 )));
151 }
152 Ok(terms.pop())
153}
154
155pub fn boolean(store: &(impl GraphRead + ?Sized), node: &Term, name: &str) -> Result<bool> {
156 match one(store, node, name)? {
157 None => Ok(false),
158 Some(Term::Literal(v)) if v.datatype() == iri(XSD, "boolean").as_ref() => match v.value() {
159 "true" | "1" => Ok(true),
160 "false" | "0" => Ok(false),
161 _ => Err(Error::Invalid(format!(
162 "Invalid sh:{name} boolean on {node}"
163 ))),
164 },
165 _ => Err(Error::Invalid(format!(
166 "sh:{name} must be an xsd:boolean on {node}"
167 ))),
168 }
169}
170
171pub fn subjects(
172 store: &(impl GraphRead + ?Sized),
173 predicate: &NamedNode,
174 object: Option<&Term>,
175) -> Result<Vec<Term>> {
176 store
177 .matching_quads(
178 None,
179 Some(predicate.as_ref()),
180 object.map(Term::as_ref),
181 Some(GraphNameRef::DefaultGraph),
182 )
183 .map(|q| q.map(|q| q.subject.into()))
184 .collect()
185}
186
187pub fn add(store: &mut Dataset, s: &Term, p: NamedNode, o: Term) -> Result<()> {
188 store.insert(&Quad::new(subject(s)?, p, o, GraphName::DefaultGraph));
189 Ok(())
190}
191
192#[derive(Default)]
196pub(crate) struct ReportClones(HashSet<(usize, Term)>);
197
198impl ReportClones {
199 pub fn copy(&mut self, source: &Dataset, target: &mut Dataset, node: &Term) -> Result<()> {
200 if !matches!(node, Term::BlankNode(_))
201 || !self
202 .0
203 .insert((std::ptr::from_ref(source) as usize, node.clone()))
204 {
205 return Ok(());
206 }
207 let mut mapping = HashMap::from([(node.clone(), node.clone())]);
208 let mut queue = VecDeque::from([node.clone()]);
209 let mut seen = HashSet::new();
210 while let Some(node) = queue.pop_front() {
211 if !seen.insert(node.clone()) {
212 continue;
213 }
214 let cloned = mapping[&node].clone();
215 for quad in source.quads_for_subject(subject(&node)?.as_ref()) {
216 let object = quad.object.into_owned();
217 let copied = if matches!(object, Term::BlankNode(_)) {
218 queue.push_back(object.clone());
219 mapping
220 .entry(object)
221 .or_insert_with(|| BlankNode::default().into())
222 .clone()
223 } else {
224 object
225 };
226 add(target, &cloned, quad.predicate.into_owned(), copied)?;
227 }
228 }
229 Ok(())
230 }
231}
232
233pub(crate) fn instances(graph: &(impl GraphRead + ?Sized), class: &Term) -> Result<HashSet<Term>> {
234 let mut result = HashSet::new();
235 let mut seen = HashSet::new();
236 let mut pending = vec![class.clone()];
237 while let Some(class) = pending.pop() {
238 if !seen.insert(class.clone()) {
239 continue;
240 }
241 result.extend(subjects(graph, &iri(RDF, "type"), Some(&class))?);
242 pending.extend(subjects(graph, &iri(RDFS, "subClassOf"), Some(&class))?);
243 }
244 Ok(result)
245}