Skip to content

Commit 19a38cf

Browse files
Eric Prud'hommeauxclaude
andcommitted
+ ShEx: EXTERNAL resolution, shape maps, JSON/YAML manifests, CLI
EXTERNAL: ShEx::ExternalResolver supplies definitions for EXTERNAL declarations, keyed by the declaration's label; SchemaExternalResolver resolves against another schema (e.g. a shexTest .shextern document). The shapeExtern shexTest cases now run un-skipped. Shape maps (lib/ShExShapeMap.{hpp,cpp}): fixed and query maps in the compact syntax -- node@shape, node@!shape (asserting nonconformance), START, and {FOCUS <p> _} / {_ <p> FOCUS} triple patterns expanded against the data. Node-side terms resolve with the data document's base/prefixes, shape-side terms with the schema's. The shexTest JSON map format ([{node, shape}]) is read via the new lib/MiniJSON.hpp, and the three sht:map manifest entries (with mf:result comparison) now run in test_ShEx. Manifests (lib/ShExManifest.{hpp,cpp}): the shex-webapp validation manifest format -- entries with schema[URL], data[URL], queryMap[URL] and an expected conformant/nonconformant status -- readable from JSON or from the YAML subset the shex-webapp examples use (top-level sequence of flat mappings with plain, quoted and '|' literal block scalars). An entry passes when every association holds as asserted. CLI: bin/sparql --shex-manifest FILE [--shex-run RANGES] runs manifest entries and reports per-association results; exit status is the number of failed entries. RANGES follows entry: range (',' range)* ','? range: index ('-' index)? index: [0-9]+ | '*' 0-based, '*' (alone or as an endpoint) selects everything, "5-2" runs descending, "2-5,*" runs 2..5 then all. tests/ShExManifest/ holds YAML and JSON fixtures (inline and by-URL entries) exercised from test_ShEx along with range-expansion checks; all 1735 shexTest + fixture cases pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0efeba2 commit 19a38cf

16 files changed

Lines changed: 1507 additions & 24 deletions

‎CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ add_library(SWObjects STATIC
163163
lib/RdfQueryDB.cpp
164164
lib/ResultSet.cpp
165165
lib/ShExSchema.cpp
166+
lib/ShExShapeMap.cpp
167+
lib/ShExManifest.cpp
166168
lib/SWObjects.cpp
167169
${SWOBJ_GENERATED_SOURCES}
168170
)

‎README‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,23 @@ On macOS: brew install cmake boost bison flex expat
4343
lib/ShExCParser.ypp and lib/ShExSchema.{hpp,cpp} implement ShEx 2.1
4444
(ShExC syntax including IMPORT, ABSTRACT and EXTENDS; validation by
4545
interval-based matching over SORBE-normalized triple expressions, after
46-
jena-shex). tests/test_ShEx.cpp runs the shexSpec/shexTest suite
47-
(schemas, negativeSyntax, negativeStructure and validation manifests);
46+
jena-shex). EXTERNAL shapes resolve through ShEx::ExternalResolver
47+
(e.g. against a .shextern schema). lib/ShExShapeMap.{hpp,cpp} parses
48+
fixed and query shape maps (node@shape, node@!shape, START,
49+
{FOCUS <p> _} patterns) and the shexTest JSON map format.
50+
tests/test_ShEx.cpp runs the shexSpec/shexTest suite (schemas,
51+
negativeSyntax, negativeStructure and validation manifests);
4852
tests/fetch-test-suites.sh clones the test data.
4953

54+
bin/sparql runs shex-webapp-style validation manifests (JSON or YAML;
55+
see lib/ShExManifest.hpp and tests/ShExManifest/):
56+
57+
bin/sparql --shex-manifest tests/ShExManifest/manifest.yaml
58+
bin/sparql --shex-manifest m.json --shex-run '2-5,0'
59+
60+
--shex-run selects 0-based entries: ranges like "1", "2-5", "5-2"
61+
(descending) or "*" (everything), comma-separated.
62+
5063
# More info
5164

5265
To learn more about SWObjects, checkout some tutorials given at SWAT4LS in 2010 and 2011:

‎bin/sparql.cpp‎

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ namespace sw = w3c_sw;
5050
#endif /* BOOST_PROCESS */
5151

5252
#include "SimpleServer.hpp"
53+
#include "ShExManifest.hpp"
5354

5455
#ifndef _MSC_VER
5556
#include <dlfcn.h>
@@ -1004,14 +1005,29 @@ int main(int ac, char* av[])
10041005
("trap-sig-int", po::value<bool>(), "0|1 no|yes")
10051006
;
10061007

1008+
po::options_description shexOpts("ShEx options");
1009+
shexOpts.add_options()
1010+
("shex-manifest", po::value<std::string>(),
1011+
"run a shex-webapp-style validation manifest (JSON or YAML): a list "
1012+
"of entries with schema[URL], data[URL], queryMap[URL] and an "
1013+
"expected status of conformant or nonconformant.")
1014+
("shex-run", po::value<std::string>()->default_value("*"),
1015+
"which manifest entries to run:\n"
1016+
" entry: range (',' range)* ','?\n"
1017+
" range: index ('-' index)?\n"
1018+
" index: [0-9]+ | '*'\n"
1019+
"0-based; '*' as a range or endpoint selects all entries; \"5-2\" "
1020+
"runs 5,4,3,2; e.g. \"2-5,*\" runs 2,3,4,5 and then everything.")
1021+
;
1022+
10071023
po::options_description cmdline_options;
1008-
cmdline_options.add(generalOpts).add(resultsOpts).add(uriOpts).add(dataOpts).add(httpOpts).add(sqlOpts).add(hidden);
1024+
cmdline_options.add(generalOpts).add(resultsOpts).add(uriOpts).add(dataOpts).add(httpOpts).add(sqlOpts).add(shexOpts).add(hidden);
10091025

10101026
po::options_description config_file_options;
10111027
config_file_options.add(resultsOpts).add(uriOpts).add(dataOpts).add(httpOpts).add(sqlOpts).add(hidden);
10121028

10131029
po::options_description visible("");
1014-
visible.add(generalOpts).add(resultsOpts).add(uriOpts).add(httpOpts).add(sqlOpts).add(dataOpts);
1030+
visible.add(generalOpts).add(resultsOpts).add(uriOpts).add(httpOpts).add(sqlOpts).add(shexOpts).add(dataOpts);
10151031

10161032
po::options_description cursory("");
10171033
cursory.add(generalOpts).add(resultsOpts).add(uriOpts).add(dataOpts);
@@ -1035,6 +1051,45 @@ int main(int ac, char* av[])
10351051
sw::YaccDriver::defaultDescriptiveBNodeLabels = true;
10361052
}
10371053

1054+
if (vm.count("shex-manifest")) {
1055+
/* Run a shex-webapp validation manifest and exit. */
1056+
sw::AtomFactory atomFactory;
1057+
sw::ShEx::Manifest manifest
1058+
= sw::ShEx::Manifest::load(vm["shex-manifest"].as<std::string>());
1059+
std::vector<size_t> toRun
1060+
= sw::ShEx::expandRanges(vm["shex-run"].as<std::string>(),
1061+
manifest.entries.size());
1062+
int failures = 0;
1063+
for (std::vector<size_t>::const_iterator it = toRun.begin();
1064+
it != toRun.end(); ++it) {
1065+
const sw::ShEx::ManifestEntry& entry = manifest.entries[*it];
1066+
sw::ShEx::EntryOutcome outcome
1067+
= sw::ShEx::runEntry(entry, manifest, atomFactory);
1068+
std::cout << "[" << *it << "] " << entry.schemaLabel
1069+
<< " / " << entry.dataLabel << ": ";
1070+
if (!outcome.error.empty()) {
1071+
++failures;
1072+
std::cout << "ERROR " << outcome.error << "\n";
1073+
continue;
1074+
}
1075+
std::cout << (outcome.allAsAsserted ? "conformant" : "nonconformant")
1076+
<< ", expected "
1077+
<< (entry.expectConformant ? "conformant" : "nonconformant")
1078+
<< (outcome.statusMatched ? " -- PASS" : " -- FAIL") << "\n";
1079+
for (std::vector<sw::ShEx::AssociationResult>::const_iterator r
1080+
= outcome.results.begin(); r != outcome.results.end(); ++r)
1081+
std::cout << " " << r->assoc.node->toString()
1082+
<< "@" << (r->assoc.negated ? "!" : "")
1083+
<< (r->assoc.shape ? r->assoc.shape->toString()
1084+
: std::string("START"))
1085+
<< ": " << (r->conformant ? "conformant" : "nonconformant")
1086+
<< "\n";
1087+
if (!outcome.statusMatched)
1088+
++failures;
1089+
}
1090+
return failures > 125 ? 125 : failures;
1091+
}
1092+
10381093
if (vm.count("post")) {
10391094
BOOST_LOG_SEV(sw::Logger::IOLog::get(), sw::Logger::info) << "Using HTTP POST.\n";
10401095
sw::ServiceGraphPattern::defaultHTTPmethod = sw::ServiceGraphPattern::HTTP_METHOD_POST;

‎lib/MiniJSON.hpp‎

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/* MiniJSON - a small self-contained JSON reader.
2+
*
3+
* Parses UTF-8 JSON text into a Value DOM (object/array/string/number/
4+
* boolean/null). Used by the ShEx shape-map and manifest readers; not a
5+
* general-purpose serializer.
6+
*/
7+
8+
#ifndef SWOBJ_MINIJSON_H
9+
#define SWOBJ_MINIJSON_H
10+
11+
#include <map>
12+
#include <vector>
13+
#include <string>
14+
#include <sstream>
15+
#include <stdexcept>
16+
17+
namespace w3c_sw {
18+
namespace MiniJSON {
19+
20+
struct ParseError : public std::runtime_error {
21+
ParseError (std::string what) : std::runtime_error("JSON: " + what) { }
22+
};
23+
24+
class Value {
25+
public:
26+
enum Type { Null_T, Bool_T, Number_T, String_T, Array_T, Object_T };
27+
Type type;
28+
bool boolean;
29+
double number;
30+
std::string string; // also holds the raw number text
31+
std::vector<Value> array;
32+
std::vector<std::pair<std::string, Value> > object; // insertion-ordered
33+
34+
Value () : type(Null_T), boolean(false), number(0) { }
35+
36+
bool isNull () const { return type == Null_T; }
37+
bool isString () const { return type == String_T; }
38+
bool isArray () const { return type == Array_T; }
39+
bool isObject () const { return type == Object_T; }
40+
41+
const Value* get (const std::string& key) const {
42+
for (std::vector<std::pair<std::string, Value> >::const_iterator it = object.begin();
43+
it != object.end(); ++it)
44+
if (it->first == key)
45+
return &it->second;
46+
return NULL;
47+
}
48+
std::string getString (const std::string& key, std::string dflt = "") const {
49+
const Value* v = get(key);
50+
return v != NULL && v->type == String_T ? v->string : dflt;
51+
}
52+
};
53+
54+
class Parser {
55+
const std::string& text;
56+
size_t pos;
57+
58+
public:
59+
Parser (const std::string& text) : text(text), pos(0) { }
60+
61+
Value parse () {
62+
Value ret = parseValue();
63+
skipWS();
64+
if (pos != text.size())
65+
throw ParseError("trailing content at offset " + offsetStr());
66+
return ret;
67+
}
68+
69+
private:
70+
std::string offsetStr () const {
71+
std::stringstream ss;
72+
ss << pos;
73+
return ss.str();
74+
}
75+
void skipWS () {
76+
while (pos < text.size() &&
77+
(text[pos] == ' ' || text[pos] == '\t' || text[pos] == '\r' || text[pos] == '\n'))
78+
++pos;
79+
}
80+
char peek () {
81+
skipWS();
82+
if (pos >= text.size())
83+
throw ParseError("unexpected end of input");
84+
return text[pos];
85+
}
86+
void expect (char c) {
87+
if (peek() != c)
88+
throw ParseError(std::string("expected '") + c + "' at offset " + offsetStr());
89+
++pos;
90+
}
91+
bool tryConsume (char c) {
92+
if (pos < text.size() && peek() == c) {
93+
++pos;
94+
return true;
95+
}
96+
return false;
97+
}
98+
99+
Value parseValue () {
100+
switch (peek()) {
101+
case '{': return parseObject();
102+
case '[': return parseArray();
103+
case '"': {
104+
Value v;
105+
v.type = Value::String_T;
106+
v.string = parseString();
107+
return v;
108+
}
109+
case 't': case 'f': return parseKeyword();
110+
case 'n': return parseKeyword();
111+
default: return parseNumber();
112+
}
113+
}
114+
115+
Value parseObject () {
116+
Value v;
117+
v.type = Value::Object_T;
118+
expect('{');
119+
if (tryConsume('}'))
120+
return v;
121+
do {
122+
if (peek() != '"')
123+
throw ParseError("expected object key at offset " + offsetStr());
124+
std::string key = parseString();
125+
expect(':');
126+
v.object.push_back(std::make_pair(key, parseValue()));
127+
} while (tryConsume(','));
128+
expect('}');
129+
return v;
130+
}
131+
132+
Value parseArray () {
133+
Value v;
134+
v.type = Value::Array_T;
135+
expect('[');
136+
if (tryConsume(']'))
137+
return v;
138+
do {
139+
v.array.push_back(parseValue());
140+
} while (tryConsume(','));
141+
expect(']');
142+
return v;
143+
}
144+
145+
std::string parseString () {
146+
expect('"');
147+
std::string ret;
148+
while (true) {
149+
if (pos >= text.size())
150+
throw ParseError("unterminated string");
151+
char c = text[pos++];
152+
if (c == '"')
153+
return ret;
154+
if (c != '\\') {
155+
ret += c;
156+
continue;
157+
}
158+
if (pos >= text.size())
159+
throw ParseError("unterminated escape");
160+
char e = text[pos++];
161+
switch (e) {
162+
case '"': ret += '"'; break;
163+
case '\\': ret += '\\'; break;
164+
case '/': ret += '/'; break;
165+
case 'b': ret += '\b'; break;
166+
case 'f': ret += '\f'; break;
167+
case 'n': ret += '\n'; break;
168+
case 'r': ret += '\r'; break;
169+
case 't': ret += '\t'; break;
170+
case 'u': {
171+
if (pos + 4 > text.size())
172+
throw ParseError("truncated \\u escape");
173+
unsigned long cp = strtoul(text.substr(pos, 4).c_str(), NULL, 16);
174+
pos += 4;
175+
// surrogate pairs
176+
if (cp >= 0xD800 && cp <= 0xDBFF && pos + 6 <= text.size()
177+
&& text[pos] == '\\' && text[pos+1] == 'u') {
178+
unsigned long lo = strtoul(text.substr(pos+2, 4).c_str(), NULL, 16);
179+
if (lo >= 0xDC00 && lo <= 0xDFFF) {
180+
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
181+
pos += 6;
182+
}
183+
}
184+
// utf-8 encode
185+
if (cp < 0x80)
186+
ret += (char)cp;
187+
else if (cp < 0x800) {
188+
ret += (char)(0xC0 | (cp >> 6));
189+
ret += (char)(0x80 | (cp & 0x3F));
190+
} else if (cp < 0x10000) {
191+
ret += (char)(0xE0 | (cp >> 12));
192+
ret += (char)(0x80 | ((cp >> 6) & 0x3F));
193+
ret += (char)(0x80 | (cp & 0x3F));
194+
} else {
195+
ret += (char)(0xF0 | (cp >> 18));
196+
ret += (char)(0x80 | ((cp >> 12) & 0x3F));
197+
ret += (char)(0x80 | ((cp >> 6) & 0x3F));
198+
ret += (char)(0x80 | (cp & 0x3F));
199+
}
200+
break;
201+
}
202+
default:
203+
throw ParseError(std::string("invalid escape \\") + e);
204+
}
205+
}
206+
}
207+
208+
Value parseKeyword () {
209+
Value v;
210+
if (text.compare(pos, 4, "true") == 0) {
211+
v.type = Value::Bool_T;
212+
v.boolean = true;
213+
pos += 4;
214+
} else if (text.compare(pos, 5, "false") == 0) {
215+
v.type = Value::Bool_T;
216+
v.boolean = false;
217+
pos += 5;
218+
} else if (text.compare(pos, 4, "null") == 0) {
219+
v.type = Value::Null_T;
220+
pos += 4;
221+
} else
222+
throw ParseError("unexpected token at offset " + offsetStr());
223+
return v;
224+
}
225+
226+
Value parseNumber () {
227+
size_t start = pos;
228+
if (pos < text.size() && text[pos] == '-')
229+
++pos;
230+
while (pos < text.size() &&
231+
((text[pos] >= '0' && text[pos] <= '9') || text[pos] == '.'
232+
|| text[pos] == 'e' || text[pos] == 'E' || text[pos] == '+' || text[pos] == '-'))
233+
++pos;
234+
if (pos == start)
235+
throw ParseError("unexpected character at offset " + offsetStr());
236+
Value v;
237+
v.type = Value::Number_T;
238+
v.string = text.substr(start, pos - start);
239+
v.number = atof(v.string.c_str());
240+
return v;
241+
}
242+
};
243+
244+
inline Value parse (const std::string& text) {
245+
Parser p(text);
246+
return p.parse();
247+
}
248+
249+
} // namespace MiniJSON
250+
} // namespace w3c_sw
251+
252+
#endif // !SWOBJ_MINIJSON_H

‎lib/ParserCommon.hpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ class ParserDriver {
135135
namespaces = newMap;
136136
freeNamespaces = pFreeNamespaces;
137137
}
138+
139+
NamespaceMap* getNamespaceMap () { return namespaces; }
138140
};
139141

140142
struct CharacterRange {

0 commit comments

Comments
 (0)