registration: Version the extension.json schema
authorKunal Mehta <legoktm@gmail.com>
Wed, 20 May 2015 01:45:10 +0000 (18:45 -0700)
committerOri.livneh <ori@wikimedia.org>
Thu, 28 May 2015 22:20:59 +0000 (22:20 +0000)
Versioning the extension.json schema will allow us to make breaking changes
to the schema in a non-breaking manner.

Extensiosn and skins will set a 'manifest_version' value, stating which
version of the schema the file is written for. Processor::extractInfo() will
be  passed the version number, and can switch behavior depending upon it.

For backwards-compatability, a version number of 1 is assumed if none is set.
The validateRegistrationFile.php script will emit a warning if this is the
case.

Bug: T99344
Change-Id: I2086a1465ceaeedd1ccc6804fda2c304ad16ffa0

docs/extension.schema.json
includes/registration/ExtensionProcessor.php
includes/registration/ExtensionRegistry.php
includes/registration/Processor.php
maintenance/convertExtensionToRegistration.php
maintenance/validateRegistrationFile.php
tests/phpunit/includes/registration/ExtensionProcessorTest.php

index d5c17a1..34cfe2c 100644 (file)
@@ -3,6 +3,11 @@
        "description": "MediaWiki extension.json schema",
        "type": "object",
        "properties": {
+               "manifest_version": {
+                       "type": "integer",
+                       "description": "Version of the extension.json schema the extension.json file is in.",
+                       "required": true
+               },
                "name": {
                        "type": "string",
                        "description": "The extension's canonical name.",
index b0398eb..0a09ff5 100644 (file)
@@ -81,6 +81,7 @@ class ExtensionProcessor implements Processor {
                'config',
                'ParserTestFiles',
                'AutoloadClasses',
+               'manifest_version',
        );
 
        /**
@@ -125,9 +126,10 @@ class ExtensionProcessor implements Processor {
        /**
         * @param string $path
         * @param array $info
+        * @param int $version manifest_version for info
         * @return array
         */
-       public function extractInfo( $path, array $info ) {
+       public function extractInfo( $path, array $info, $version ) {
                $this->extractConfig( $info );
                $this->extractHooks( $info );
                $dir = dirname( $path );
index ac39699..1c36407 100644 (file)
  */
 class ExtensionRegistry {
 
+       /**
+        * Version of the highest supported manifest version
+        */
+       const MANIFEST_VERSION = 1;
+
+       /**
+        * Version of the oldest supported manifest version
+        */
+       const OLDEST_MANIFEST_VERSION = 1;
+
        /**
         * @var BagOStuff
         */
@@ -128,11 +138,19 @@ class ExtensionRegistry {
                        if ( !is_array( $info ) ) {
                                throw new Exception( "$path is not a valid JSON file." );
                        }
+                       if ( !isset( $info['manifest_version' ] ) ) {
+                               // For backwards-compatability, assume a version of 1
+                               $info['manifest_version'] = 1;
+                       }
+                       $version = $info['manifest_version'];
+                       if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
+                               throw new Exception( "$path: unsupported manifest_version: {$version}" );
+                       }
                        $autoload = $this->processAutoLoader( dirname( $path ), $info );
                        // Set up the autoloader now so custom processors will work
                        $GLOBALS['wgAutoloadClasses'] += $autoload;
                        $autoloadClasses += $autoload;
-                       $processor->extractInfo( $path, $info );
+                       $processor->extractInfo( $path, $info, $version );
                }
                $data = $processor->getExtractedInfo();
                // Need to set this so we can += to it later
index 391f108..e1aaca7 100644 (file)
@@ -16,9 +16,10 @@ interface Processor {
         *
         * @param string $path Absolute path of JSON file
         * @param array $info
+        * @param int $version manifest_version for info
         * @return array "credits" information to store
         */
-       public function extractInfo( $path, array $info );
+       public function extractInfo( $path, array $info, $version );
 
        /**
         * @return array With following keys:
index e0631a7..8adae2d 100644 (file)
@@ -118,7 +118,8 @@ class ConvertExtensionToRegistration extends Maintenance {
                        }
                }
                $out += $this->json;
-
+               // Put this at the bottom
+               $out['manifest_version'] = ExtensionRegistry::MANIFEST_VERSION;
                $type = $this->hasOption( 'skin' ) ? 'skin' : 'extension';
                $fname = "{$this->dir}/$type.json";
                $prettyJSON = FormatJson::encode( $out, "\t", FormatJson::ALL_OK );
index e764661..08af11a 100644 (file)
@@ -12,18 +12,39 @@ class ValidateRegistrationFile extends Maintenance {
                        $this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
                }
 
-               $retriever = new JsonSchema\Uri\UriRetriever();
-               $schema = $retriever->retrieve('file://' . dirname( __DIR__ ) . '/docs/extension.schema.json' );
                $path = $this->getArg( 0 );
                $data = json_decode( file_get_contents( $path ) );
                if ( !is_object( $data ) ) {
                        $this->error( "$path is not a valid JSON file.", 1 );
                }
+               if ( !isset( $data->manifest_version ) ) {
+                       $this->output("Warning: No manifest_version set, assuming 1.\n" );
+                       // For backwards-compatability assume 1
+                       $data->manifest_version = 1;
+               }
+               $version = $data->manifest_version;
+               if ( $version !== ExtensionRegistry::MANIFEST_VERSION ) {
+                       $schemaPath = dirname( __DIR__ ) . "/docs/extension.schema.v$version.json";
+               } else {
+                       $schemaPath = dirname( __DIR__ ) . '/docs/extension.schema.json';
+               }
+
+               if ( $version < ExtensionRegistry::OLDEST_MANIFEST_VERSION
+                       || $version > ExtensionRegistry::MANIFEST_VERSION
+               ) {
+                       $this->error( "Error: $path is using a non-supported schema version, it should use "
+                               . ExtensionRegistry::MANIFEST_VERSION, 1 );
+               } elseif ( $version < ExtensionRegistry::MANIFEST_VERSION ) {
+                       $this->output( "Warning: $path is using a deprecated schema, and should be updated to "
+                               . ExtensionRegistry::MANIFEST_VERSION . "\n" );
+               }
+               $retriever = new JsonSchema\Uri\UriRetriever();
+               $schema = $retriever->retrieve('file://' . $schemaPath );
 
                $validator = new JsonSchema\Validator();
                $validator->check( $data, $schema );
                if ( $validator->isValid() ) {
-                       $this->output( "$path validates against the schema!\n" );
+                       $this->output( "$path validates against the version $version schema!\n" );
                } else {
                        foreach ( $validator->getErrors() as $error ) {
                                $this->output( "[{$error['property']}] {$error['message']}\n" );
index 9474496..2df5568 100644 (file)
@@ -28,7 +28,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
                        '@metadata' => array( 'foobarbaz' ),
                        'AnAttribute' => array( 'omg' ),
                        'AutoloadClasses' => array( 'FooBar' => 'includes/FooBar.php' ),
-               ) );
+               ), 1 );
 
                $extracted = $processor->getExtractedInfo();
                $attributes = $extracted['attributes'];
@@ -95,7 +95,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
         */
        public function testRegisterHooks( $pre, $info, $expected ) {
                $processor = new MockExtensionProcessor( array( 'wgHooks' => $pre ) );
-               $processor->extractInfo( $this->dir, $info );
+               $processor->extractInfo( $this->dir, $info, 1 );
                $extracted = $processor->getExtractedInfo();
                $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
        }
@@ -112,7 +112,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
                                '@IGNORED' => 'yes',
                        ),
                ) + self::$default;
-               $processor->extractInfo( $this->dir, $info );
+               $processor->extractInfo( $this->dir, $info, 1 );
                $extracted = $processor->getExtractedInfo();
                $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
                $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
@@ -149,7 +149,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
         */
        public function testExtracttExtensionMessagesFiles( $input, $expected ) {
                $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default );
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
                $out = $processor->getExtractedInfo();
                foreach ( $expected as $key => $value ) {
                        $this->assertEquals( $value, $out['globals'][$key] );
@@ -177,7 +177,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
         */
        public function testExtractMessagesDirs( $input, $expected ) {
                $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default );
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
                $out = $processor->getExtractedInfo();
                foreach ( $expected as $key => $value ) {
                        $this->assertEquals( $value, $out['globals'][$key] );
@@ -190,7 +190,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
         */
        public function testExtractResourceLoaderModules( $input, $expected ) {
                $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default );
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
                $out = $processor->getExtractedInfo();
                foreach ( $expected as $key => $value ) {
                        $this->assertEquals( $value, $out['globals'][$key] );