mod common;
const OBJECT_QUOTED: &str = concat!(
"<http://ex/occ1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> .\n",
"<http://ex/jsmith> <http://ex/recorded> << <http://ex/occ1> \
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> >> .\n",
"<http://ex/claim1> <http://ex/states> << <http://ex/occ1> <http://ex/count> \
\"5\"^^<http://www.w3.org/2001/XMLSchema#integer> >> .\n",
"<http://ex/claim2> <http://ex/states> << <http://ex/a> <http://ex/b> \
<< <http://ex/x> <http://ex/y> <http://ex/z> >> >> <http://ex/g1> .\n",
"_:b0 <http://ex/notes> << _:b1 <http://ex/b> \"a > b << c\"@en >> <http://ex/g1> .\n",
);
const OBJECT_QUOTED_RDF12: &str = concat!(
"<http://ex/occ1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> .\n",
"<http://ex/jsmith> <http://ex/recorded> <<( <http://ex/occ1> \
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> )>> .\n",
"<http://ex/claim1> <http://ex/states> <<( <http://ex/occ1> <http://ex/count> \
\"5\"^^<http://www.w3.org/2001/XMLSchema#integer> )>> .\n",
"<http://ex/claim2> <http://ex/states> <<( <http://ex/a> <http://ex/b> \
<<( <http://ex/x> <http://ex/y> <http://ex/z> )>> )>> <http://ex/g1> .\n",
"_:b0 <http://ex/notes> <<( _:b1 <http://ex/b> \"a > b << c\"@en )>> <http://ex/g1> .\n",
);
const SUBJECT_QUOTED: &str = concat!(
"<http://ex/occ1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> .\n",
"<< <http://ex/occ1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Swallow> >> \
<http://ex/recordedBy> <http://ex/jsmith> .\n",
);
const NESTED_SUBJECT_QUOTED: &str = "<http://ex/claim> <http://ex/states> \
<< << <http://ex/x> <http://ex/y> <http://ex/z> >> <http://ex/p> <http://ex/o> >> .\n";
const PLAIN: &str = concat!(
"<http://ex/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/T> .\n",
"<http://ex/a> <http://ex/p> \"x\"@en .\n",
"<http://ex/a> <http://ex/q> _:b0 .\n",
"<http://ex/c> <http://ex/p> <http://ex/3> <http://ex/g1> .\n",
);
fn build_from(nq: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.nq");
let out = dir.path().join("out.rete");
std::fs::write(&src, nq).unwrap();
common::build(&src, &out, &["--no-pyramid"]);
(dir, out)
}
fn run(file: &std::path::Path, args: &[&str]) -> std::process::Output {
common::rete()
.arg("export")
.arg(file)
.args(args)
.output()
.unwrap()
}
fn export(file: &std::path::Path, args: &[&str]) -> String {
let out = run(file, args);
assert!(
out.status.success(),
"export {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn identity(file: &std::path::Path) -> Vec<String> {
let mut v: Vec<String> = export(
file,
&["--format", "nq", "--quoted-triple-syntax", "rdf-star"],
)
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_string)
.collect();
v.sort();
v
}
fn rebuild(dir: &tempfile::TempDir, text: &str, fmt: &str) -> std::path::PathBuf {
let src = dir.path().join(format!("round.{fmt}"));
let out = dir.path().join(format!("round-{fmt}.rete"));
std::fs::write(&src, text).unwrap();
common::build(&src, &out, &["--no-pyramid", "--format", fmt]);
out
}
#[test]
fn the_default_surface_is_the_rdf12_triple_term() {
let (_dir, file) = build_from(OBJECT_QUOTED);
for fmt in ["nq", "trig"] {
let out = export(&file, &["--format", fmt]);
assert!(
out.contains(
"<<( <http://ex/occ1> <http://ex/count> \
\"5\"^^<http://www.w3.org/2001/XMLSchema#integer> )>>"
),
"{fmt} should carry RDF 1.2 triple terms:\n{out}"
);
assert!(
!out.contains("<<<"),
"{fmt} still carries the RDF-star surface:\n{out}"
);
}
}
#[test]
fn nesting_is_rewritten_at_every_depth() {
let (_dir, file) = build_from(OBJECT_QUOTED);
let nq = export(&file, &["--format", "nq"]);
assert!(
nq.contains(
"<<( <http://ex/a> <http://ex/b> \
<<( <http://ex/x> <http://ex/y> <http://ex/z> )>> )>>"
),
"the inner triple term must be rewritten too:\n{nq}"
);
}
#[test]
fn a_literal_inside_a_triple_term_is_not_disturbed() {
let (_dir, file) = build_from(OBJECT_QUOTED);
let nq = export(&file, &["--format", "nq"]);
assert!(
nq.contains("<<( _:b1 <http://ex/b> \"a > b << c\"@en )>>"),
"the literal must survive verbatim:\n{nq}"
);
}
#[test]
fn the_rdf_star_surface_is_still_available_and_exact() {
let (_dir, file) = build_from(OBJECT_QUOTED);
for fmt in ["nq", "trig"] {
let out = export(
&file,
&["--format", fmt, "--quoted-triple-syntax", "rdf-star"],
);
assert!(
out.contains(
"<<<http://ex/occ1> <http://ex/count> \
\"5\"^^<http://www.w3.org/2001/XMLSchema#integer>>>"
),
"{fmt} --quoted-triple-syntax rdf-star should carry the stored token:\n{out}"
);
assert!(
!out.contains("<<("),
"{fmt} must not mix in RDF 1.2 triple terms:\n{out}"
);
}
}
#[test]
fn an_unknown_surface_is_rejected_by_name() {
let (_dir, file) = build_from(PLAIN);
let out = run(
&file,
&["--format", "nq", "--quoted-triple-syntax", "rdf-1.2"],
);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("rdf12") && err.contains("rdf-star"),
"the error should name both surfaces: {err}"
);
}
#[test]
fn both_surfaces_rebuild_into_the_same_graph() {
let (dir, file) = build_from(OBJECT_QUOTED);
let want = identity(&file);
for (label, args) in [
("rdf12", &["--format", "nq"][..]),
(
"rdf-star",
&["--format", "nq", "--quoted-triple-syntax", "rdf-star"][..],
),
] {
let dump = export(&file, args);
let back = rebuild(&dir, &dump, "nq");
assert_eq!(
identity(&back),
want,
"the {label} surface did not round-trip"
);
}
}
#[test]
fn trig_round_trips_through_rete_in_the_rdf_star_surface() {
let (dir, file) = build_from(OBJECT_QUOTED);
let dump = export(
&file,
&["--format", "trig", "--quoted-triple-syntax", "rdf-star"],
);
let back = rebuild(&dir, &dump, "trig");
assert_eq!(identity(&back), identity(&file));
}
#[test]
fn a_rdf12_trig_dump_is_not_readable_by_retes_own_trig_parser_yet() {
let (dir, file) = build_from(OBJECT_QUOTED);
let out = common::rete()
.arg("export")
.arg(&file)
.args(["--format", "trig"])
.output()
.unwrap();
assert!(out.status.success());
let note = String::from_utf8_lossy(&out.stderr);
assert!(
note.contains("RDF 1.2 triple term") && note.contains("--quoted-triple-syntax rdf-star"),
"the export must warn that rete cannot read this back: {note}"
);
let src = dir.path().join("rdf12.trig");
std::fs::write(&src, String::from_utf8(out.stdout).unwrap()).unwrap();
let build = common::rete()
.arg("build")
.arg(&src)
.arg("-o")
.arg(dir.path().join("rdf12-trig.rete"))
.args(["--no-pyramid", "--format", "trig"])
.output()
.unwrap();
assert!(
!build.status.success(),
"if this now passes, oxttl was upgraded — delete this test and the note in `export`"
);
}
#[test]
fn ingest_accepts_both_input_surfaces_as_the_same_graph() {
let (_a, star) = build_from(OBJECT_QUOTED);
let (_b, rdf12) = build_from(OBJECT_QUOTED_RDF12);
assert_eq!(
identity(&star),
identity(&rdf12),
"an RDF-star file and an RDF 1.2 file must build into the same graph"
);
}
#[test]
fn a_subject_position_quoted_triple_is_refused_by_name() {
let (_dir, file) = build_from(SUBJECT_QUOTED);
for fmt in ["nq", "trig", "ttl"] {
let out = run(&file, &["--format", fmt]);
assert!(
!out.status.success(),
"{fmt} should refuse a subject-position quoted triple"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("subject position") && err.contains("OBJECT position only"),
"{fmt}: the refusal should name the slot and the rule: {err}"
);
assert!(
err.contains("--quoted-triple-syntax rdf-star"),
"{fmt}: the refusal should name the way out: {err}"
);
}
}
#[test]
fn the_same_graph_still_exports_in_the_rdf_star_surface() {
let (dir, file) = build_from(SUBJECT_QUOTED);
let dump = export(
&file,
&["--format", "nq", "--quoted-triple-syntax", "rdf-star"],
);
assert!(dump.contains("<<<http://ex/occ1>"), "{dump}");
let back = rebuild(&dir, &dump, "nq");
assert_eq!(identity(&back), identity(&file));
}
#[test]
fn a_quoted_triple_nested_in_a_subject_is_refused_too() {
let (_dir, file) = build_from(NESTED_SUBJECT_QUOTED);
let out = run(&file, &["--format", "nq"]);
assert!(!out.status.success(), "the nested case must be caught too");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("nested in the SUBJECT of another quoted triple"),
"the refusal should name the nested case: {err}"
);
}
#[test]
fn hdt_refuses_a_file_with_quoted_triples() {
let (_dir, file) = build_from(OBJECT_QUOTED);
let out = run(&file, &["--format", "hdt"]);
assert!(!out.status.success(), "HDT should refuse");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("no term kind for one") && err.contains("--format trig"),
"the refusal should say why and where to go: {err}"
);
assert!(
out.stdout.is_empty(),
"nothing should have been written to stdout"
);
}
#[test]
fn jsonld_refuses_a_file_with_quoted_triples() {
let (_dir, file) = build_from(OBJECT_QUOTED);
let out = run(&file, &["--format", "jsonld"]);
assert!(!out.status.success(), "JSON-LD should refuse");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("no term kind for one"), "{err}");
}
#[test]
fn hdt_and_jsonld_are_untouched_without_quoted_triples() {
let (_dir, file) = build_from(PLAIN);
assert!(run(&file, &["--format", "hdt"]).status.success());
assert!(run(&file, &["--format", "jsonld"]).status.success());
}
#[test]
fn a_file_with_no_quoted_triples_is_byte_identical_in_either_surface() {
let (_dir, file) = build_from(PLAIN);
for fmt in ["nq", "trig", "ttl"] {
let default = export(&file, &["--format", fmt]);
let star = export(
&file,
&["--format", fmt, "--quoted-triple-syntax", "rdf-star"],
);
assert_eq!(default, star, "{fmt} differs between the two surfaces");
assert!(!default.is_empty(), "{fmt} wrote nothing");
}
}