forked from friendica/php-json-ld
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:
parent
9b557af763
commit
ef8769fe5a
138
jsonld-tests.php
138
jsonld-tests.php
|
@ -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 {
|
||||
public function __construct() {
|
||||
// set up groups, add root group
|
||||
|
@ -159,16 +183,8 @@ class TestRunner {
|
|||
else {
|
||||
$this->failed += 1;
|
||||
echo "FAIL$eol";
|
||||
echo 'Expect: ' . print_r($expect, true) . $eol;
|
||||
echo 'Result: ' . print_r($result, true) . $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;
|
||||
*/
|
||||
echo 'Expect: ' . jsonld_encode($expect) . $eol;
|
||||
echo 'Result: ' . jsonld_encode($result) . $eol;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,54 +259,62 @@ class TestRunner {
|
|||
$type = $test->{'@type'};
|
||||
$options = array(
|
||||
'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
|
||||
$this->check($test, $test->expect, $result);
|
||||
try {
|
||||
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')) {
|
||||
$this->ungroup();
|
||||
|
|
180
jsonld.php
180
jsonld.php
|
@ -115,7 +115,10 @@ function jsonld_normalize($input, $options=array()) {
|
|||
* @param assoc [$options] the options to use:
|
||||
* [format] the format if input not an array:
|
||||
* '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.
|
||||
*/
|
||||
|
@ -221,6 +224,7 @@ class JsonLdProcessor {
|
|||
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
|
||||
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
|
||||
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
||||
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
||||
|
||||
/** RDF constants */
|
||||
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:
|
||||
* [format] the format if input is a string:
|
||||
* '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.
|
||||
*/
|
||||
|
@ -513,7 +520,8 @@ class JsonLdProcessor {
|
|||
|
||||
// set default options
|
||||
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)) {
|
||||
// supported formats (processor-specific and global)
|
||||
|
@ -1036,7 +1044,8 @@ class JsonLdProcessor {
|
|||
array('\\\\', '\t', '\n', '\r', '\"'),
|
||||
$o->nominalValue);
|
||||
$quad .= '"' . $escaped . '"';
|
||||
if(property_exists($o, 'datatype')) {
|
||||
if(property_exists($o, 'datatype') &&
|
||||
$o->datatype->nominalValue !== JsonLdProcessor::XSD_STRING) {
|
||||
$quad .= "^^<{$o->datatype->nominalValue}>";
|
||||
}
|
||||
else if(property_exists($o, 'language')) {
|
||||
|
@ -1717,7 +1726,7 @@ class JsonLdProcessor {
|
|||
$entry = $list_map->{$s};
|
||||
}
|
||||
// set object value
|
||||
$entry->first = $this->_rdfToObject($o);
|
||||
$entry->first = $this->_rdfToObject($o, $options['useNativeTypes']);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1754,14 +1763,14 @@ class JsonLdProcessor {
|
|||
}
|
||||
|
||||
// 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
|
||||
self::addValue(
|
||||
$value, '@type', $o->nominalValue, array('propertyIsArray' => true));
|
||||
}
|
||||
else {
|
||||
// add property to value as needed
|
||||
$object = $this->_rdfToObject($o);
|
||||
$object = $this->_rdfToObject($o, $options['useNativeTypes']);
|
||||
self::addValue($value, $p, $object, array('propertyIsArray' => true));
|
||||
|
||||
// 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;
|
||||
}
|
||||
else if(is_double($value)) {
|
||||
// do special JSON-LD double format, printf('%1.15e') equivalent
|
||||
$value = preg_replace('/(e(?:\+|-))([0-9])$/', '${1}0${2}',
|
||||
sprintf('%1.15e', $value));
|
||||
// canonical double representation
|
||||
$value = preg_replace('/(\d)0*E\+?/', '$1E',
|
||||
sprintf('%1.15E', $value));
|
||||
$datatype or $datatype = self::XSD_DOUBLE;
|
||||
}
|
||||
else {
|
||||
$value = strval($value);
|
||||
$datatype or $datatype = self::XSD_INTEGER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to xsd:string datatype
|
||||
$datatype or $datatype = self::XSD_STRING;
|
||||
|
||||
$object = (object)array(
|
||||
'nominalValue' => $value,
|
||||
'interfaceName' => 'LiteralNode');
|
||||
|
||||
if($datatype !== null) {
|
||||
$object->datatype = (object)array(
|
||||
'interfaceName' => 'LiteralNode',
|
||||
'datatype' => (object)array(
|
||||
'nominalValue' => $datatype,
|
||||
'interfaceName' => 'IRI');
|
||||
}
|
||||
else if(property_exists($element, '@language')) {
|
||||
'interfaceName' => 'IRI'));
|
||||
if(property_exists($element, '@language') &&
|
||||
$datatype === self::XSD_STRING) {
|
||||
$object->language = $element->{'@language'};
|
||||
}
|
||||
|
||||
|
@ -2102,10 +2112,11 @@ class JsonLdProcessor {
|
|||
* Converts an RDF statement object to a JSON-LD object.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
protected function _rdfToObject($o) {
|
||||
protected function _rdfToObject($o, $use_native_types) {
|
||||
// convert empty list
|
||||
if($o->interfaceName === 'IRI' && $o->nominalValue === self::RDF_NIL) {
|
||||
return (object)array('@list' => array());
|
||||
|
@ -2121,22 +2132,39 @@ class JsonLdProcessor {
|
|||
|
||||
// add datatype
|
||||
if(property_exists($o, 'datatype')) {
|
||||
/*
|
||||
// use native datatypes for certain xsd types
|
||||
$type = $o->datatype->nominalValue;
|
||||
if($type === self::XSD_BOOLEAN) {
|
||||
$element = !($element === 'false' || $element === '0');
|
||||
}
|
||||
else if($type === self::XSD_INTEGER) {
|
||||
$element = intval($element);
|
||||
}
|
||||
else if($type === self::XSD_DOUBLE) {
|
||||
$element = doubleval($element);
|
||||
}*/
|
||||
$rval->{'@type'} = $o->datatype->nominalValue;
|
||||
$type = $o->datatype->nominalValue;
|
||||
// use native types for certain xsd types
|
||||
if($use_native_types) {
|
||||
if($type === self::XSD_BOOLEAN) {
|
||||
if($rval->{'@value'} === 'true') {
|
||||
$rval->{'@value'} = true;
|
||||
}
|
||||
else if($rval->{'@value'} === 'false') {
|
||||
$rval->{'@value'} = false;
|
||||
}
|
||||
}
|
||||
else if(is_numeric($rval->{'@value'})) {
|
||||
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
|
||||
else if(property_exists($o, 'language')) {
|
||||
if(property_exists($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
|
||||
if(count($terms) === 0) {
|
||||
foreach($ctx->mappings as $term => $entry) {
|
||||
|
@ -3143,6 +3185,30 @@ class JsonLdProcessor {
|
|||
$value = $ctx->{$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
|
||||
if($key !== '@language') {
|
||||
throw new JsonLdException(
|
||||
|
@ -3379,8 +3445,14 @@ class JsonLdProcessor {
|
|||
return $value;
|
||||
}
|
||||
|
||||
// prepend vocab
|
||||
if(property_exists($ctx, '@vocab') && $ctx->{'@vocab'} !== null) {
|
||||
$value = $this->_prependBase($ctx->{'@vocab'}, $value);
|
||||
}
|
||||
// prepend base
|
||||
$value = $this->_prependBase($base, $value);
|
||||
else {
|
||||
$value = $this->_prependBase($base, $value);
|
||||
}
|
||||
|
||||
// value must now be an absolute IRI
|
||||
if(!self::_isAbsoluteIri($value)) {
|
||||
|
@ -3444,8 +3516,16 @@ class JsonLdProcessor {
|
|||
return $term;
|
||||
}
|
||||
|
||||
// use vocab
|
||||
if(property_exists($ctx, '@vocab') && $ctx->{'@vocab'} !== null) {
|
||||
$term = $this->_prependBase($ctx->{'@vocab'}, $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(
|
||||
'mappings' => new stdClass(),
|
||||
'keywords' => (object)array(
|
||||
'@context'=> array(),
|
||||
'@container'=> array(),
|
||||
'@default'=> array(),
|
||||
'@embed'=> array(),
|
||||
'@explicit'=> array(),
|
||||
'@graph'=> array(),
|
||||
'@id'=> array(),
|
||||
'@language'=> array(),
|
||||
'@list'=> array(),
|
||||
'@omitDefault'=> array(),
|
||||
'@preserve'=> array(),
|
||||
'@set'=> array(),
|
||||
'@type'=> array(),
|
||||
'@value'=> array()
|
||||
'@context' => array(),
|
||||
'@container' => array(),
|
||||
'@default' => array(),
|
||||
'@embed' => array(),
|
||||
'@explicit' => array(),
|
||||
'@graph' => array(),
|
||||
'@id' => array(),
|
||||
'@language' => array(),
|
||||
'@list' => array(),
|
||||
'@omitDefault' => array(),
|
||||
'@preserve' => array(),
|
||||
'@set' => array(),
|
||||
'@type' => array(),
|
||||
'@value' => array(),
|
||||
'@vocab' => array()
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -3765,6 +3846,7 @@ class JsonLdProcessor {
|
|||
case '@set':
|
||||
case '@type':
|
||||
case '@value':
|
||||
case '@vocab':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue