From 6ceacedfb3ef16c51f79b4ff7a3e38d7dfa67ecb Mon Sep 17 00:00:00 2001 From: mwjames Date: Wed, 5 Oct 2016 21:03:36 +0200 Subject: [PATCH] ParserParameterProcessor to support JSON typed annotation This allows to add JSON typed annotation in the #set and #subobject parser function using the `@json` marker. ``` {{#set: |@json={ "foo": [ "bar", "foobar" ] } }} ``` --- i18n/en.json | 3 +- .../LanguageFileContentsReader.php | 19 +-- src/Libs/ErrorCode.php | 77 +++++++++++++ src/ParserParameterProcessor.php | 108 ++++++++++++++---- .../ByJsonScript/Contents/p-0211.1.json | 20 ++++ .../ByJsonScript/Contents/p-0211.2.json | 27 +++++ .../ByJsonScript/Contents/p-0211.3.json | 11 ++ .../ByJsonScript/Fixtures/p-0211.json | 74 ++++++++++++ .../LanguageFileContentsReaderTest.php | 8 ++ tests/phpunit/Unit/Libs/ErrorCodeTest.php | 48 ++++++++ .../Unit/ParserParameterProcessorTest.php | 37 ++++++ 11 files changed, 391 insertions(+), 41 deletions(-) create mode 100644 src/Libs/ErrorCode.php create mode 100644 tests/phpunit/Integration/ByJsonScript/Contents/p-0211.1.json create mode 100644 tests/phpunit/Integration/ByJsonScript/Contents/p-0211.2.json create mode 100644 tests/phpunit/Integration/ByJsonScript/Contents/p-0211.3.json create mode 100644 tests/phpunit/Integration/ByJsonScript/Fixtures/p-0211.json create mode 100644 tests/phpunit/Unit/Libs/ErrorCodeTest.php diff --git a/i18n/en.json b/i18n/en.json index c23f375f4b..77904a8075 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -444,5 +444,6 @@ "smw-datavalue-wikipage-empty": "The wikipage input value is empty (e.g. [[SomeProperty::]], [[]]) and therefore it cannot be used as a name or as part of a query condition.", "smw-sp-types_ref_rec": "\"$1\" is a [https://www.semantic-mediawiki.org/wiki/Container container] type that allows to record additional information (e.g. provenance data) about a value assignment.", "smw-datavalue-reference-outputformat": "$1: $2", - "smw-datavalue-reference-invalid-fields-definition": "The [[Special:Types/Reference|Reference]] type expects a list of properties to be declared using the [https://www.semantic-mediawiki.org/wiki/Help:Special_property_Has_fields Has fields] property." + "smw-datavalue-reference-invalid-fields-definition": "The [[Special:Types/Reference|Reference]] type expects a list of properties to be declared using the [https://www.semantic-mediawiki.org/wiki/Help:Special_property_Has_fields Has fields] property.", + "smw-parser-invalid-json-format": "The JSON parser returned with a \"$1\"." } diff --git a/src/ExtraneousLanguage/LanguageFileContentsReader.php b/src/ExtraneousLanguage/LanguageFileContentsReader.php index e8af0fcb9c..57438034ff 100644 --- a/src/ExtraneousLanguage/LanguageFileContentsReader.php +++ b/src/ExtraneousLanguage/LanguageFileContentsReader.php @@ -5,6 +5,7 @@ use RuntimeException; use Onoi\Cache\Cache; use Onoi\Cache\NullCache; +use SMW\Libs\ErrorCode; /** * @license GNU GPL v2+ @@ -174,7 +175,7 @@ protected function doReadJsonContentsFromFileBy( $languageCode, $cacheKey ) { return $contents; } - throw new RuntimeException( $this->getMessageForJsonErrorCode( json_last_error() ) ); + throw new RuntimeException( ErrorCode::getMessageFromJsonErrorCode( json_last_error() ) ); } private function getFileForLanguageCode( $languageCode ) { @@ -188,22 +189,6 @@ private function getFileForLanguageCode( $languageCode ) { throw new RuntimeException( "Expected a {$file} file" ); } - private function getMessageForJsonErrorCode( $errorCode ) { - - $errorMessages = array( - JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch, malformed JSON', - JSON_ERROR_CTRL_CHAR => 'Unexpected control character found, possibly incorrectly encoded', - JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', - JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', - JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded' - ); - - return sprintf( - "Expected a JSON compatible format but failed with '%s'", - $errorMessages[$errorCode] - ); - } - private function getCacheKeyFrom( $languageCode ) { return $this->cachePrefix . ':' . $languageCode . ':' . md5( $this->ttl . $this->getModificationTimeByLanguageCode( $languageCode ) ); } diff --git a/src/Libs/ErrorCode.php b/src/Libs/ErrorCode.php new file mode 100644 index 0000000000..b513165602 --- /dev/null +++ b/src/Libs/ErrorCode.php @@ -0,0 +1,77 @@ + $value ) { + if ( !strncmp( $name, "JSON_ERROR_", 11 ) ) { + self::$jsonErrors[$value] = $name; + } + } + } + + return isset( self::$jsonErrors[$errorCode] ) ? self::$jsonErrors[$errorCode] : 'UNKNOWN'; + } + + /** + * @since 2.5 + * + * @param integer $errorCode + * + * @return string + */ + public static function getMessageFromJsonErrorCode( $errorCode ) { + + $errorMessages = array( + JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch, malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found, possibly incorrectly encoded', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', + JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded' + ); + + if ( !isset( $errorMessages[$errorCode] ) ) { + return self::getStringFromJsonErrorCode( $errorCode ); + } + + return sprintf( + "Expected a JSON compatible format but failed with '%s'", + $errorMessages[$errorCode] + ); + } + +} diff --git a/src/ParserParameterProcessor.php b/src/ParserParameterProcessor.php index 2b965cc0a0..8cbad0496b 100644 --- a/src/ParserParameterProcessor.php +++ b/src/ParserParameterProcessor.php @@ -2,6 +2,8 @@ namespace SMW; +use SMW\Libs\ErrorCode; + /** * @license GNU GPL v2+ * @since 1.9 @@ -108,6 +110,8 @@ public function toArray() { /** * @since 2.3 * + * @param string $key + * * @return boolean */ public function hasParameter( $key ) { @@ -115,11 +119,23 @@ public function hasParameter( $key ) { } /** + * @deprecated since 2.5, use ParserParameterProcessor::getParameterValuesByKey * @since 2.3 * * @return array */ public function getParameterValuesFor( $key ) { + return $this->getParameterValuesByKey( $key ); + } + + /** + * @since 2.5 + * + * @param string $key + * + * @return array + */ + public function getParameterValuesByKey( $key ) { if ( $this->hasParameter( $key ) ) { return $this->parameters[$key]; @@ -129,11 +145,9 @@ public function getParameterValuesFor( $key ) { } /** - * Inject parameters from an outside source - * * @since 1.9 * - * @param array + * @param array $parameters */ public function setParameters( array $parameters ) { $this->parameters = $parameters; @@ -172,7 +186,7 @@ private function doMap( array $params ) { $previousProperty = null; while ( key( $params ) !== null ) { - $separator = ''; + $pipe = false; $values = array(); @@ -185,23 +199,7 @@ private function doMap( array $params ) { $currentElement = explode( '=', trim( current ( $params ) ), 2 ); // Looking to the next element for comparison - if( next( $params ) ) { - $nextElement = explode( '=', trim( current( $params ) ), 2 ); - - if ( $nextElement !== array() ) { - // This allows assignments of type |Has property=Test1,Test2|+sep=, - // as a means to support multiple value declaration - if ( substr( $nextElement[0], - 5 ) === '+sep' ) { - $separator = isset( $nextElement[1] ) ? $nextElement[1] !== '' ? $nextElement[1] : $this->defaultSeparator : $this->defaultSeparator; - next( $params ); - } - } - - if ( current( $params ) === '+pipe' ) { - $pipe = true; - next( $params ); - } - } + $separator = $this->lookAheadOnNextElement( $params, $pipe ); // First named parameter if ( count( $currentElement ) == 1 && $previousProperty === null ) { @@ -226,7 +224,7 @@ private function doMap( array $params ) { // Remap properties and values to output a simple array foreach ( $values as $value ) { - if ( $value !== '' ){ + if ( $value !== '' ) { $results[$currentElement[0]][] = $value; } } @@ -238,6 +236,70 @@ private function doMap( array $params ) { } } - return $results; + return $this->parseFromJson( $results ); } + + private function lookAheadOnNextElement( &$params, &$pipe ) { + + $separator = ''; + + if( !next( $params ) ) { + return $separator; + } + + $nextElement = explode( '=', trim( current( $params ) ), 2 ); + + if ( $nextElement !== array() ) { + // This allows assignments of type |Has property=Test1,Test2|+sep=, + // as a means to support multiple value declaration + if ( substr( $nextElement[0], - 5 ) === '+sep' ) { + $separator = isset( $nextElement[1] ) ? $nextElement[1] !== '' ? $nextElement[1] : $this->defaultSeparator : $this->defaultSeparator; + next( $params ); + } + } + + if ( current( $params ) === '+pipe' ) { + $pipe = true; + next( $params ); + } + + return $separator; + } + + private function parseFromJson( $results ) { + + if ( !isset( $results['@json'] ) || !isset( $results['@json'][0] ) ) { + return $results; + } + + // Restrict the depth to avoid resolving recursive assignment + // that can not be handled beyond the 2:n + $depth = 3; + $params = json_decode( $results['@json'][0], true, $depth ); + + if ( $params === null || json_last_error() !== JSON_ERROR_NONE ) { + $this->addError( Message::encode( + array( + 'smw-parser-invalid-json-format', + ErrorCode::getStringFromJsonErrorCode( json_last_error() ) + ) + ) ); + return $results; + } + + array_walk( $params, function( &$value, $key ) { + + if ( $value === '' ) { + $value = array(); + } + + if ( !is_array( $value ) ) { + $value = array( $value ); + } + } ); + + unset( $results['@json'] ); + return array_merge( $results, $params ); + } + } diff --git a/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.1.json b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.1.json new file mode 100644 index 0000000000..f1fc060710 --- /dev/null +++ b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.1.json @@ -0,0 +1,20 @@ +@source: http://www.simile-widgets.org/exhibit3/examples/other-versions/HEAD/icd/icd10-infectious.json + +{{#subobject: + |@category=P0211 + |@json={ + "kind": "chapter", + "code": "I", + "exclusion": "carrier or suspected carrier of infectious diseaseZ22.-", + "icdId": "http://id.who.int/icd/icd10/I", + "inclusion": "diseases generally recognized as communicable or transmissible", + "label": "Certain infectious and parasitic diseases", + "link": "http://apps.who.int/classifications/icd10/browse/2010/en#/I", + "subclasses": [ + "http://id.who.int/icd/icd10/A00-A09", + "http://id.who.int/icd/icd10/A15-A19", + "http://id.who.int/icd/icd10/A20-A28" + ], + "id": "http://id.who.int/icd/icd10/I" +} +}} \ No newline at end of file diff --git a/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.2.json b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.2.json new file mode 100644 index 0000000000..4bb0aad541 --- /dev/null +++ b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.2.json @@ -0,0 +1,27 @@ +@source: http://www.simile-widgets.org/exhibit3/examples/other-versions/HEAD/icd/icd10-infectious.json + +{{#set: + |@json={ + "kind": "block", + "code": "A00-A09", + "icdId": "http://id.who.int/icd/icd10/A00-A09", + "label": "Intestinal infectious diseases", + "link": "http://apps.who.int/classifications/icd10/browse/2010/en#/A00-A09", + "superclasses": [ + "http://id.who.int/icd/icd10/I" + ], + "subclasses": [ + "http://id.who.int/icd/icd10/A00", + "http://id.who.int/icd/icd10/A01", + "http://id.who.int/icd/icd10/A02", + "http://id.who.int/icd/icd10/A03", + "http://id.who.int/icd/icd10/A04", + "http://id.who.int/icd/icd10/A05", + "http://id.who.int/icd/icd10/A06", + "http://id.who.int/icd/icd10/A07", + "http://id.who.int/icd/icd10/A08", + "http://id.who.int/icd/icd10/A09" + ], + "id": "http://id.who.int/icd/icd10/A00-A09" +} +}} \ No newline at end of file diff --git a/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.3.json b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.3.json new file mode 100644 index 0000000000..b728ce6bee --- /dev/null +++ b/tests/phpunit/Integration/ByJsonScript/Contents/p-0211.3.json @@ -0,0 +1,11 @@ +3+ depth is not permitted + +{{#set: + |@json={ + "foo": { + "kind": { + "kind": "bar" + } + } +} +}} \ No newline at end of file diff --git a/tests/phpunit/Integration/ByJsonScript/Fixtures/p-0211.json b/tests/phpunit/Integration/ByJsonScript/Fixtures/p-0211.json new file mode 100644 index 0000000000..b4178a02d0 --- /dev/null +++ b/tests/phpunit/Integration/ByJsonScript/Fixtures/p-0211.json @@ -0,0 +1,74 @@ +{ + "description": "Test `#set`/`#subobject` to import annotation via `@json` syntax (`wgContLang=en`, `wgLang=en`)", + "properties": [ + { + "name": "Has date", + "contents": "[[Has type::Date]]" + } + ], + "subjects": [ + { + "name": "Example/P0211/1", + "contents": { + "import-from": "/../Contents/p-0211.1.json" + } + }, + { + "name": "Example/P0211/2", + "contents": { + "import-from": "/../Contents/p-0211.2.json" + } + }, + { + "name": "Example/P0211/3", + "contents": { + "import-from": "/../Contents/p-0211.3.json" + } + } + ], + "parser-testcases": [ + { + "about": "#0 #subobject", + "subject": "Example/P0211/1", + "store": { + "semantic-data": { + "strict-mode-valuematch": false, + "propertyCount": 3, + "propertyKeys": [ "_SOBJ", "_SKEY", "_MDAT" ] + } + } + }, + { + "about": "#1 #set", + "subject": "Example/P0211/2", + "store": { + "semantic-data": { + "strict-mode-valuematch": false, + "propertyCount": 10, + "propertyKeys": [ "Kind", "_cod", "IcdId", "Label", "Link", "Superclasses", "Subclasses", "Id", "_SKEY", "_MDAT" ] + } + } + }, + { + "about": "#3 #set depth issue", + "subject": "Example/P0211/3", + "store": { + "semantic-data": { + "strict-mode-valuematch": false, + "propertyCount": 3, + "propertyKeys": [ "_ERRC", "_SKEY", "_MDAT" ] + } + } + } + ], + "settings": { + "wgContLang": "en", + "wgLang": "en", + "smwgPageSpecialProperties": [ "_MDAT" ] + }, + "meta": { + "version": "0.1", + "is-incomplete": false, + "debug": false + } +} \ No newline at end of file diff --git a/tests/phpunit/Unit/ExtraneousLanguage/LanguageFileContentsReaderTest.php b/tests/phpunit/Unit/ExtraneousLanguage/LanguageFileContentsReaderTest.php index ceb8931161..40117b7f08 100644 --- a/tests/phpunit/Unit/ExtraneousLanguage/LanguageFileContentsReaderTest.php +++ b/tests/phpunit/Unit/ExtraneousLanguage/LanguageFileContentsReaderTest.php @@ -106,6 +106,14 @@ public function testReadByLanguageCodeIsForcedToRereadFromFile() { $instance->readByLanguageCode( 'bar', true ); } + public function testTryToReadInaccessibleFileByLanguageThrowsException() { + + $instance = new LanguageFileContentsReader(); + + $this->setExpectedException( 'RuntimeException' ); + $instance->readByLanguageCode( 'foo', true ); + } + /** * This method is just for convenience so that one can quickly add contents to files * without requiring an extra class when extending the language content. Normally the diff --git a/tests/phpunit/Unit/Libs/ErrorCodeTest.php b/tests/phpunit/Unit/Libs/ErrorCodeTest.php new file mode 100644 index 0000000000..bfc694b291 --- /dev/null +++ b/tests/phpunit/Unit/Libs/ErrorCodeTest.php @@ -0,0 +1,48 @@ +assertInternalType( + 'string', + ErrorCode::getStringFromJsonErrorCode( 'Foo' ) + ); + + $contents = json_decode( '{ Foo: Bar }' ); + + $this->assertInternalType( + 'string', + ErrorCode::getStringFromJsonErrorCode( json_last_error() ) + ); + } + + public function testGetMessageFromJsonErrorCode() { + + $this->assertInternalType( + 'string', + ErrorCode::getMessageFromJsonErrorCode( 'Foo' ) + ); + + $contents = json_decode( '{ Foo: Bar }' ); + + $this->assertInternalType( + 'string', + ErrorCode::getMessageFromJsonErrorCode( json_last_error() ) + ); + } + +} diff --git a/tests/phpunit/Unit/ParserParameterProcessorTest.php b/tests/phpunit/Unit/ParserParameterProcessorTest.php index 7817cad3f4..5c537b6658 100644 --- a/tests/phpunit/Unit/ParserParameterProcessorTest.php +++ b/tests/phpunit/Unit/ParserParameterProcessorTest.php @@ -280,6 +280,43 @@ public function parametersDataProvider() { ) ); + // {{#...: + // |@json={ "Foo": 123} + // }} + $provider[] = array( + array( + '@json={ "Foo": 123}' + ), + array( + 'Foo' => array( '123' ) + ) + ); + + // {{#...: + // |@json={ "Foo": [123, 456] } + // }} + $provider[] = array( + array( + '@json={ "Foo": [123, 456] }' + ), + array( + 'Foo' => array( '123', '456' ) + ) + ); + + // Error + // {{#...: + // |@json={ "Foo": [123, 456] } + // }} + $provider[] = array( + array( + '@json={ Foo: [123, 456] }' + ), + array( + '@json' => array( '{ Foo: [123, 456] }' ) + ) + ); + return $provider; }