Fixes to comply with updates to the spec(s).

- Add @vocab support.
- Convert native types in fromRDF().
- Use new canonical form for doubles in toRDF().
- Support xsd:string special cases.
- Keep going if a JSON-LD exception occurs in test runner.
This commit is contained in:
Dave Longley 2012-09-26 16:36:51 -04:00
parent 9b557af763
commit ef8769fe5a
2 changed files with 212 additions and 106 deletions

View file

@ -104,6 +104,30 @@ function read_test_nquads($file, $filepath) {
} }
} }
/**
* JSON-encodes the given input (does not escape slashes).
*
* @param mixed $input the input to encode.
*
* @return the encoded input.
*/
function jsonld_encode($input) {
// newer PHP has a flag to avoid escaped '/'
if(defined('JSON_UNESCAPED_SLASHES')) {
$options = JSON_UNESCAPED_SLASHES;
if(defined('JSON_PRETTY_PRINT')) {
$options |= JSON_PRETTY_PRINT;
}
$json = json_encode($input, $options);
}
else {
// use a simple string replacement of '\/' to '/'.
$json = str_replace('\\/', '/', json_encode($input));
}
return $json;
}
class TestRunner { class TestRunner {
public function __construct() { public function __construct() {
// set up groups, add root group // set up groups, add root group
@ -159,16 +183,8 @@ class TestRunner {
else { else {
$this->failed += 1; $this->failed += 1;
echo "FAIL$eol"; echo "FAIL$eol";
echo 'Expect: ' . print_r($expect, true) . $eol; echo 'Expect: ' . jsonld_encode($expect) . $eol;
echo 'Result: ' . print_r($result, true) . $eol; echo 'Result: ' . jsonld_encode($result) . $eol;
/*
$flags = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
echo 'JSON Expect: ' .
json_encode(json_decode(expect, $flags)) . $eol;
echo 'JSON Result: ' .
json_encode(json_decode(result, $flags)) . $eol;
*/
} }
} }
@ -243,54 +259,62 @@ class TestRunner {
$type = $test->{'@type'}; $type = $test->{'@type'};
$options = array( $options = array(
'base' => 'http://json-ld.org/test-suite/tests/' . $test->input); 'base' => 'http://json-ld.org/test-suite/tests/' . $test->input);
if(in_array('jld:NormalizeTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_nquads($test->expect, $filepath);
$options['format'] = 'application/nquads';
$result = jsonld_normalize($input, $options);
}
else if(in_array('jld:ExpandTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_expand($input, $options);
}
else if(in_array('jld:CompactTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->context = read_test_json($test->context, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_compact($input, $test->context, $options);
}
else if(in_array('jld:FrameTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->frame = read_test_json($test->frame, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_frame($input, $test->frame, $options);
}
else if(in_array('jld:FromRDFTest', $type)) {
$this->test($test->name);
$input = read_test_nquads($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_from_rdf($input, $options);
}
else if(in_array('jld:ToRDFTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_nquads($test->expect, $filepath);
$options['format'] = 'application/nquads';
$result = jsonld_to_rdf($input, $options);
}
else {
echo "Skipping test \"{$test->name}\" of type: " .
json_encode($type) . $eol;
continue;
}
// check results try {
$this->check($test, $test->expect, $result); if(in_array('jld:NormalizeTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_nquads($test->expect, $filepath);
$options['format'] = 'application/nquads';
$result = jsonld_normalize($input, $options);
}
else if(in_array('jld:ExpandTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_expand($input, $options);
}
else if(in_array('jld:CompactTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->context = read_test_json($test->context, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_compact($input, $test->context, $options);
}
else if(in_array('jld:FrameTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->frame = read_test_json($test->frame, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_frame($input, $test->frame, $options);
}
else if(in_array('jld:FromRDFTest', $type)) {
$this->test($test->name);
$input = read_test_nquads($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_from_rdf($input, $options);
}
else if(in_array('jld:ToRDFTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_nquads($test->expect, $filepath);
$options['format'] = 'application/nquads';
$result = jsonld_to_rdf($input, $options);
}
else {
echo "Skipping test \"{$test->name}\" of type: " .
json_encode($type) . $eol;
continue;
}
// check results
$this->check($test, $test->expect, $result);
}
catch(JsonLdException $e) {
echo $eol . $e;
$this->failed += 1;
echo "FAIL$eol";
}
} }
if(property_exists($manifest, 'name')) { if(property_exists($manifest, 'name')) {
$this->ungroup(); $this->ungroup();

View file

@ -115,7 +115,10 @@ function jsonld_normalize($input, $options=array()) {
* @param assoc [$options] the options to use: * @param assoc [$options] the options to use:
* [format] the format if input not an array: * [format] the format if input not an array:
* 'application/nquads' for N-Quads (default). * 'application/nquads' for N-Quads (default).
* [notType] true to use rdf:type, false to use @type (default). * [useRdfType] true to use rdf:type, false to use @type
* (default: false).
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: true).
* *
* @return array the JSON-LD output. * @return array the JSON-LD output.
*/ */
@ -221,6 +224,7 @@ class JsonLdProcessor {
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean'; const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double'; const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
/** RDF constants */ /** RDF constants */
const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first'; const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
@ -504,7 +508,10 @@ class JsonLdProcessor {
* @param assoc $options the options to use: * @param assoc $options the options to use:
* [format] the format if input is a string: * [format] the format if input is a string:
* 'application/nquads' for N-Quads (default). * 'application/nquads' for N-Quads (default).
* [notType] true to use rdf:type, false to use @type (default). * [useRdfType] true to use rdf:type, false to use @type
* (default: false).
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: true).
* *
* @return array the JSON-LD output. * @return array the JSON-LD output.
*/ */
@ -513,7 +520,8 @@ class JsonLdProcessor {
// set default options // set default options
isset($options['format']) or $options['format'] = 'application/nquads'; isset($options['format']) or $options['format'] = 'application/nquads';
isset($options['notType']) or $options['notType'] = false; isset($options['useRdfType']) or $options['useRdfType'] = false;
isset($options['useNativeTypes']) or $options['useNativeTypes'] = true;
if(!is_array($statements)) { if(!is_array($statements)) {
// supported formats (processor-specific and global) // supported formats (processor-specific and global)
@ -1036,7 +1044,8 @@ class JsonLdProcessor {
array('\\\\', '\t', '\n', '\r', '\"'), array('\\\\', '\t', '\n', '\r', '\"'),
$o->nominalValue); $o->nominalValue);
$quad .= '"' . $escaped . '"'; $quad .= '"' . $escaped . '"';
if(property_exists($o, 'datatype')) { if(property_exists($o, 'datatype') &&
$o->datatype->nominalValue !== JsonLdProcessor::XSD_STRING) {
$quad .= "^^<{$o->datatype->nominalValue}>"; $quad .= "^^<{$o->datatype->nominalValue}>";
} }
else if(property_exists($o, 'language')) { else if(property_exists($o, 'language')) {
@ -1717,7 +1726,7 @@ class JsonLdProcessor {
$entry = $list_map->{$s}; $entry = $list_map->{$s};
} }
// set object value // set object value
$entry->first = $this->_rdfToObject($o); $entry->first = $this->_rdfToObject($o, $options['useNativeTypes']);
continue; continue;
} }
@ -1754,14 +1763,14 @@ class JsonLdProcessor {
} }
// convert to @type unless options indicate to treat rdf:type as property // convert to @type unless options indicate to treat rdf:type as property
if($p === self::RDF_TYPE && !$options['notType']) { if($p === self::RDF_TYPE && !$options['useRdfType']) {
// add value of object as @type // add value of object as @type
self::addValue( self::addValue(
$value, '@type', $o->nominalValue, array('propertyIsArray' => true)); $value, '@type', $o->nominalValue, array('propertyIsArray' => true));
} }
else { else {
// add property to value as needed // add property to value as needed
$object = $this->_rdfToObject($o); $object = $this->_rdfToObject($o, $options['useNativeTypes']);
self::addValue($value, $p, $object, array('propertyIsArray' => true)); self::addValue($value, $p, $object, array('propertyIsArray' => true));
// a bnode might be the beginning of a list, so add it to the list map // a bnode might be the beginning of a list, so add it to the list map
@ -1858,27 +1867,28 @@ class JsonLdProcessor {
$datatype or $datatype = self::XSD_BOOLEAN; $datatype or $datatype = self::XSD_BOOLEAN;
} }
else if(is_double($value)) { else if(is_double($value)) {
// do special JSON-LD double format, printf('%1.15e') equivalent // canonical double representation
$value = preg_replace('/(e(?:\+|-))([0-9])$/', '${1}0${2}', $value = preg_replace('/(\d)0*E\+?/', '$1E',
sprintf('%1.15e', $value)); sprintf('%1.15E', $value));
$datatype or $datatype = self::XSD_DOUBLE; $datatype or $datatype = self::XSD_DOUBLE;
} }
else { else {
$value = strval($value); $value = strval($value);
$datatype or $datatype = self::XSD_INTEGER; $datatype or $datatype = self::XSD_INTEGER;
} }
} }
// default to xsd:string datatype
$datatype or $datatype = self::XSD_STRING;
$object = (object)array( $object = (object)array(
'nominalValue' => $value, 'nominalValue' => $value,
'interfaceName' => 'LiteralNode'); 'interfaceName' => 'LiteralNode',
'datatype' => (object)array(
if($datatype !== null) {
$object->datatype = (object)array(
'nominalValue' => $datatype, 'nominalValue' => $datatype,
'interfaceName' => 'IRI'); 'interfaceName' => 'IRI'));
} if(property_exists($element, '@language') &&
else if(property_exists($element, '@language')) { $datatype === self::XSD_STRING) {
$object->language = $element->{'@language'}; $object->language = $element->{'@language'};
} }
@ -2102,10 +2112,11 @@ class JsonLdProcessor {
* Converts an RDF statement object to a JSON-LD object. * Converts an RDF statement object to a JSON-LD object.
* *
* @param stdClass $o the RDF statement object to convert. * @param stdClass $o the RDF statement object to convert.
* @param bool $use_native_types true to output native types, false not to.
* *
* @return stdClass the JSON-LD object. * @return stdClass the JSON-LD object.
*/ */
protected function _rdfToObject($o) { protected function _rdfToObject($o, $use_native_types) {
// convert empty list // convert empty list
if($o->interfaceName === 'IRI' && $o->nominalValue === self::RDF_NIL) { if($o->interfaceName === 'IRI' && $o->nominalValue === self::RDF_NIL) {
return (object)array('@list' => array()); return (object)array('@list' => array());
@ -2121,22 +2132,39 @@ class JsonLdProcessor {
// add datatype // add datatype
if(property_exists($o, 'datatype')) { if(property_exists($o, 'datatype')) {
/* $type = $o->datatype->nominalValue;
// use native datatypes for certain xsd types // use native types for certain xsd types
$type = $o->datatype->nominalValue; if($use_native_types) {
if($type === self::XSD_BOOLEAN) { if($type === self::XSD_BOOLEAN) {
$element = !($element === 'false' || $element === '0'); if($rval->{'@value'} === 'true') {
} $rval->{'@value'} = true;
else if($type === self::XSD_INTEGER) { }
$element = intval($element); else if($rval->{'@value'} === 'false') {
} $rval->{'@value'} = false;
else if($type === self::XSD_DOUBLE) { }
$element = doubleval($element); }
}*/ else if(is_numeric($rval->{'@value'})) {
$rval->{'@type'} = $o->datatype->nominalValue; if($type === self::XSD_INTEGER) {
$i = intval($rval->{'@value'});
if(strval($i) === $rval->{'@value'}) {
$rval->{'@value'} = $i;
}
}
else if($type === self::XSD_DOUBLE) {
$rval->{'@value'} = doubleval($rval->{'@value'});
}
}
// do not add xsd:string type
if($type !== self::XSD_STRING) {
$rval->{'@type'} = $type;
}
}
else {
$rval->{'@type'} = $type;
}
} }
// add language // add language
else if(property_exists($o, 'language')) { if(property_exists($o, 'language')) {
$rval->{'@language'} = $o->language; $rval->{'@language'} = $o->language;
} }
@ -3069,6 +3097,20 @@ class JsonLdProcessor {
} }
} }
// no matching terms, use @vocab if available
if(count($terms) === 0 && property_exists($ctx, '@vocab') &&
$ctx->{'@vocab'} !== null) {
// determine if vocab is a prefix of the iri
$vocab = $ctx->{'@vocab'};
if(strpos($iri, $vocab) === 0) {
// use suffix as relative iri if it is not a term in the active context
$suffix = substr($iri, strlen($vocab));
if(!property_exists($ctx->mappings, $suffix)) {
return $suffix;
}
}
}
// no term matches, add possible CURIEs // no term matches, add possible CURIEs
if(count($terms) === 0) { if(count($terms) === 0) {
foreach($ctx->mappings as $term => $entry) { foreach($ctx->mappings as $term => $entry) {
@ -3143,6 +3185,30 @@ class JsonLdProcessor {
$value = $ctx->{$key}; $value = $ctx->{$key};
if(self::_isKeyword($key)) { if(self::_isKeyword($key)) {
// support vocab
if($key === '@vocab') {
if($value !== null && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@vocab" in a ' +
'@context must be a string or null.',
'jsonld.SyntaxError', array('context' => $ctx));
}
if(!self::_isAbsoluteIri($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@vocab" in a ' +
'@context must be an absolute IRI.',
'jsonld.SyntaxError', array('context' => $ctx));
}
if($value === null) {
unset($active_ctx->{'@vocab'});
}
else {
$active_ctx->{'@vocab'} = $value;
}
$defined->{$key} = true;
return;
}
// only @language is permitted // only @language is permitted
if($key !== '@language') { if($key !== '@language') {
throw new JsonLdException( throw new JsonLdException(
@ -3379,8 +3445,14 @@ class JsonLdProcessor {
return $value; return $value;
} }
// prepend vocab
if(property_exists($ctx, '@vocab') && $ctx->{'@vocab'} !== null) {
$value = $this->_prependBase($ctx->{'@vocab'}, $value);
}
// prepend base // prepend base
$value = $this->_prependBase($base, $value); else {
$value = $this->_prependBase($base, $value);
}
// value must now be an absolute IRI // value must now be an absolute IRI
if(!self::_isAbsoluteIri($value)) { if(!self::_isAbsoluteIri($value)) {
@ -3444,8 +3516,16 @@ class JsonLdProcessor {
return $term; return $term;
} }
// use vocab
if(property_exists($ctx, '@vocab') && $ctx->{'@vocab'} !== null) {
$term = $this->_prependBase($ctx->{'@vocab'}, $term);
}
// prepend base to term // prepend base to term
return $this->_prependBase($base, $term); else {
$term = $this->_prependBase($base, $term);
}
return $term;
} }
/** /**
@ -3645,20 +3725,21 @@ class JsonLdProcessor {
return (object)array( return (object)array(
'mappings' => new stdClass(), 'mappings' => new stdClass(),
'keywords' => (object)array( 'keywords' => (object)array(
'@context'=> array(), '@context' => array(),
'@container'=> array(), '@container' => array(),
'@default'=> array(), '@default' => array(),
'@embed'=> array(), '@embed' => array(),
'@explicit'=> array(), '@explicit' => array(),
'@graph'=> array(), '@graph' => array(),
'@id'=> array(), '@id' => array(),
'@language'=> array(), '@language' => array(),
'@list'=> array(), '@list' => array(),
'@omitDefault'=> array(), '@omitDefault' => array(),
'@preserve'=> array(), '@preserve' => array(),
'@set'=> array(), '@set' => array(),
'@type'=> array(), '@type' => array(),
'@value'=> array() '@value' => array(),
'@vocab' => array()
)); ));
} }
@ -3765,6 +3846,7 @@ class JsonLdProcessor {
case '@set': case '@set':
case '@type': case '@type':
case '@value': case '@value':
case '@vocab':
return true; return true;
} }
} }