Implement new "maps" property in Root

Add "maps" as an allowed root value to template data JSON.

Maps allow applications to map equivalencies between their
application-specific parameters and valid template parameters.

Each Map has arbitrary keys (from the consuming application) that
are mapped to one or more template parameters.

* Added root.maps to TemplateDataBlob
  * Validates that values are strings or arrays
  * Validates that values are valid params
* Added templatedata-invalid-param to i18n/en.json
  * Richer error message for when invalid value is a param
* Fixed existing tests to work with new "maps" property
* Added two new tests for maps
  * Test for invalid value (!string or !array)
  * Test for invalid parameter
* Added specification for root.maps property and Map object

Change-Id: I3bf5e002ad6c1632e02c4c2e393b244c51f77177
This commit is contained in:
Marielle Volz 2014-10-18 16:28:26 +01:00 committed by Timo Tijhof
parent e03a12b393
commit d6cac31082
5 changed files with 206 additions and 21 deletions

View file

@ -6,7 +6,7 @@
<dt>This version</dt>
<dd><a href="https://git.wikimedia.org/blob/mediawiki%2Fextensions%2FTemplateData/master/Specification.md">https://git.wikimedia.org/blob/mediawiki%2Fextensions%2FTemplateData/master/Specification.md</a></dd>
<dt>Editors</dt>
<dd>Timo Tijhof, Trevor Parscal, James D. Forrester</dd>
<dd>Timo Tijhof, Trevor Parscal, James D. Forrester, Marielle Volz</dd>
</dl>
***
@ -83,6 +83,20 @@ Authors MUST ensure that the `sets` object contains only `Set` objects. Authors
A Consumer MAY encourage users to interact with parameters in a `Set` together (e.g. add, edit, or remove those parameters at once).
#### 3.1.5 `maps`
* Required
* Value: `Object`
An object describing which parameter(s) specific Consumers SHOULD use for some purpose.
The `maps` property contains keyvalue pairs where the key identifies a given Consumer, and the value a `Map` object.
Consumers are NOT REQUIRED to have a corresponding `Map` object.
Consumers that look for a `Map` SHOULD publicly document their identifier key.
Authors MUST ensure that the `maps` object contains only `Map` objects. Authors MAY include a parameter in multiple `Map` objects. Authors are NOT REQUIRED to reference each parameter in at least one `Map` object.
### 3.2 Param
* Value: `Object`
@ -231,6 +245,13 @@ One of the following:
A free-form string (no wikitext) in the content-language of the wiki, or,
an object containing those strings keyed by language code.
### 3.6 Map
* Value: `Object`
Each key in a `Map` object can be arbitrary. The value must match a parameter name, a list of parameter names, or list containing lists of parameter names.
The key corresponds to the name of a Consumer variable that relates to the specified parameter(s).
## 4 Examples
### 4.1 The "Unsigned" template
@ -276,7 +297,19 @@ an object containing those strings keyed by language code.
"label": "Date",
"params": ["year", "month", "day"]
}
]
],
"maps": {
"ExampleConsumer": {
"foo": "user",
"bar": ["year", "month", "day"],
"quux": [
"date",
["day", "month"],
["month", "year"],
"year"
]
}
}
}
</pre>

View file

@ -34,6 +34,7 @@ class TemplateDataBlob {
* @return TemplateDataBlob
*/
public static function newFromJSON( $json ) {
$tdb = new self( json_decode( $json ) );
$status = $tdb->parse();
@ -46,6 +47,7 @@ class TemplateDataBlob {
$tdb->data->description = null;
$tdb->data->params = new stdClass();
$tdb->data->sets = array();
$tdb->data->maps = new stdClass();
}
$tdb->status = $status;
return $tdb;
@ -79,6 +81,7 @@ class TemplateDataBlob {
'params',
'paramOrder',
'sets',
'maps',
);
static $paramKeys = array(
@ -424,6 +427,77 @@ class TemplateDataBlob {
}
}
// Root.maps
if ( isset( $data->maps ) ) {
if ( !is_object( $data->maps ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'maps', 'object' );
}
} else {
$data->maps = new stdClass();
}
foreach ( $data->maps as $consumerId => $map ) {
if ( !is_object( $map ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'maps', 'object' );
}
foreach ( $map as $key => $value ) {
// Key is not validated as this is used by a third-party application
// Value must be 2d array of parameter names, 1d array of parameter names, or valid
// parameter name
if ( is_array( $value ) ) {
foreach ( $value as $key2 => $value2 ) {
if ( is_array( $value2 ) ) {
foreach ( $value2 as $key3 => $value3 ) {
if ( !is_string( $value3 ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"maps.{$consumerId}.{$key}[$key2][$key3]",
'string'
);
}
if ( !isset( $data->params->$value3 ) ) {
return Status::newFatal(
'templatedata-invalid-param',
$value3,
"maps.{$consumerId}.{$key}"
);
}
}
} elseif ( is_string( $value2 ) ){
if ( !isset( $data->params->$value2 ) ) {
return Status::newFatal(
'templatedata-invalid-param',
$value2,
"maps.{$consumerId}.{$key}"
);
}
} else {
return Status::newFatal(
'templatedata-invalid-type',
"maps.{$consumerId}.{$key}[$key2]",
'string|array'
);
}
}
} elseif ( is_string( $value ) ) {
if ( !isset( $data->params->$value ) ) {
return Status::newFatal(
'templatedata-invalid-param',
$value,
"maps.{$consumerId}.{$key}"
);
}
} else {
return Status::newFatal(
'templatedata-invalid-type',
"maps.{$consumerId}.{$key}",
'string|array'
);
}
}
}
$length = strlen( $this->getJSONForDatabase() );
if ( $length > self::MAX_LENGTH ) {
return Status::newFatal( 'templatedata-invalid-length', $length, self::MAX_LENGTH );

View file

@ -33,6 +33,7 @@
"templatedata-invalid-empty-array": "Property \"$1\" must have at least one value in its array.",
"templatedata-invalid-length": "Data too large to save ({{formatnum:$1}} {{PLURAL:$1|byte|bytes}}, {{PLURAL:$2|limit is}} {{formatnum:$2}})",
"templatedata-invalid-missing": "Required property \"$1\" not found.",
"templatedata-invalid-param": "Invalid parameter \"$1\" for property \"$2\".",
"templatedata-invalid-parse": "Syntax error in JSON.",
"templatedata-invalid-type": "Property \"$1\" is expected to be of type \"$2\".",
"templatedata-invalid-unknown": "Unexpected property \"$1\".",

View file

@ -39,6 +39,7 @@
"templatedata-invalid-empty-array": "Error message when a property that must be non-empty is empty. Parameters:\n* $1 - property name (\"paramOrder\" or \"sets.{$setNr}.params\")",
"templatedata-invalid-length": "Error message when generated JSON's length exceed database limits.\n* $1 - length of generated JSON\n* $2 - maximal allowed length",
"templatedata-invalid-missing": "Error message when a required property is not found.\n* $1 - name of property. e.g. \"params\"\n* $2 - type of property (Unused)",
"templatedata-invalid-param": "Error message for when the supplied parameter is invalid.\n* $1 - name of parameter. e.g. \"username\"\n* $2 - name of property. e.g. maps property \"applicationUser\"",
"templatedata-invalid-parse": "Error message when there is a syntax error in JSON.",
"templatedata-invalid-type": "Error message when a property is of the wrong type.\n* $1 - name of property. e.g. \"params.1.required\"\n* $2 - expected type of property. e.g. \"boolean\"",
"templatedata-invalid-unknown": "Error message when an unknown property is found.\n* $1 - name of property. e.g. \"params.1.foobar\"",

View file

@ -49,7 +49,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": null,
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'status' => true,
@ -100,7 +101,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["foo"],
"sets": []
"sets": [],
"maps": {}
}
',
'msg' => 'Optional properties are added if missing'
@ -130,7 +132,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["comment"],
"sets": []
"sets": [],
"maps": {}
}
',
'msg' => 'Old string/* types are mapped to the unprefixed versions'
@ -174,7 +177,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["nickname"],
"sets": []
"sets": [],
"maps": {}
}
',
'msg' => 'InterfaceText is expanded to langcode-keyed object, assuming content language'
@ -228,7 +232,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["1d", "2d"],
"sets": []
"sets": [],
"maps" : {}
}
',
'msg' => 'The inherits property copies over properties from another parameter '
@ -348,7 +353,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
},
"params": ["bar", "quux"]
}
]
],
"maps": {}
}',
'status' => true
),
@ -392,18 +398,78 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["bar"],
"sets": []
"sets": [],
"maps" : {}
}
',
'msg' => 'Parameter attributes preserve information.'
),
array(
'input' => '{
"params": {
"foo": {
},
"bar": {
}
},
"sets": [],
"maps": {
"application": {
"things": [
"foo",
["bar", "quux"]
]
}
}
}',
'status' => 'Invalid parameter "quux" for property "maps.application.things".'
),
array(
'input' => '{
"params": {
"foo": {
},
"bar": {
}
},
"sets": [],
"maps": {
"application": {
"things": {
"appbar": "bar",
"appfoo": "foo"
}
}
}
}',
'status' => 'Property "maps.application.things" is expected to be of type "string|array".'
),
array(
'input' => '{
"params": {
"foo": {
},
"bar": {
}
},
"sets": [],
"maps": {
"application": {
"things": [
[ true ]
]
}
}
}',
'status' => 'Property "maps.application.things[0][0]" is expected to be of type "string".'
),
array(
// Should be long enough to trigger this condition after gzipping.
'input' => '{
"description": "' . self::generatePseudorandomString( 100000, 42 ) . '",
"params": {}
}',
'status' => 'Data too large to save (75,195 bytes, limit is 65,535)'
'status' => 'Data too large to save (75,204 bytes, limit is 65,535)'
),
);
$calls = array();
@ -471,7 +537,7 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
if ( !isset( $case['output'] ) ) {
if ( is_string( $case['status'] ) ) {
$case['output'] = '{ "description": null, "params": {}, "sets": [] }';
$case['output'] = '{ "description": null, "params": {}, "sets": [], "maps": {} }';
} else {
$case['output'] = $case['input'];
}
@ -560,7 +626,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": "German",
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'de',
@ -575,7 +642,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": "Hi",
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'fr',
@ -594,7 +662,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": "Dutch",
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'fr',
@ -612,7 +681,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": null,
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'fr',
@ -631,7 +701,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
'output' => '{
"description": "German",
"params": {},
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'de-formal',
@ -665,7 +736,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["foo"],
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'fr',
@ -699,7 +771,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["foo"],
"sets": []
"sets": [],
"maps" : {}
}
',
'lang' => 'fr',
@ -742,7 +815,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
"label": "Spanish",
"params": ["foo"]
}
]
],
"maps": {}
}
',
'lang' => 'fr',
@ -837,7 +911,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["foo", "bar", "baz"],
"sets": []
"sets": [],
"maps" : {}
}
',
'msg' => 'Normalisation adds paramOrder'
@ -890,7 +965,8 @@ class TemplateDataBlobTest extends MediaWikiTestCase {
}
},
"paramOrder": ["baz", "foo", "bar"],
"sets": []
"sets": [],
"maps" : {}
}
',
'msg' => 'Custom paramOrder'