jenkins-bot has submitted this change and it was merged. Change subject: \SMW\ApiBrowse enables to browse a subject via api.php?action=browse ......................................................................
\SMW\ApiBrowse enables to browse a subject via api.php?action=browse Code coverage: 100% CRAP: 22 This is the corresponding Api for the Special:Browse interface (without support of incoming properties). In order to work with the Api, the following schema [2] can be used. - Uses the new SMW\Serializer [1] - JSON and XML output are supported [1] https://gerrit.wikimedia.org/r/#/c/89098/ [2] api.php?action=browse&subject=Main_Page Change-Id: I00c2fab386d0c74ef0b576876edb2f873c17bfde --- M SemanticMediaWiki.classes.php A docs/api.md M includes/Setup.php A includes/api/ApiBrowse.php M includes/dataitems/SMW_DI_Property.php M tests/phpunit/MockObjectRepository.php M tests/phpunit/includes/SetupTest.php A tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php A tests/phpunit/includes/api/ApiBrowseTest.php 9 files changed, 663 insertions(+), 2 deletions(-) Approvals: Mwjames: Looks good to me, approved jenkins-bot: Verified diff --git a/SemanticMediaWiki.classes.php b/SemanticMediaWiki.classes.php index 7f6544a..db6248f 100644 --- a/SemanticMediaWiki.classes.php +++ b/SemanticMediaWiki.classes.php @@ -359,6 +359,7 @@ 'SMW\ApiAsk' => 'includes/api/ApiAsk.php', 'SMW\ApiAskArgs' => 'includes/api/ApiAskArgs.php', 'SMW\ApiInfo' => 'includes/api/ApiInfo.php', + 'SMW\ApiBrowse' => 'includes/api/ApiBrowse.php', // Maintenance scripts 'SMWSetupScript' => 'maintenance/SMW_setup.php', diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..2ca1e54 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,128 @@ +This file contains details about Semantic MediaWiki's API for external use with a description that is reflecting the current master branch (SMW 1.9). For more details on "how to use" MediaWiki's WebAPI, it is recommended to read this [website][api]. + +## AskApi +The Ask API allows you to do ask queries against SMW using the MediaWiki API and get results back serialized in one of the formats it supports. + +The ask module supports one parameter, query, which takes the same string you'd feed into an #ask tag, but urlencoded. + +> api.php?action=ask&query=[[Modification date::%2B]]|%3FModification date|sort%3DModification date|order%3Ddesc&format=jsonfm + +### AskArgsApi +The Askargs module aims to take arguments in un-serialized form, so with as little ask-specific syntax as possible. It supports 3 arguments: + +* "conditions": The query conditions, ie the requirements for a subject to be included +* "printouts": The query printeouts, ie the properties to show per subject +* "parameters": The query parameters, ie all non-condition and non-printeout arguments + +> api.php?action=askargs&conditions=Modification date::%2B&printouts=Modification date¶meters=|sort%3DModification date|order%3Ddesc&format=jsonfm + +#### Output serialization +```php +{ + "query-continue-offset": 50, + "query": { + "printrequests": [ + { + "label": "", + "typeid": "_wpg", + "mode": 2, + "format": false + }, + { + "label": "Modification date", + "typeid": "_dat", + "mode": 1, + "format": "" + } + ], + "results": { + "Main Page": { + "printouts": { + "Modification date": [ + "1381456128" + ] + }, + "fulltext": "Main Page", + "fullurl": "http:\/\/localhost:8080\/mw\/index.php\/Main_Page", + "namespace": 0, + "exists": true + }, + ... + }, + "meta": { + "hash": "a9abdb34024fa8735f6b044305a48619", + "count": 50, + "offset": 0 + } + } +} +``` + +## AskInfo +An interface to access statistical information about the properties, values etc.. + +> api.php?action=smwinfo&format=json&info=proppagecount|propcount + +The following parameters are available and can be concatenate using the "|" character. +* proppagecount +* propcount +* querycount +* usedpropcount +* declaredpropcount +* conceptcount +* querysize +* subobjectcount +* formatcount + +#### Output serialization + +```php +{ + "info": { + "proppagecount": 40, + "formatcount": { + "table": 14, + "list": 3, + "broadtable": 1 + } + } +} +``` +The parameter "formatcount" will output an array of used formats together with its count information. + +## BrowseApi +An interface to browse facts of a subject (wikipage). + +> api.php?action=browse&subject=Main%20Page + +#### Output serialization + +```php +{ + "query": { + "subject": "Main_Page#0#", + "data": [ + { + "property": "Foo", + "dataitem": [ + { + "type": 2, + "item": "Bar" + } + ] + }, + ... + "serializer": "SMW\Serializers\SemanticDataSerializer", + "version": 0.1 + } +} +``` +The output is generated using the <code>SMW\SerializerFactory</code> which if necessary can also be used to un-serialize the data received from the Api. For details about the output format and how to use <code>SMW\SerializerFactory</code>, see <code>/docs/serializer.md</code>. + +```php +$pai = new ApiBrowse( ... ) +$result = $api->getResultData(); +$semanticData = SerializerFactory::deserialize( $result['query'] ); +``` + +[api]: https://www.mediawiki.org/wiki/Api "Manual:Api" \ No newline at end of file diff --git a/includes/Setup.php b/includes/Setup.php index 3a59cbd..fed4ac9 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -151,6 +151,7 @@ $this->globals['wgAPIModules']['smwinfo'] = '\SMW\ApiInfo'; $this->globals['wgAPIModules']['ask'] = '\SMW\ApiAsk'; $this->globals['wgAPIModules']['askargs'] = '\SMW\ApiAskArgs'; + $this->globals['wgAPIModules']['browse'] = '\SMW\ApiBrowse'; } diff --git a/includes/api/ApiBrowse.php b/includes/api/ApiBrowse.php new file mode 100644 index 0000000..b7525a2 --- /dev/null +++ b/includes/api/ApiBrowse.php @@ -0,0 +1,191 @@ +<?php + +namespace SMW; + +use Title; + +/** + * Api module to browse a subject + * + * @since 1.9 + * + * @licence GNU GPL v2+ + * @author mwjames + */ +class ApiBrowse extends ApiBase { + + /** + * @see ApiBase::execute + */ + public function execute() { + + $params = $this->extractRequestParams(); + + $serialized = SerializerFactory::serialize( + $this->getSemanticData( $this->getSubject( $params['subject'] ) ) + ); + + $this->getResult()->addValue( null, 'query', $this->runFormatter( $serialized ) ); + } + + /** + * @since 1.9 + */ + protected function getSubject( $text ) { + return DIWikiPage::newFromTitle( $this->newFromText( $text ) ); + } + + /** + * @codeCoverageIgnore + * @since 1.9 + */ + protected function newFromText( $text ) { + + $title = Title::newFromText( $text ); + + if ( $title instanceOf Title ) { + return $title; + } + + $this->dieUsageMsg( array( 'invalidtitle', $text ) ); + } + + /** + * @since 1.9 + */ + protected function getSemanticData( DIWikiPage $subject ) { + + $semanticData = $this->getStore()->getSemanticData( $subject ); + + foreach ( $semanticData->getProperties() as $property ) { + if ( $property->getKey() === DIProperty::TYPE_SUBOBJECT || $property->getKey() === DIProperty::TYPE_ASKQUERY ) { + $this->addSubSemanticData( $property, $semanticData ); + } + } + + return $semanticData; + } + + /** + * @note In case where the original SemanticData container does not include + * subobjects, this method will add them to ensure a "complete object" for + * all available entities that belong to this subject (excluding incoming + * properties) + * + * @note If the subobject already exists within the current SemanticData + * instance it will not be imported again (this avoids calling the Store + * again) + * + * @since 1.9 + */ + protected function addSubSemanticData( $property, &$semanticData ) { + + $subSemanticData = $semanticData->getSubSemanticData(); + + foreach ( $semanticData->getPropertyValues( $property ) as $value ) { + if ( $value instanceOf DIWikiPage && !isset( $subSemanticData[ $value->getSubobjectName() ] ) ) { + $semanticData->addSubSemanticData( $this->getStore()->getSemanticData( $value ) ); + } + } + } + + /** + * @since 1.9 + */ + protected function runFormatter( $serialized ) { + + $this->addIndexTags( $serialized ); + + if ( isset( $serialized['sobj'] ) ) { + + $this->getResult()->setIndexedTagName( $serialized['sobj'], 'subobject' ); + + foreach ( $serialized['sobj'] as $key => &$value ) { + $this->addIndexTags( $value ); + } + } + + return $serialized; + } + + /** + * @since 1.9 + */ + protected function addIndexTags( &$serialized ) { + + if ( isset( $serialized['data'] ) ) { + + $this->getResult()->setIndexedTagName( $serialized['data'], 'property' ); + + foreach ( $serialized['data'] as $key => $value ) { + if ( isset( $serialized['data'][ $key ]['dataitem'] ) ) { + $this->getResult()->setIndexedTagName( $serialized['data'][ $key ]['dataitem'], 'value' ); + } + } + } + + } + + /** + * @codeCoverageIgnore + * @see ApiBase::getAllowedParams + * + * @return array + */ + public function getAllowedParams() { + return array( + 'subject' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => false, + ApiBase::PARAM_REQUIRED => true, + ) + ); + } + + /** + * @codeCoverageIgnore + * @see ApiBase::getParamDescription + * + * @return array + */ + public function getParamDescription() { + return array( + 'subject' => 'The subject to be queried', + ); + } + + /** + * @codeCoverageIgnore + * @see ApiBase::getDescription + * + * @return array + */ + public function getDescription() { + return array( + 'API module to query a subject.' + ); + } + + /** + * @codeCoverageIgnore + * @see ApiBase::getExamples + * + * @return array + */ + protected function getExamples() { + return array( + 'api.php?action=browse&subject=Main_Page', + ); + } + + /** + * @codeCoverageIgnore + * @see ApiBase::getVersion + * + * @return string + */ + public function getVersion() { + return __CLASS__ . '-' . SMW_VERSION; + } + +} diff --git a/includes/dataitems/SMW_DI_Property.php b/includes/dataitems/SMW_DI_Property.php index 5642cc3..769f3eb 100644 --- a/includes/dataitems/SMW_DI_Property.php +++ b/includes/dataitems/SMW_DI_Property.php @@ -43,6 +43,8 @@ const TYPE_HAS_TYPE = '_TYPE'; // Property "corresponds to" const TYPE_CONVERSION = '_CONV'; + // Property "has query" + const TYPE_ASKQUERY = '_ASK'; /** * Array for assigning types to predefined properties. Each @@ -408,7 +410,7 @@ '_SF_DF' => array( '__spf', true ), // Semantic Form's default form property '_SF_AF' => array( '__spf', true ), // Semantic Form's alternate form property self::TYPE_SUBOBJECT => array( '_wpg', true ), // "has subobject" - '_ASK' => array( '_wpg', false ), // "has query" + self::TYPE_ASKQUERY => array( '_wpg', false ), // "has query" '_ASKST' => array( '_cod', true ), // "has query string" '_ASKFO' => array( '_txt', true ), // "has query format" '_ASKSI' => array( '_num', true ), // "has query size" diff --git a/tests/phpunit/MockObjectRepository.php b/tests/phpunit/MockObjectRepository.php index c768114..5cc885f 100644 --- a/tests/phpunit/MockObjectRepository.php +++ b/tests/phpunit/MockObjectRepository.php @@ -642,7 +642,7 @@ $store->expects( $this->any() ) ->method( 'getSemanticData' ) - ->will( $this->returnValue( $this->builder->setValue( 'getSemanticData' ) ) ); + ->will( $this->builder->setCallback( 'getSemanticData' ) ); $store->expects( $this->any() ) ->method( 'getUnusedPropertiesSpecial' ) diff --git a/tests/phpunit/includes/SetupTest.php b/tests/phpunit/includes/SetupTest.php index 9b134cf..e8b28cb 100644 --- a/tests/phpunit/includes/SetupTest.php +++ b/tests/phpunit/includes/SetupTest.php @@ -394,6 +394,7 @@ 'ask', 'smwinfo', 'askargs', + 'browse', ); foreach ( $modules as $module ) { diff --git a/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php b/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php new file mode 100644 index 0000000..55c6f06 --- /dev/null +++ b/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php @@ -0,0 +1,195 @@ +<?php + +namespace SMW\Test; + +use SMW\SerializerFactory; +use SMW\DataValueFactory; +use SMW\ApiBrowse; +use SMW\SemanticData; +use SMW\DIWikiPage; +use SMW\Subobject; + +/** + * @covers \SMW\ApiBrowse + * + * @since 1.9 + * + * @group SMW + * @group SMWExtension + * @group API + * + * @licence GNU GPL v2+ + * @author mwjames + */ +class ApiBrowseSerializationRoundtripTest extends ApiTestCase { + + /** + * @return string|false + */ + public function getClass() { + return '\SMW\ApiBrowse'; + } + + /** + * @since 1.9 + */ + private function newSemanticData( $text ) { + return $data = new SemanticData( DIWikiPage::newFromTitle( $this->newTitle( NS_MAIN, $text ) ) ); + } + + /** + * @dataProvider semanticDataProvider + * + * @since 1.9 + */ + public function testExecuteOnMockStore( $setup ) { + + $api = new ApiBrowse( $this->getApiMain( array( 'subject' => $setup['subject'] ) ), 'browse' ); + $api->setStore( $setup['store'] ); + $api->execute(); + + $result = $api->getResultData(); + + $this->assertStructuralIntegrity( $setup, $result ); + + // We gimmick a bit here otherwise matching the array will be cumbersome, + // we'll use the result from the Api and recreate (using the Serializer) + // the SemanticData container and then compare the hash from the original + // container with that of the newly created container from the Api + $this->assertEquals( + $setup['data']->getHash(), + SerializerFactory::deserialize( $result['query'] )->getHash(), + 'Asserts that getHash() compares to both SemanticData containers' + ); + + } + + /** + * The Serializer enforces a specific output format therefore expected + * elements are verified + * + * @since 1.9 + */ + public function assertStructuralIntegrity( $type, $result ) { + + if ( isset( $type['hasError'] ) && $type['hasError'] ) { + $this->assertInternalType( 'array', $result['error'] ); + } + + if ( isset( $type['hasResult'] ) && $type['hasResult'] ) { + $this->assertInternalType( 'array', $result['query'] ); + } + + if ( isset( $type['hasSubject'] ) && $type['hasSubject'] ) { + $this->assertInternalType( 'string', $result['query']['subject'] ); + } + + if ( isset( $type['hasData'] ) && $type['hasData'] ) { + $this->assertInternalType( 'array', $result['query']['data'] ); + } + + if ( isset( $type['hasSobj'] ) && $type['hasSobj'] ) { + $this->assertInternalType( 'array', $result['query']['sobj'] ); + } + + } + + /** + * @return array + */ + public function semanticDataProvider() { + + $provider = array(); + + // #0 Empty container + $data = $this->newSemanticData( 'Foo-0' ); + + $mockStore = $this->newMockBuilder()->newObject( 'Store', array( + 'getSemanticData' => $data + ) ); + + $provider[] = array( + array( + 'subject' => 'Foo-0', + 'store' => $mockStore, + 'data' => $data, + 'hasData' => false, + 'hasSobj' => false + ) + ); + + // #1 Single entry + $data = $this->newSemanticData( 'Foo-1' ); + $data->addDataValue( DataValueFactory::newPropertyValue( 'Has fooQuex', 'Bar' ) ); + + $mockStore = $this->newMockBuilder()->newObject( 'Store', array( + 'getSemanticData' => $data + ) ); + + $provider[] = array( + array( + 'subject' => 'Foo-1', + 'store' => $mockStore, + 'data' => $data, + 'hasData' => true, + 'hasSobj' => false + ) + ); + + // #2 Single + single subobject entry + $title = $this->newTitle( NS_MAIN, 'Foo-2' ); + $data = $this->newSemanticData( 'Foo-2' ); + $data->addDataValue( DataValueFactory::newPropertyValue( 'Has fooQuex', 'Bar' ) ); + + $subobject = new Subobject( $title ); + $subobject->setSemanticData( 'Foo-sub' ); + $subobject->addDataValue( DataValueFactory::newPropertyValue( 'Has subobjects', 'Bam' ) ); + + // Adding a reference but not the container itself + $data->addPropertyObjectValue( $subobject->getProperty(), $subobject->getSemanticData()->getSubject() ); + + $mockStore = $this->newMockBuilder()->newObject( 'Store', array( + 'getSemanticData' => function ( $subject ) use( $data, $subobject ) { + return $subject->getSubobjectName() === 'Foo-sub' ? $subobject->getSemanticData() : $data; + } + ) ); + + $provider[] = array( + array( + 'subject' => 'Foo-2', + 'store' => $mockStore, + 'data' => $data, + 'hasData' => true, + 'hasSobj' => true + ) + ); + + // #3 Single + single subobject where the subobject already exists + $title = $this->newTitle( NS_MAIN, 'Foo-3' ); + $data = $this->newSemanticData( 'Foo-3' ); + $data->addDataValue( DataValueFactory::newPropertyValue( 'Has fooQuex', 'Bar' ) ); + + $subobject = new Subobject( $title ); + $subobject->setSemanticData( 'Foo-sub' ); + $subobject->addDataValue( DataValueFactory::newPropertyValue( 'Has subobjects', 'Bam' ) ); + + $data->addPropertyObjectValue( $subobject->getProperty(), $subobject->getContainer() ); + + $mockStore = $this->newMockBuilder()->newObject( 'Store', array( + 'getSemanticData' => $data + ) ); + + $provider[] = array( + array( + 'subject' => 'Foo-3', + 'store' => $mockStore, + 'data' => $data, + 'hasData' => true, + 'hasSobj' => true + ) + ); + + return $provider; + } + +} diff --git a/tests/phpunit/includes/api/ApiBrowseTest.php b/tests/phpunit/includes/api/ApiBrowseTest.php new file mode 100644 index 0000000..3e14073 --- /dev/null +++ b/tests/phpunit/includes/api/ApiBrowseTest.php @@ -0,0 +1,142 @@ +<?php + +namespace SMW\Test; + +use SMW\ApiBrowse; +use SMW\SemanticData; +use SMW\DIWikiPage; + +/** + * @covers \SMW\ApiBrowse + * + * @since 1.9 + * + * @group SMW + * @group SMWExtension + * @group API + * + * @licence GNU GPL v2+ + * @author mwjames + */ +class ApiBrowseTest extends ApiTestCase { + + /** + * @return string|false + */ + public function getClass() { + return '\SMW\ApiBrowse'; + } + + /** + * @since 1.9 + */ + private function newSemanticData( $text ) { + return $data = new SemanticData( DIWikiPage::newFromTitle( $this->newTitle( NS_MAIN, $text ) ) ); + } + + /** + * @dataProvider subjectDataProvider + * + * @since 1.9 + */ + public function testExecuteOnSQLStore( $setup ) { + + $this->runOnlyOnSQLStore(); + + $result = $this->doApiRequest( array( + 'action' => 'browse', + 'subject' => $setup['subject'] + ) ); + + $this->assertStructuralIntegrity( $setup, $result ); + + } + + /** + * @dataProvider invalidTitleDataProvider + * + * @since 1.9 + */ + public function testInvalidTitleExecuteOnSQLStore( $setup ) { + + $this->runOnlyOnSQLStore(); + + $this->setExpectedException( 'Exception' ); + + $result = $this->doApiRequest( array( + 'action' => 'browse', + 'subject' => $setup['subject'] + ) ); + + $this->assertStructuralIntegrity( $setup, $result ); + + } + + /** + * The Serializer enforces a specific output format therefore expected + * elements are verified + * + * @since 1.9 + */ + public function assertStructuralIntegrity( $type, $result ) { + + if ( isset( $type['hasError'] ) && $type['hasError'] ) { + $this->assertInternalType( 'array', $result['error'] ); + } + + if ( isset( $type['hasResult'] ) && $type['hasResult'] ) { + $this->assertInternalType( 'array', $result['query'] ); + } + + if ( isset( $type['hasSubject'] ) && $type['hasSubject'] ) { + $this->assertInternalType( 'string', $result['query']['subject'] ); + } + + if ( isset( $type['hasData'] ) && $type['hasData'] ) { + $this->assertInternalType( 'array', $result['query']['data'] ); + } + + if ( isset( $type['hasSobj'] ) && $type['hasSobj'] ) { + $this->assertInternalType( 'array', $result['query']['sobj'] ); + } + + } + + /** + * @return array + */ + public function subjectDataProvider() { + + $provider = array(); + + // #0 Valid + $provider[] = array( + array( + 'subject' => 'Main_Page', + 'hasSubject' => true, + 'hasResult' => true + ) + ); + + return $provider; + } + + /** + * @return array + */ + public function invalidTitleDataProvider() { + + $provider = array(); + + $provider[] = array( + array( + 'subject' => '{}', + 'hasError' => true, + 'hasResult' => false + ) + ); + + return $provider; + } + +} -- To view, visit https://gerrit.wikimedia.org/r/89147 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I00c2fab386d0c74ef0b576876edb2f873c17bfde Gerrit-PatchSet: 9 Gerrit-Project: mediawiki/extensions/SemanticMediaWiki Gerrit-Branch: master Gerrit-Owner: Mwjames <[email protected]> Gerrit-Reviewer: Jeroen De Dauw <[email protected]> Gerrit-Reviewer: Mwjames <[email protected]> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
