From ef8769fe5a05b4e93215f45cbd87e4f7a7ba456f Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Wed, 26 Sep 2012 16:36:51 -0400 Subject: [PATCH] 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. --- jsonld-tests.php | 138 +++++++++++++++++++++--------------- jsonld.php | 180 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 212 insertions(+), 106 deletions(-) diff --git a/jsonld-tests.php b/jsonld-tests.php index 198e330..939e09e 100644 --- a/jsonld-tests.php +++ b/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(); diff --git a/jsonld.php b/jsonld.php index 9524ada..3059b8f 100644 --- a/jsonld.php +++ b/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; } }