From 7e1e1d493c65c08f36166a5e484bda1592094568 Mon Sep 17 00:00:00 2001 From: Tobias Oetterer Date: Tue, 29 Nov 2016 18:25:05 +0100 Subject: [PATCH] this adds the parser function ask including tests and documentation --- docs/README.md | 20 ++- docs/mw.smw.ask.md | 108 ++++++++++++ docs/mw.smw.getQueryResult.md | 8 +- src/LibraryFactory.php | 18 +- src/LuaAskResultProcessor.php | 156 ++++++++++++++++++ src/ScribuntoLuaLibrary.php | 100 +++++++---- src/mw.smw.lua | 14 +- .../Unit/LuaAskResultProcessorTest.php | 49 ++++++ .../Unit/ScribuntoLuaLibraryAskTest.php | 49 ++++++ tests/phpunit/Unit/mw.smw.ask.tests.lua | 28 ++++ 10 files changed, 505 insertions(+), 45 deletions(-) create mode 100644 docs/mw.smw.ask.md create mode 100644 src/LuaAskResultProcessor.php create mode 100644 tests/phpunit/Unit/LuaAskResultProcessorTest.php create mode 100644 tests/phpunit/Unit/ScribuntoLuaLibraryAskTest.php create mode 100644 tests/phpunit/Unit/mw.smw.ask.tests.lua diff --git a/docs/README.md b/docs/README.md index f0c4817..98e717e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,8 +6,9 @@ The following functions are available in the `mw.smw` package. - Data retrieval functions - - [`mw.smw.getQueryResult`](mw.smw.getQueryResult.md) + - [`mw.smw.ask`](mw.smw.ask.md) - [`mw.smw.getPropertyType`](mw.smw.getPropertyType.md) + - [`mw.smw.getQueryResult`](mw.smw.getQueryResult.md) - Data storage functions @@ -20,10 +21,23 @@ The following functions are available in the `mw.smw` package. ## Notes +### Difference between `mw.smw.ask` and `mw.smw.getQueryResult` +Both functions allow you to retrieve data from your smw store. The difference lies in the returned table. Where `mw.smw.ask` +returns a very simplistic result set (its values are all pre-formatted and already type cast), `mw.smw.getQueryResult` leaves +you with full control over your returned data, giving you abundant information but delegates all the data processing to you. + +In other words: +* `ask` is a quick and easy way to get data which is already pre-processed and may not suite your needs entirely +(e.g. it does not link page properties). However it utilizes native SMW functionality like printout formatting +(see [smwdoc] for more information) +* `getQueryResult` gets you the full result set in the same format provided by the [api] + +For more information see the sample results in [`mw.smw.ask`](mw.smw.ask.md) and [`mw.smw.getQueryResult`](mw.smw.getQueryResult.md). + ### Using #invoke For a detailed description of the `#invoke` function, please have a look at the [Lua reference][lua] manual. -[lua]: https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual -[api]: https://www.semantic-mediawiki.org/wiki/Serialization_%28JSON%29 [smwdoc]: https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki +[api]: https://www.semantic-mediawiki.org/wiki/Serialization_%28JSON%29 +[lua]: https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual diff --git a/docs/mw.smw.ask.md b/docs/mw.smw.ask.md new file mode 100644 index 0000000..f5a9f74 --- /dev/null +++ b/docs/mw.smw.ask.md @@ -0,0 +1,108 @@ +## mw.smw.ask + +With `mw.smw.ask` you can execute an smw query. It returns the result as a lua table for direct use in modules. +For available parameters, please consult the [Semantic Media Wiki documentation hub][smwdoc]. + +Please see the [return format below](#result) for the difference between this and [`mw.smw.getQueryResult`](mw.smw.getQueryResult.md). + +This is a sample call: +```lua +-- Module:SMW +local p = {} + +-- Return results +function p.ask(frame) + + if not mw.smw then + return "mw.smw module not found" + end + + if frame.args[1] == nil then + return "no parameter found" + end + + local queryResult = mw.smw.ask( frame.args ) + + if queryResult == nil then + return "(no values)" + end + + if type( queryResult ) == "table" then + local myResult = "" + for num, row in pairs( queryResult ) do + myResult = myResult .. '* This is result #' .. num .. '\n' + for k, v in pairs( row ) do + myResult = myResult .. '** ' .. k .. ': ' .. v .. '\n' + end + end + return myResult + end + + return queryResult +end + +-- another example, ask used inside another function +function p.inlineAsk( frame ) + + local entityAttributes = { + 'has name=name', + 'has age=age', + 'has color=color' + } + local category = 'thingies' + + -- build query + local query = {} + table.insert(query, '[[Category:' .. category .. ']]') + + for _, v in pairs( entityAttributes ) do + table.insert( query, '?' .. v ) + end + + query.mainlabel = 'origin' + query.limit = 10 + + local result = mw.smw.ask( query ) + + local output = '' + if result and #result then + + for num, entityData in pairs( result ) do + -- further process your data + output = output .. entityData.origin .. ' (' .. num .. ') has name ' .. entityData.name + .. ', color ' .. entityData.color .. ', and age ' .. entityData.age + end + end + + return output +end + +return p +``` + +### Return format + +The return format is a simple collection of tables (one per result set) holding your smw data, +each indexed by property names or labels respectively. You can see an example below: + +```lua +-- assuming sample call +local result = mw.smw.ask( '[[Modification date::+]]|?Modification date|?Last editor is=editor|limit=2|mainlabel=page' ) +-- your result would look something like +{ + { + ['Modification date'] = '1 January 1970 23:59:59', + editor = 'User:Mwjames', + page = 'Main page' + }, + { + ['Modification date'] = '2 January 1970 00:00:42', + editor = 'User:Oetterer', + page = 'User:Oetterer' + }, +} +``` + +This function is meant to be used inside lua modules and should not be directly exposed to #invoke. + +[smwdoc]: https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki diff --git a/docs/mw.smw.getQueryResult.md b/docs/mw.smw.getQueryResult.md index a2bbeb4..24d64d6 100644 --- a/docs/mw.smw.getQueryResult.md +++ b/docs/mw.smw.getQueryResult.md @@ -3,6 +3,8 @@ With `mw.smw.getQueryResult` you can execute an smw query. It returns the result as a lua table for direct use in modules. For available parameters, please consult the [Semantic Media Wiki documentation hub][smwdoc]. +Please see the [return format below](#result) for the difference between this and [`mw.smw.ask`](mw.smw.ask.md). + This is a sample call: ```lua -- Module:SMW @@ -43,10 +45,13 @@ end return p ``` +### Return format + The return format matches the data structure delivered by the [api]. You can see an example below: + ```lua -- assuming sample call -local result = mw.smw.getQueryResult( '[[Modification date::+]]|?Modification date|?Last editor is|limit=2|mainlabel=page' ) +local result = mw.smw.getQueryResult( '[[Modification date::+]]|?Modification date|?Last editor is=editor|limit=2|mainlabel=page' ) -- your result would look something like { printrequests = { @@ -134,3 +139,4 @@ Calling `{{#invoke:smw|ask|[[Modification date::+]]|?Modification date|limit=0|m makes sense in a template or another module that can handle `table` return values. [smwdoc]: https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki +[api]: https://www.semantic-mediawiki.org/wiki/Serialization_%28JSON%29 \ No newline at end of file diff --git a/src/LibraryFactory.php b/src/LibraryFactory.php index 0485346..ddeffef 100644 --- a/src/LibraryFactory.php +++ b/src/LibraryFactory.php @@ -31,11 +31,14 @@ public function __construct( Store $store ) { } /** + * Creates a new SMWQueryResult from passed arguments, + * utilizing the {@see SMWQueryProcessor} + * * @since 1.0 * * @param string|array $rawParameters * - * @return QueryResult + * @return \SMWQueryResult */ public function newQueryResultFrom( $rawParameters ) { @@ -58,36 +61,41 @@ public function newQueryResultFrom( $rawParameters ) { } /** + * Creates a new ParserParameterProcessor from passed arguments + * * @since 1.0 * * @param array $arguments * - * @return ParserParameterProcessor + * @return \SMW\ParserParameterProcessor */ public function newParserParameterProcessorFrom( $arguments ) { return ParameterProcessorFactory::newFromArray( $arguments ); } /** + * Creates a new SetParserFunction utilizing a Parser + * * @since 1.0 * * @param Parser $parser * - * @return SetParserFunction + * @return \SMW\SetParserFunction */ public function newSetParserFunction( Parser $parser ) { return ApplicationFactory::getInstance()->newParserFunctionFactory( $parser )->newSetParserFunction( $parser ); } /** + * Creates a new SubobjectParserFunction utilizing a Parser + * * @since 1.0 * * @param Parser $parser * - * @return SubobjectParserFunction + * @return \SMW\SubobjectParserFunction */ public function newSubobjectParserFunction( Parser $parser ) { return ApplicationFactory::getInstance()->newParserFunctionFactory( $parser )->newSubobjectParserFunction( $parser ); } - } diff --git a/src/LuaAskResultProcessor.php b/src/LuaAskResultProcessor.php new file mode 100644 index 0000000..1ec151f --- /dev/null +++ b/src/LuaAskResultProcessor.php @@ -0,0 +1,156 @@ +queryResult = $queryResult; + $this->msgTrue = explode( ',', wfMessage( 'smw_true_words' )->text() . ',true,t,yes,y' ); + } + + /** + * Extracts the data in {@see $queryResult} and returns it as a table + * usable in lua context + * + * @return array|null + */ + public function getQueryResultAsLuaTable() { + if ( $this->queryResult == null ) { + return null; + } + + $result = array(); + + while ( $resultRow = $this->queryResult->getNext() ) { + $result[] = $this->getDataFromQueryResultRow( $resultRow ); + } + + return $result; + } + + /** + * Extracts the data of a single result row in the {@see $queryResult} + * and returns it as a table usable in lua context + * + * @param array $resultRow result row as an array of {@see SMWResultArray} objects + * + * @return array + */ + private function getDataFromQueryResultRow( array $resultRow ) { + + $rowData = array(); + $numberFallBack = 1; + + /** @var \SMWResultArray $resultArray */ + foreach ( $resultRow as $resultArray ) { + // find out, which key to use for this printRequest / query field + if ( $resultArray->getPrintRequest()->getLabel() === '' ) { + $key = $numberFallBack++; + } else { + $key = $resultArray->getPrintRequest()->getText( SMW_OUTPUT_WIKI ); + } + + // and get the data + $rowData[$key] = $this->getDataFromSMWResultArray( $resultArray ); + } + + return $rowData; + } + + /** + * Extracts the data of a single printRequest / query field + * + * @param \SMWResultArray $resultArray + * + * @return mixed + */ + private function getDataFromSMWResultArray( \SMWResultArray $resultArray ) { + + $resultArrayData = array(); + + // get all data that is stored in this printRequest / query field + /** @var \SMWDataValue $dataValue */ + while ( ( $dataValue = $resultArray->getNextDataValue() ) !== false ) { + + $resultArrayData[] = $this->getValueFromSMWDataValue( $dataValue ); + } + + if ( ! $resultArrayData || ! count( $resultArrayData ) ) { + // this key has no value(s). set to null + $resultArrayData = null; + } elseif ( count( $resultArrayData ) == 1 ) { + // there was only one semantic value found. reduce the array to this value + $resultArrayData = array_shift( $resultArrayData ); + } else { + // $key has multiple values. keep the array, but un-shift it (remember: lua array counting starts with 1) + array_unshift( $resultArrayData, null ); + } + + return $resultArrayData; + } + + /** + * Extracts the data of a single value of the current printRequest / query field + * The value is formatted according to the type of the property + * + * @param \SMWDataValue $dataValue + * + * @return mixed + */ + private function getValueFromSMWDataValue( \SMWDataValue $dataValue ) { + + $value = null; + + switch ( $dataValue->getTypeID() ) { + case '_boo': + // boolean value found, convert it + $value = in_array( $dataValue->getShortText( SMW_OUTPUT_WIKI ), $this->msgTrue ); + break; + case '_num': + // number value found + /** @var \SMWNumberValue $dataValue */ + $value = $dataValue->getNumber(); + $value = ( $value == (int) $value ) ? intval( $value ) : $value; + break; + case '_wpg' : + /** @var \SMWWikiPageValue $dataValue */ + $value = $dataValue->getShortWikiText( ); + // $value = $dataValue->getShortText( SMW_OUTPUT_WIKI ); + break; + default: + $value = $dataValue->getShortText( SMW_OUTPUT_WIKI ); + } + + return $value; + } +} \ No newline at end of file diff --git a/src/ScribuntoLuaLibrary.php b/src/ScribuntoLuaLibrary.php index ad4dda2..24402ad 100644 --- a/src/ScribuntoLuaLibrary.php +++ b/src/ScribuntoLuaLibrary.php @@ -34,6 +34,7 @@ class ScribuntoLuaLibrary extends Scribunto_LuaLibraryBase { public function register() { $lib = array( + 'ask' => array( $this, 'ask' ), 'getPropertyType' => array( $this, 'getPropertyType' ), 'getQueryResult' => array( $this, 'getQueryResult' ), 'info' => array( $this, 'info' ), @@ -45,27 +46,33 @@ public function register() { } /** - * Returns query results in for of the standard API return format + * This mirrors the functionality of the parser function #ask and makes it + * available in lua. * * @since 1.0 * - * @param string|array $arguments + * @param string|array $arguments parameters passed from lua, string or array depending on call * - * @return array + * @return null|array[] */ - public function getQueryResult( $arguments = null ) { + public function ask ( $arguments = null ) { - $queryResult = $this->getLibraryFactory()->newQueryResultFrom( - $this->processLuaArguments( $arguments ) - ); + // if we have no arguments, do nothing + if ( !count( $arguments = $this->processLuaArguments( $arguments ) ) ) { + return null; + } - $result = $queryResult->toArray(); + $queryResult = $this->getLibraryFactory()->newQueryResultFrom( $arguments ); - if( !empty( $result["results"] ) ) { - $result["results"] = array_combine( range( 1, count( $result["results"] ) ), array_values( $result["results"] ) ); + $luaResultProcessor = new LuaAskResultProcessor( $queryResult ); + + $result = $luaResultProcessor->getQueryResultAsLuaTable(); + + if ( is_array( $result ) && count ( $result ) ) { + array_unshift( $result, null ); } - return array( $result ); + return array ( $result ); } /** @@ -95,6 +102,30 @@ public function getPropertyType( $propertyName = null ) { return array( $property->findPropertyTypeID() ); } + /** + * Returns query results in for of the standard API return format + * + * @since 1.0 + * + * @param string|array $arguments + * + * @return array + */ + public function getQueryResult( $arguments = null ) { + + $queryResult = $this->getLibraryFactory()->newQueryResultFrom( + $this->processLuaArguments( $arguments ) + ); + + $result = $queryResult->toArray(); + + if( !empty( $result["results"] ) ) { + $result["results"] = array_combine( range( 1, count( $result["results"] ) ), array_values( $result["results"] ) ); + } + + return array( $result ); + } + /** * This mirrors the functionality of the parser function #info and makes it * available to lua. @@ -141,16 +172,16 @@ public function info( $text, $icon = 'info' ) { * * @since 1.0 * - * @param string|array $parameters parameters passed from lua, string or array depending on call + * @param string|array $arguments parameters passed from lua, string or array depending on call * * @return null|array|array[] */ - public function set( $parameters ) { + public function set( $arguments ) { $parser = $this->getEngine()->getParser(); // if we have no arguments, do nothing - if ( !count( $arguments = $this->processLuaArguments( $parameters ) ) ) { + if ( !count( $arguments = $this->processLuaArguments( $arguments ) ) ) { return null; } @@ -164,7 +195,7 @@ public function set( $parameters ) { $result = $this->doPostProcessParserFunctionCallResult( $parserFunctionCallResult ); if ( strlen( $result ) ) { - // if result a non empty string, assume an error message + // if result is a non empty string, assume an error message return array( [ 1 => false, self::SMW_ERROR_FIELD => preg_replace( '/<[^>]+>/', '', $result ) ] ); } else { // on success, return true @@ -176,17 +207,17 @@ public function set( $parameters ) { * This mirrors the functionality of the parser function #subobject and * makes it available to lua. * - * @param string|array $parameters parameters passed from lua, string or array depending on call + * @param string|array $arguments parameters passed from lua, string or array depending on call * @param string $subobjectId if you need to manually assign an id, do this here * * @return null|array|array[] */ - public function subobject( $parameters, $subobjectId = null ) { + public function subobject( $arguments, $subobjectId = null ) { $parser = $this->getEngine()->getParser(); // if we have no arguments, do nothing - if ( !count( $arguments = $this->processLuaArguments( $parameters ) ) ) { + if ( !count( $arguments = $this->processLuaArguments( $arguments ) ) ) { return null; } @@ -251,6 +282,26 @@ private function doPostProcessParserFunctionCallResult( $parserFunctionResult ) return trim( $result ); } + /** + * Returns the LibraryFactory singleton (and creates it first, if not already present) + * + * @since 1.0 + * + * @return LibraryFactory + */ + private function getLibraryFactory() { + + if ( $this->libraryFactory !== null ) { + return $this->libraryFactory; + } + + $this->libraryFactory = new LibraryFactory( + ApplicationFactory::getInstance()->getStore() + ); + + return $this->libraryFactory; + } + /** * Takes the $arguments passed from lua and pre-processes them: make sure, * we have a sequence array (not associative) @@ -285,17 +336,4 @@ private function processLuaArguments( $arguments ) { return $processedArguments; } - private function getLibraryFactory() { - - if ( $this->libraryFactory !== null ) { - return $this->libraryFactory; - } - - $this->libraryFactory = new LibraryFactory( - ApplicationFactory::getInstance()->getStore() - ); - - return $this->libraryFactory; - } - } diff --git a/src/mw.smw.lua b/src/mw.smw.lua index a02bbb0..9dd9f07 100644 --- a/src/mw.smw.lua +++ b/src/mw.smw.lua @@ -24,11 +24,9 @@ function smw.setupInterface() package.loaded['mw.smw'] = smw end --- getQueryResult -function smw.getQueryResult( queryString ) - local queryResult = php.getQueryResult( queryString ) - if queryResult == nil then return nil end - return queryResult +-- ask +function smw.ask( parameters ) + return php.ask( parameters ) end -- getPropertyType @@ -36,6 +34,12 @@ function smw.getPropertyType( name ) return php.getPropertyType( name ) end +-- getQueryResult +function smw.getQueryResult( queryString ) + local queryResult = php.getQueryResult( queryString ) + if queryResult == nil then return nil end + return queryResult +end -- info function smw.info( text, icon ) diff --git a/tests/phpunit/Unit/LuaAskResultProcessorTest.php b/tests/phpunit/Unit/LuaAskResultProcessorTest.php new file mode 100644 index 0000000..7422799 --- /dev/null +++ b/tests/phpunit/Unit/LuaAskResultProcessorTest.php @@ -0,0 +1,49 @@ +queryResult = $this->getMockBuilder( '\SMWQueryResult' ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + } + + public function testCanConstruct() { + + $this->assertInstanceOf( + '\SMW\Scribunto\LuaAskResultProcessor', + new LuaAskResultProcessor( + $this->queryResult + ) + ); + + } +} diff --git a/tests/phpunit/Unit/ScribuntoLuaLibraryAskTest.php b/tests/phpunit/Unit/ScribuntoLuaLibraryAskTest.php new file mode 100644 index 0000000..bd20742 --- /dev/null +++ b/tests/phpunit/Unit/ScribuntoLuaLibraryAskTest.php @@ -0,0 +1,49 @@ + __DIR__ . '/' . 'mw.smw.ask.tests.lua', + ); + } + + + /** + * Tests method info + * + * @return void + */ + public function testAsk() { + $this->assertEmpty( + $this->getScribuntoLuaLibrary()->ask( null )[0] + ); + $this->assertEmpty( + $this->getScribuntoLuaLibrary()->ask( '' )[0] + ); + $this->assertArrayHasKey( + 0, + $this->getScribuntoLuaLibrary()->ask( '[[Modification date::+]]|?Modification date|limit=2|mainlabel=main' ) + ); + } +} \ No newline at end of file diff --git a/tests/phpunit/Unit/mw.smw.ask.tests.lua b/tests/phpunit/Unit/mw.smw.ask.tests.lua new file mode 100644 index 0000000..f94f15b --- /dev/null +++ b/tests/phpunit/Unit/mw.smw.ask.tests.lua @@ -0,0 +1,28 @@ +--[[ + Tests for smw.info module + + @since 1.0 + + @licence GNU GPL v2+ + @author Tobias Oetterer +]] + +local testframework = require 'Module:TestFramework' + +-- Tests +local tests = { + { name = 'ask (nil argument)', func = mw.smw.ask, + args = { nil }, + expect = { nil } + }, + { name = 'ask (no argument)', func = mw.smw.ask, + args = { '' }, + expect = { nil } + }, + { name = 'ask (limit 0)', func = mw.smw.ask, + args = { { '[[Modification date::+]]', '?Modification date', limit = 0, mainlabel = 'main' } }, + expect = { {} } + }, +} + +return testframework.getTestProvider( tests )