Skip to content

Commit d2c14e9

Browse files
Eric Prud'hommeauxclaude
andcommitted
+ bin/sparql --shex-endpoint: ShEx validation over a live SPARQL endpoint
--shex-endpoint <URL> --shex-schema <path/URL> --shex-map <text> validates a shape map against a live SPARQL protocol endpoint instead of an in-memory graph. Wiring: HTTPSPARQLClient -> bnr::RemoteGraphProvider -> Validator::neighborhoodSource fetches each node's neighborhood on demand as validation visits it, exactly the hook lib/ShExSchema.hpp already carried for this. Fixed associations (node@shape, node@!shape, @start) work directly; --shex-map's node-side prefixes borrow the schema's own PREFIX declarations, since there's no separate data document here. lib/ShExShapeMap.{hpp,cpp}: {FOCUS <p> _} / {_ <p> FOCUS} triple-pattern matching is now abstracted behind a PatternMatcher interface (matchFocusSubject/matchFocusObject) instead of hardcoded local-graph iteration. LocalPatternMatcher preserves the exact prior behavior (all --shex-manifest tests unaffected); bin/sparql's new RemotePatternMatcher resolves a pattern's FOCUS candidates with one SPARQL round trip through BNodeResolver::select, so a bnode FOCUS is told-bnode-safe. tests: shex_query_map_over_endpoint mirrors RemotePatternMatcher against the in-process MiniEndpoint (it's CLI-only code, not in the library) and checks the remote {FOCUS ex:member _} resolution finds and validates exactly the same groups a local scan would, bnode memberships included. Verified manually end to end against bin/sparql --serve: fixed maps, @! negation, query-pattern FOCUS resolution over bnode-membership groups, and correct nonconformant/exit-code reporting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8b4dc2c commit d2c14e9

5 files changed

Lines changed: 295 additions & 21 deletions

File tree

‎README‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@ packages/extension-map examples):
7272

7373
bin/sparql --shex-manifest tests/ShExMap/manifest.yaml
7474

75+
--shex-endpoint validates a shape map against a *live* SPARQL protocol
76+
endpoint instead of an in-memory graph: node neighborhoods are fetched
77+
and cached as validation visits them (lib/BNodeResolver.hpp's
78+
RemoteGraphProvider wired in as Validator::NeighborhoodSource), so
79+
response blank nodes crossing the round trips this needs are told-bnode-
80+
safe (see the algae3 repo's doc/told-bnodes.md). Fixed associations and
81+
ONE query pattern are both supported — the pattern's FOCUS candidates
82+
are fetched with a single additional round trip
83+
(lib/ShExShapeMap.hpp's PatternMatcher abstracts this from the existing
84+
local-graph matching, now LocalPatternMatcher). Demo (from tests/, with
85+
bin/sparql itself serving the evidence data):
86+
87+
../bin/sparql --serve http://127.0.0.1:8099/sparql \
88+
-d Algae3/data/evidence.ttl -d Algae3/data/evidence-groups.ttl
89+
../bin/sparql --shex-endpoint http://127.0.0.1:8099/sparql \
90+
--shex-schema schema.shex \
91+
--shex-map 'evgrp:PeerReviewedCohorts@<GroupShape>'
92+
# or validate every node matching a pattern in one go:
93+
../bin/sparql --shex-endpoint http://127.0.0.1:8099/sparql \
94+
--shex-schema schema.shex --shex-map '{FOCUS ex:member _}@<GroupShape>'
95+
96+
Node-side prefixes in --shex-map borrow the schema's own PREFIX
97+
declarations (there's no separate "data document" to carry its own).
98+
"@!shape" asserts the node should NOT conform (exit code counts
99+
mismatches, matching --shex-manifest's PASS/FAIL convention).
100+
75101
# Algae 3
76102

77103
lib/Algae3Parser.ypp, lib/Algae3Scanner.lpp and lib/Algae3.{hpp,cpp}

‎bin/sparql.cpp‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ namespace sw = w3c_sw;
5252

5353
#include "SimpleServer.hpp"
5454
#include "ShExManifest.hpp"
55+
#include "ShExCParser.hpp"
56+
#include "BNodeResolver.hpp"
5557

5658
#ifndef _MSC_VER
5759
#include <dlfcn.h>
@@ -814,6 +816,36 @@ std::string adjustPath (std::string nameStr) {
814816
return nameStr;
815817
}
816818

819+
/** --shex-endpoint's PatternMatcher: a {FOCUS <p> o} / {s <p> FOCUS} query
820+
* pattern draws its candidates from one SPARQL round trip through the
821+
* BNodeResolver (told-bnode-safe: a bnode FOCUS gets a stable proxy the
822+
* same way a neighborhood fetch would). */
823+
struct RemotePatternMatcher : public sw::ShEx::PatternMatcher {
824+
sw::bnr::BNodeResolver& resolver;
825+
RemotePatternMatcher (sw::bnr::BNodeResolver& resolver) : resolver(resolver) { }
826+
827+
virtual void matchFocusSubject (const sw::TTerm* p, const sw::TTerm* o,
828+
std::vector<const sw::TTerm*>& into) {
829+
run("?bnrFocus " + sw::bnr::sparqlTerm(p) + " "
830+
+ (o == NULL ? "?bnrAny" : sw::bnr::sparqlTerm(o)) + " . ", into);
831+
}
832+
virtual void matchFocusObject (const sw::TTerm* s, const sw::TTerm* p,
833+
std::vector<const sw::TTerm*>& into) {
834+
run((s == NULL ? "?bnrAny" : sw::bnr::sparqlTerm(s)) + " "
835+
+ sw::bnr::sparqlTerm(p) + " ?bnrFocus . ", into);
836+
}
837+
838+
private:
839+
void run (const std::string& where, std::vector<const sw::TTerm*>& into) {
840+
sw::bnr::Table t = resolver.select("SELECT DISTINCT ?bnrFocus WHERE { " + where + "}", where);
841+
for (sw::bnr::Table::const_iterator r = t.begin(); r != t.end(); ++r) {
842+
sw::bnr::Row::const_iterator it = r->find("bnrFocus");
843+
if (it != r->end())
844+
into.push_back(it->second);
845+
}
846+
}
847+
};
848+
817849
sw::Operation* parseQuery (const sw::TTerm* query) {
818850
std::string querySpec = query->getLexicalValue();
819851
sw::IStreamContext::e_opts opts =
@@ -1019,6 +1051,22 @@ int main(int ac, char* av[])
10191051
" index: [0-9]+ | '*'\n"
10201052
"0-based; '*' as a range or endpoint selects all entries; \"5-2\" "
10211053
"runs 5,4,3,2; e.g. \"2-5,*\" runs 2,3,4,5 and then everything.")
1054+
("shex-endpoint", po::value<std::string>(),
1055+
"validate --shex-map against live data at this SPARQL protocol "
1056+
"endpoint (plain http; used with --shex-schema and --shex-map). "
1057+
"Response blank nodes are re-identified across the round trips "
1058+
"this needs by lib/BNodeResolver.hpp (see the algae3 repo's "
1059+
"doc/told-bnodes.md); node neighborhoods are fetched and cached "
1060+
"as validation visits them.")
1061+
("shex-schema", po::value<std::string>(),
1062+
"ShExC schema (file path or URL) for --shex-endpoint")
1063+
("shex-map", po::value<std::string>(),
1064+
"shape map for --shex-endpoint: fixed associations "
1065+
"(\"node@shape,node@!shape,...\"; \"@START\" for the start shape; "
1066+
"\"@!shape\" asserts the node should NOT conform) or ONE query "
1067+
"pattern (\"{FOCUS <p> _}@shape\" / \"{_ <p> FOCUS}@shape\"), "
1068+
"whose FOCUS candidates are fetched from the endpoint with one "
1069+
"round trip before validating each.")
10221070
;
10231071

10241072
po::options_description cmdline_options;
@@ -1097,6 +1145,76 @@ int main(int ac, char* av[])
10971145
return failures > 125 ? 125 : failures;
10981146
}
10991147

1148+
if (vm.count("shex-endpoint")) {
1149+
/* Validate a shape map against a live SPARQL endpoint and exit. */
1150+
if (!vm.count("shex-schema") || !vm.count("shex-map")) {
1151+
std::cerr << "--shex-endpoint requires --shex-schema and --shex-map\n";
1152+
return 2;
1153+
}
1154+
sw::AtomFactory atomFactory;
1155+
std::string endpoint = vm["shex-endpoint"].as<std::string>();
1156+
std::string schemaSpec = vm["shex-schema"].as<std::string>();
1157+
1158+
sw::ShEx::Schema schema;
1159+
sw::ShExDriver schemaDriver(schemaSpec, &atomFactory);
1160+
sw::RdfDB db(NULL, &TheServer.engine.xmlParser);
1161+
sw::BasicGraphPattern* cache = db.ensureGraph(sw::DefaultGraph);
1162+
std::vector<sw::ShEx::Association> associations;
1163+
sw::bnr::HTTPSPARQLClient* client = NULL;
1164+
try {
1165+
sw::IStreamContext sistr(schemaSpec, sw::IStreamContext::STDIN,
1166+
NULL, &TheServer.engine.webClient);
1167+
schemaDriver.parse(sistr, &schema);
1168+
schema.checkStructure();
1169+
1170+
client = new sw::bnr::HTTPSPARQLClient(&atomFactory, endpoint);
1171+
sw::bnr::RemoteGraphProvider provider(&atomFactory, client, cache);
1172+
RemotePatternMatcher matcher(provider.resolver);
1173+
/* no separate "data document" exists here, so the map's node
1174+
* side borrows the schema's PREFIX declarations too */
1175+
sw::ShEx::PrefixEnv nodeEnv(endpoint, schemaDriver.getNamespaceMap());
1176+
sw::ShEx::PrefixEnv shapeEnv(schemaSpec, schemaDriver.getNamespaceMap());
1177+
associations = sw::ShEx::parseQueryMap(vm["shex-map"].as<std::string>(),
1178+
&atomFactory, nodeEnv, shapeEnv, matcher);
1179+
if (associations.empty()) {
1180+
std::cerr << "shape map matched no nodes\n";
1181+
delete client;
1182+
return 1;
1183+
}
1184+
1185+
struct Src : public sw::ShEx::Validator::NeighborhoodSource {
1186+
sw::bnr::RemoteGraphProvider& p;
1187+
Src (sw::bnr::RemoteGraphProvider& p) : p(p) { }
1188+
virtual void ensure (const sw::TTerm* focus) { p.ensureNode(focus); }
1189+
} src(provider);
1190+
1191+
sw::ShEx::Validator validator(schema, *cache);
1192+
validator.neighborhoodSource = &src;
1193+
int failures = 0;
1194+
for (std::vector<sw::ShEx::Association>::const_iterator it = associations.begin();
1195+
it != associations.end(); ++it) {
1196+
bool conformant = validator.validate(it->node, it->shape);
1197+
bool asAsserted = conformant != it->negated;
1198+
std::cout << it->node->toString() << "@" << (it->negated ? "!" : "")
1199+
<< (it->shape ? it->shape->toString() : std::string("START"))
1200+
<< ": " << (conformant ? "conformant" : "nonconformant")
1201+
<< (asAsserted ? "" : " -- unexpected") << "\n";
1202+
if (!asAsserted)
1203+
++failures;
1204+
}
1205+
delete client;
1206+
return failures > 125 ? 125 : failures;
1207+
} catch (std::exception& e) {
1208+
std::cerr << "shex-endpoint: " << e.what() << "\n";
1209+
delete client;
1210+
return 1;
1211+
} catch (std::string& e) {
1212+
std::cerr << "shex-endpoint: " << e << "\n";
1213+
delete client;
1214+
return 1;
1215+
}
1216+
}
1217+
11001218
if (vm.count("post")) {
11011219
w3c_sw_LOG(IOLog, sw::Logger::info) << "Using HTTP POST.\n";
11021220
sw::ServiceGraphPattern::defaultHTTPmethod = sw::ServiceGraphPattern::HTTP_METHOD_POST;

‎lib/ShExShapeMap.cpp‎

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ namespace ShEx {
1717
AtomFactory* atomFactory;
1818
const PrefixEnv& nodeEnv;
1919
const PrefixEnv& shapeEnv;
20-
const BasicGraphPattern& data;
20+
PatternMatcher& matcher;
2121

2222
MapParser (const std::string& text, AtomFactory* atomFactory,
2323
const PrefixEnv& nodeEnv, const PrefixEnv& shapeEnv,
24-
const BasicGraphPattern& data)
24+
PatternMatcher& matcher)
2525
: text(text), pos(0), atomFactory(atomFactory),
26-
nodeEnv(nodeEnv), shapeEnv(shapeEnv), data(data) { }
26+
nodeEnv(nodeEnv), shapeEnv(shapeEnv), matcher(matcher) { }
2727

2828
void error (const std::string& msg) {
2929
std::stringstream ss;
@@ -179,20 +179,10 @@ namespace ShEx {
179179
error("triple pattern predicate must be an IRI");
180180
if ((skind == 0) == (okind == 0))
181181
error("triple pattern needs FOCUS in subject or object");
182-
for (std::vector<const TriplePattern*>::const_iterator it = data.begin();
183-
it != data.end(); ++it) {
184-
if ((*it)->getP() != p)
185-
continue;
186-
if (skind == 0) { // subject is FOCUS
187-
if (okind == 2 && (*it)->getO() != o)
188-
continue;
189-
into.push_back((*it)->getS());
190-
} else { // object is FOCUS
191-
if (skind == 2 && (*it)->getS() != s)
192-
continue;
193-
into.push_back((*it)->getO());
194-
}
195-
}
182+
if (skind == 0) // subject is FOCUS
183+
matcher.matchFocusSubject(p, okind == 2 ? o : NULL, into);
184+
else // object is FOCUS
185+
matcher.matchFocusObject(skind == 2 ? s : NULL, p, into);
196186
return;
197187
}
198188
if (c == '_' && pos + 1 < text.size() && text[pos+1] == ':') {
@@ -261,13 +251,29 @@ namespace ShEx {
261251
};
262252
} // namespace
263253

254+
void LocalPatternMatcher::matchFocusSubject (const TTerm* p, const TTerm* o,
255+
std::vector<const TTerm*>& into) {
256+
for (std::vector<const TriplePattern*>::const_iterator it = data.begin();
257+
it != data.end(); ++it)
258+
if ((*it)->getP() == p && (o == NULL || (*it)->getO() == o))
259+
into.push_back((*it)->getS());
260+
}
261+
262+
void LocalPatternMatcher::matchFocusObject (const TTerm* s, const TTerm* p,
263+
std::vector<const TTerm*>& into) {
264+
for (std::vector<const TriplePattern*>::const_iterator it = data.begin();
265+
it != data.end(); ++it)
266+
if ((*it)->getP() == p && (s == NULL || (*it)->getS() == s))
267+
into.push_back((*it)->getO());
268+
}
269+
264270
std::vector<Association> parseQueryMap (const std::string& text,
265271
AtomFactory* atomFactory,
266272
const PrefixEnv& nodeEnv,
267273
const PrefixEnv& shapeEnv,
268-
const BasicGraphPattern& data,
274+
PatternMatcher& matcher,
269275
TTerm::String2BNode* dataBNodeMap) {
270-
MapParser p(text, atomFactory, nodeEnv, shapeEnv, data);
276+
MapParser p(text, atomFactory, nodeEnv, shapeEnv, matcher);
271277
TTerm::String2BNode transientBNodes;
272278
return p.parse(dataBNodeMap != NULL ? dataBNodeMap : &transientBNodes);
273279
}

‎lib/ShExShapeMap.hpp‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,54 @@ namespace ShEx {
5353
: base(base), namespaces(namespaces) { }
5454
};
5555

56+
/** Where a {FOCUS <p> o} / {s <p> FOCUS} triple pattern in a shape map
57+
* draws its candidates from: a local graph already in memory (the
58+
* default; see LocalPatternMatcher) or, e.g., a live SPARQL endpoint
59+
* (see bin/sparql's --shex-endpoint, which implements one against
60+
* bnr::BNodeResolver so candidate blank nodes are told-bnode-safe). */
61+
struct PatternMatcher {
62+
virtual ~PatternMatcher () { }
63+
/** ?FOCUS p o ; o == NULL matches any object. */
64+
virtual void matchFocusSubject (const TTerm* p, const TTerm* o,
65+
std::vector<const TTerm*>& into) = 0;
66+
/** s p ?FOCUS ; s == NULL matches any subject. */
67+
virtual void matchFocusObject (const TTerm* s, const TTerm* p,
68+
std::vector<const TTerm*>& into) = 0;
69+
};
70+
71+
/** The original behavior: matches within a BasicGraphPattern already
72+
* resident in memory. */
73+
struct LocalPatternMatcher : public PatternMatcher {
74+
const BasicGraphPattern& data;
75+
LocalPatternMatcher (const BasicGraphPattern& data) : data(data) { }
76+
virtual void matchFocusSubject (const TTerm* p, const TTerm* o,
77+
std::vector<const TTerm*>& into);
78+
virtual void matchFocusObject (const TTerm* s, const TTerm* p,
79+
std::vector<const TTerm*>& into);
80+
};
81+
5682
/** Parse a query/fixed shape map in the compact syntax, expanding triple
57-
* patterns against data. "_:label" nodes are resolved through
83+
* patterns through `matcher`. "_:label" nodes are resolved through
5884
* dataBNodeMap when given. Throws ShapeMapError. */
5985
std::vector<Association> parseQueryMap (const std::string& text,
6086
AtomFactory* atomFactory,
6187
const PrefixEnv& nodeEnv,
6288
const PrefixEnv& shapeEnv,
63-
const BasicGraphPattern& data,
89+
PatternMatcher& matcher,
6490
TTerm::String2BNode* dataBNodeMap = NULL);
6591

92+
/** Convenience overload for a shape map matched against a local graph
93+
* (the pre-existing behavior; used by manifest-style tests). */
94+
inline std::vector<Association> parseQueryMap (const std::string& text,
95+
AtomFactory* atomFactory,
96+
const PrefixEnv& nodeEnv,
97+
const PrefixEnv& shapeEnv,
98+
const BasicGraphPattern& data,
99+
TTerm::String2BNode* dataBNodeMap = NULL) {
100+
LocalPatternMatcher matcher(data);
101+
return parseQueryMap(text, atomFactory, nodeEnv, shapeEnv, matcher, dataBNodeMap);
102+
}
103+
66104
/** Parse the shexTest JSON fixed map format:
67105
* [{"node": "iri-or-literal", "shape": "iri"}, ...]. */
68106
std::vector<Association> parseJsonMap (const std::string& text,

0 commit comments

Comments
 (0)