use crate::commands::card::{self, CardArgs};
use rete_core::ingest;
fn read_input(path: &str) -> anyhow::Result<String> {
if path == "-" {
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s)?;
Ok(s)
} else {
Ok(std::fs::read_to_string(path)?)
}
}
pub(crate) fn input_format(path: &str, override_fmt: Option<&str>) -> &'static str {
if let Some(f) = override_fmt {
return match f {
"nq" => "nq",
"ttl" => "ttl",
"rdfxml" | "rdf" | "owl" | "xml" => "rdfxml",
_ => "nt",
};
}
let p = path.to_ascii_lowercase();
if p.ends_with(".nq") || p.ends_with(".nquads") {
"nq"
} else if p.ends_with(".ttl") || p.ends_with(".turtle") {
"ttl"
} else if p.ends_with(".rdf") || p.ends_with(".owl") || p.ends_with(".rdfxml") {
"rdfxml"
} else {
"nt"
}
}
fn looks_like_non_rdf_owl(text: &str) -> bool {
let head = &text[..text.len().min(4096)];
(!head.contains("rdf:RDF") && head.contains("<Ontology"))
|| (!head.trim_start().starts_with('<') && head.contains("Ontology("))
}
fn parse_inputs(inputs: &[String], format: Option<&str>) -> anyhow::Result<Vec<ingest::RawQuad>> {
let mut quads: Vec<ingest::RawQuad> = Vec::new();
for input in inputs {
let fmt = input_format(input, format);
let parsed = if input != "-" && (fmt == "nt" || fmt == "nq") {
let file = std::fs::File::open(input).map_err(|e| anyhow::anyhow!("{input}: {e}"))?;
let cap = file
.metadata()
.map(|m| (m.len() / 64) as usize)
.unwrap_or(0);
ingest::parse_reader(std::io::BufReader::new(file), fmt, cap)
.map_err(|e| anyhow::anyhow!("{input}: {e}"))?
} else {
let text = read_input(input)?;
ingest::parse_statements(&text, fmt).map_err(|e| {
if fmt == "rdfxml" && looks_like_non_rdf_owl(&text) {
anyhow::anyhow!(
"{input}: {e}\n\
hint: this looks like OWL/XML or OWL Functional Syntax, which \
are not RDF. Convert to RDF/XML or Turtle first (e.g. owlready2, \
`robot convert`, or Protégé → Save as → RDF/XML), then build that."
)
} else {
anyhow::anyhow!("{input}: {e}")
}
})?
};
if quads.is_empty() {
quads = parsed;
} else {
quads.extend(parsed);
}
}
Ok(quads)
}
pub(crate) fn validate(inputs: &[String], format: Option<&str>) -> anyhow::Result<()> {
let quads = parse_inputs(inputs, format)?;
let named: std::collections::BTreeSet<&String> =
quads.iter().filter_map(|(_, _, _, g)| g.as_ref()).collect();
let in_default = quads.iter().filter(|(_, _, _, g)| g.is_none()).count();
println!(
"valid: {} statement(s) — {} in the default graph, {} named graph(s)",
quads.len(),
in_default,
named.len()
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build(
inputs: &[String],
output: &str,
format: Option<&str>,
materialize: bool,
no_pyramid: bool,
reason: bool,
pyramid_algo: rete_core::PyramidAlgo,
text_index: bool,
type_predicate: Option<&str>,
card_args: CardArgs,
) -> anyhow::Result<()> {
let streamable = !materialize
&& !reason
&& inputs
.iter()
.all(|i| i != "-" && matches!(input_format(i, format), "nt" | "nq"));
if streamable {
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let inputs_fmt: Vec<(&str, &'static str)> = inputs
.iter()
.map(|i| (i.as_str(), input_format(i, format)))
.collect();
let stream = |visit: &mut dyn FnMut(ingest::RawQuad)| -> Result<(), ingest::IngestError> {
for (path, fmt) in &inputs_fmt {
let file = std::fs::File::open(path)
.map_err(|e| ingest::IngestError::Io(format!("{path}: {e}")))?;
ingest::stream_reader(std::io::BufReader::new(file), fmt, visit)?;
}
Ok(())
};
let (bytes, stats) = ingest::assemble_dataset_streaming_algo(
stream,
!no_pyramid,
text_index,
type_predicate,
pyramid_algo,
|stats, dict, triples| match curated {
Some(curated) => {
let blob = card::derive_card_encoded(
dict,
triples,
stats.statements as u64,
stats.terms as u64,
stats.named_graphs as u64,
curated,
)
.to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
}
None => Vec::new(),
},
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
std::fs::write(output, &bytes)?;
print_build_summary(output, &stats, bytes.len());
return Ok(());
}
let mut quads = parse_inputs(inputs, format)?;
let reasoning = if materialize || reason {
let base: Vec<(String, String, String)> = quads
.iter()
.filter(|(_, _, _, g)| g.is_none())
.map(|(s, p, o, _)| (s.clone(), p.clone(), o.clone()))
.collect();
Some(rete_core::reason(&base))
} else {
None
};
if materialize {
let r = reasoning
.as_ref()
.expect("reasoning computed when materialize");
if !r.inconsistencies.is_empty() {
for inc in &r.inconsistencies {
eprintln!(" incoherent [{}] {}", inc.kind, inc.detail);
}
anyhow::bail!(
"{} inconsistency(ies) — refusing to materialize an incoherent graph",
r.inconsistencies.len()
);
}
let inferred = r.inferred.len();
quads.extend(r.inferred.iter().cloned().map(|(s, p, o)| (s, p, o, None)));
eprintln!("materialized {inferred} inferred triple(s) into the default graph");
}
let curated = if card_args.requested() || reason {
Some(card::load_curated(&card_args)?)
} else {
None
};
let (bytes, stats) = ingest::assemble_dataset_with_opts_algo(
quads,
!no_pyramid,
text_index,
type_predicate,
pyramid_algo,
|stats, quads| match curated {
Some(curated) => {
let mut dataset_card = card::derive_card(
quads,
stats.terms as u64,
stats.named_graphs as u64,
curated,
);
if let Some(r) = reasoning.as_ref() {
dataset_card = dataset_card.with_coherence(r, materialize);
}
let blob = dataset_card.to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
}
None => Vec::new(),
},
);
std::fs::write(output, &bytes)?;
print_build_summary(output, &stats, bytes.len());
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_external_cmd(
inputs: &[String],
output: &str,
format: Option<&str>,
memory_budget_mb: u64,
tmp_dir: Option<&str>,
materialize: bool,
reason: bool,
text_index: bool,
card_args: CardArgs,
) -> anyhow::Result<()> {
if materialize || reason {
anyhow::bail!(
"--memory-budget-mb is incompatible with --materialize/--reason \
(they need the whole graph resident); run them via the standard build"
);
}
if text_index {
anyhow::bail!("--memory-budget-mb does not support --text-index yet");
}
let inputs_fmt: Vec<(&str, &'static str)> = inputs
.iter()
.map(|i| (i.as_str(), input_format(i, format)))
.collect();
let uses_stdin = inputs.iter().any(|i| i == "-");
if uses_stdin && (inputs.len() != 1 || format.is_none()) {
anyhow::bail!(
"--memory-budget-mb with stdin: `-` must be the only input and \
requires an explicit --format nt|nq"
);
}
if let Some((bad, fmt)) = inputs_fmt
.iter()
.find(|(i, f)| *i != "-" && !matches!(*f, "nt" | "nq"))
{
anyhow::bail!(
"--memory-budget-mb streams N-Triples/N-Quads only ({bad} is {fmt}); \
convert Turtle/RDF-XML inputs to .nt first"
);
}
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let out_path = std::path::Path::new(output).to_path_buf();
let stats = rete_core::extbuild::build_external(
|visit| {
for (path, fmt) in &inputs_fmt {
let mut err: Option<rete_core::extbuild::ExtBuildError> = None;
let mut on_quad = |q: ingest::RawQuad| {
if err.is_none() {
if let Err(e) = visit(q) {
err = Some(e);
}
}
};
let res = if *path == "-" {
let stdin = std::io::stdin();
ingest::stream_reader(
std::io::BufReader::with_capacity(1 << 20, stdin.lock()),
fmt,
&mut on_quad,
)
} else {
let file = std::fs::File::open(path).map_err(|e| {
rete_core::extbuild::ExtBuildError::Ingest(ingest::IngestError::Io(
format!("{path}: {e}"),
))
})?;
ingest::stream_reader(
std::io::BufReader::with_capacity(1 << 20, file),
fmt,
&mut on_quad,
)
};
if let Some(e) = err {
return Err(e);
}
res.map_err(rete_core::extbuild::ExtBuildError::Ingest)?;
}
Ok(())
},
&out_path,
rete_core::extbuild::ExternalBuildOptions {
memory_budget: memory_budget_mb.saturating_mul(1 << 20),
tmp_dir: tmp_dir.map(std::path::PathBuf::from),
metadata: Box::new(move |stats| match curated {
Some(curated) => {
let blob = card::curated_counts_card(
stats.statements as u64,
stats.terms as u64,
curated,
)
.to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
}
None => Vec::new(),
}),
},
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let byte_len = std::fs::metadata(output).map(|m| m.len()).unwrap_or(0) as usize;
print_build_summary(output, &stats, byte_len);
Ok(())
}
fn print_build_summary(output: &str, stats: &ingest::BuildStats, byte_len: usize) {
if stats.named_graphs > 0 {
println!(
"wrote {output}: {} quads ({} default + {} named graph(s)), {} terms, {} bytes",
stats.statements, stats.default_triples, stats.named_graphs, stats.terms, byte_len
);
} else {
println!(
"wrote {output}: {} triples, {} terms, {} pyramid level(s), {} bytes",
stats.default_triples, stats.terms, stats.pyramid_levels, byte_len
);
}
}
pub(crate) fn repyramid(
input: &str,
output: &str,
text_index: bool,
type_predicate: Option<&str>,
pyramid_algo: rete_core::PyramidAlgo,
card_args: CardArgs,
) -> anyhow::Result<()> {
let bytes = std::fs::read(input)?;
let rete = rete_core::Rete::open(&bytes)?;
drop(bytes);
let mut quads: Vec<ingest::RawQuad> = rete
.dump(None)
.into_iter()
.map(|(s, p, o)| (s, p, o, None))
.collect();
let named: Vec<String> = rete.graph_names().iter().map(|g| g.to_string()).collect();
for g in &named {
for (s, p, o) in rete.dump(Some(g)) {
quads.push((s, p, o, Some(g.clone())));
}
}
drop(rete);
let curated = if card_args.requested() {
Some(card::load_curated(&card_args)?)
} else {
None
};
let (out_bytes, stats) = ingest::assemble_dataset_with_opts_algo(
quads,
true,
text_index,
type_predicate,
pyramid_algo,
|stats, quads| match curated {
Some(curated) => {
let blob = card::derive_card(
quads,
stats.terms as u64,
stats.named_graphs as u64,
curated,
)
.to_json_bytes();
eprintln!("embedded dataset card ({} bytes of metadata)", blob.len());
blob
}
None => Vec::new(),
},
);
std::fs::write(output, &out_bytes)?;
if stats.named_graphs > 0 {
println!(
"repyramid: wrote {output} — {} quads ({} default + {} named), {} terms, {} bytes",
stats.statements,
stats.default_triples,
stats.named_graphs,
stats.terms,
out_bytes.len()
);
} else {
println!(
"repyramid: wrote {output} — {} triples, {} terms, {} pyramid level(s), {} bytes",
stats.default_triples,
stats.terms,
stats.pyramid_levels,
out_bytes.len()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rete_core::Rete;
#[test]
fn build_materialize_bakes_in_entailments() {
const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
const SUBCLASS: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
let nt = format!("<http://x> {TYPE} <http://C> .\n<http://C> {SUBCLASS} <http://D> .\n");
let pid = std::process::id();
let inp = std::env::temp_dir().join(format!("rete_mat_in_{pid}.nt"));
let out = std::env::temp_dir().join(format!("rete_mat_out_{pid}.rete"));
std::fs::write(&inp, nt).unwrap();
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
true,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
CardArgs::default(),
)
.unwrap();
let bytes = std::fs::read(&out).unwrap();
let rete = Rete::open(&bytes).unwrap();
let inferred = rete.query(Some("<http://x>"), Some(TYPE), Some("<http://D>"));
assert_eq!(inferred.len(), 1, "x a D should be materialized");
build(
&[inp.to_str().unwrap().to_string()],
out.to_str().unwrap(),
None,
false,
false,
false,
rete_core::PyramidAlgo::Louvain,
false,
None,
CardArgs::default(),
)
.unwrap();
let plain = Rete::open(&std::fs::read(&out).unwrap()).unwrap();
assert!(plain
.query(Some("<http://x>"), Some(TYPE), Some("<http://D>"))
.is_empty());
std::fs::remove_file(&inp).ok();
std::fs::remove_file(&out).ok();
}
}