aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrad Jorsch <bjorsch@wikimedia.org>2019-09-11 13:05:47 -0400
committerBrad Jorsch <bjorsch@wikimedia.org>2019-10-17 15:57:54 -0400
commit2a3b546f3b7f7b9a69dd3b2bcef794c18e5a7106 (patch)
treef17f77c751ae170e7ad4c678332b599ef05a0347
parenta2aa77740f4a464a5df2c6a64728f6a9755e2d25 (diff)
downloadmediawikicore-2a3b546f3b7f7b9a69dd3b2bcef794c18e5a7106.tar.gz
mediawikicore-2a3b546f3b7f7b9a69dd3b2bcef794c18e5a7106.zip
libs/Message: Add DataMessageValue
MediaWiki's ApiMessage was a hack, but a useful one. Let's provide that functionality here without it being a hack. Change-Id: Icaf88950ae6e083fc1fb89a3cadbb7f353822eaf
-rw-r--r--includes/libs/Message/DataMessageValue.php90
-rw-r--r--includes/libs/Message/MessageValue.php32
-rw-r--r--includes/libs/Message/README.md24
-rw-r--r--tests/phpunit/includes/libs/Message/DataMessageValueTest.php59
4 files changed, 189 insertions, 16 deletions
diff --git a/includes/libs/Message/DataMessageValue.php b/includes/libs/Message/DataMessageValue.php
new file mode 100644
index 000000000000..686b2ed4bfd9
--- /dev/null
+++ b/includes/libs/Message/DataMessageValue.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * Value object representing a message for i18n with alternative
+ * machine-readable data.
+ *
+ * This augments a MessageValue with an additional machine-readable code and
+ * structured data. The intended use is to facilitate error reporting in APIs.
+ *
+ * For example, a MessageValue reporting an "integer out of range" error might
+ * use one of three message keys, depending on whether there is a minimum, a
+ * maximum, or both. But an API would likely want to use one code for all three
+ * cases, and would likely want the endpoints represented along the lines of
+ * `[ 'min' => 1, 'max' => 10 ]` rather than
+ * `[ 0 => new ScalarParam( ParamType::TEXT, 1 ), 1 => new ScalarParam( ParamType::TEXT, 10 ) ]`.
+ *
+ * DataMessageValues are pure value objects and are safely newable.
+ */
+class DataMessageValue extends MessageValue {
+ /** @var string */
+ private $code;
+
+ /** @var array|null */
+ private $data;
+
+ /**
+ * @param string $key
+ * @param (MessageParam|MessageValue|string|int|float)[] $params
+ * @param string|null $code String representing the concept behind
+ * this message.
+ * @param array|null $data Structured data representing the concept
+ * behind this message.
+ */
+ public function __construct( $key, $params = [], $code = null, array $data = null ) {
+ parent::__construct( $key, $params );
+
+ $this->code = $code ?? $key;
+ $this->data = $data;
+ }
+
+ /**
+ * Static constructor for easier chaining of `->params()` methods
+ * @param string $key
+ * @param (MessageParam|MessageValue|string|int|float)[] $params
+ * @param string|null $code
+ * @param array|null $data
+ * @return DataMessageValue
+ */
+ public static function new( $key, $params = [], $code = null, array $data = null ) {
+ return new DataMessageValue( $key, $params, $code, $data );
+ }
+
+ /**
+ * Get the message code
+ * @return string
+ */
+ public function getCode() {
+ return $this->code;
+ }
+
+ /**
+ * Get the message's structured data
+ * @return array|null
+ */
+ public function getData() {
+ return $this->data;
+ }
+
+ public function dump() {
+ $contents = '';
+ if ( $this->getParams() ) {
+ $contents = '<params>';
+ foreach ( $this->getParams() as $param ) {
+ $contents .= $param->dump();
+ }
+ $contents .= '</params>';
+ }
+
+ if ( $this->data !== null ) {
+ $contents .= '<data>' . htmlspecialchars( json_encode( $this->data ), ENT_NOQUOTES ) . '</data>';
+ }
+
+ return '<datamessage key="' . htmlspecialchars( $this->getKey() ) . '"'
+ . ' code="' . htmlspecialchars( $this->code ) . '">'
+ . $contents
+ . '</datamessage>';
+ }
+}
diff --git a/includes/libs/Message/MessageValue.php b/includes/libs/Message/MessageValue.php
index 73b7c34241ba..78e3c22cb463 100644
--- a/includes/libs/Message/MessageValue.php
+++ b/includes/libs/Message/MessageValue.php
@@ -61,7 +61,7 @@ class MessageValue {
* Chainable mutator which adds text parameters and MessageParam parameters
*
* @param MessageParam|MessageValue|string|int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function params( ...$values ) {
foreach ( $values as $value ) {
@@ -79,7 +79,7 @@ class MessageValue {
*
* @param string $type One of the ParamType constants
* @param MessageValue|string|int|float ...$values Scalar values
- * @return MessageValue
+ * @return $this
*/
public function textParamsOfType( $type, ...$values ) {
foreach ( $values as $value ) {
@@ -94,7 +94,7 @@ class MessageValue {
* @param string $listType One of the ListType constants
* @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
* is an array of items suitable to pass as $params to ListParam::__construct()
- * @return MessageValue
+ * @return $this
*/
public function listParamsOfType( $listType, ...$values ) {
foreach ( $values as $value ) {
@@ -107,7 +107,7 @@ class MessageValue {
* Chainable mutator which adds parameters of type text (ParamType::TEXT).
*
* @param MessageValue|string|int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function textParams( ...$values ) {
return $this->textParamsOfType( ParamType::TEXT, ...$values );
@@ -117,7 +117,7 @@ class MessageValue {
* Chainable mutator which adds numeric parameters (ParamType::NUM).
*
* @param int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function numParams( ...$values ) {
return $this->textParamsOfType( ParamType::NUM, ...$values );
@@ -131,7 +131,7 @@ class MessageValue {
* more verbose.
*
* @param int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function longDurationParams( ...$values ) {
return $this->textParamsOfType( ParamType::DURATION_LONG, ...$values );
@@ -145,7 +145,7 @@ class MessageValue {
* compact.
*
* @param int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function shortDurationParams( ...$values ) {
return $this->textParamsOfType( ParamType::DURATION_SHORT, ...$values );
@@ -156,7 +156,7 @@ class MessageValue {
*
* @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library,
* or "infinity"
- * @return MessageValue
+ * @return $this
*/
public function expiryParams( ...$values ) {
return $this->textParamsOfType( ParamType::EXPIRY, ...$values );
@@ -166,7 +166,7 @@ class MessageValue {
* Chainable mutator which adds parameters which are a number of bytes (ParamType::SIZE).
*
* @param int ...$values
- * @return MessageValue
+ * @return $this
*/
public function sizeParams( ...$values ) {
return $this->textParamsOfType( ParamType::SIZE, ...$values );
@@ -177,7 +177,7 @@ class MessageValue {
* second (ParamType::BITRATE).
*
* @param int|float ...$values
- * @return MessageValue
+ * @return $this
*/
public function bitrateParams( ...$values ) {
return $this->textParamsOfType( ParamType::BITRATE, ...$values );
@@ -191,7 +191,7 @@ class MessageValue {
* documenting what that intended output format is.
*
* @param string ...$values
- * @return MessageValue
+ * @return $this
*/
public function rawParams( ...$values ) {
return $this->textParamsOfType( ParamType::RAW, ...$values );
@@ -205,7 +205,7 @@ class MessageValue {
* so as to be represented as plain text rather than as any sort of markup.
*
* @param string ...$values
- * @return MessageValue
+ * @return $this
*/
public function plaintextParams( ...$values ) {
return $this->textParamsOfType( ParamType::PLAINTEXT, ...$values );
@@ -219,7 +219,7 @@ class MessageValue {
*
* @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
* is an array of items suitable to pass as $params to ListParam::__construct()
- * @return MessageValue
+ * @return $this
*/
public function commaListParams( ...$values ) {
return $this->listParamsOfType( ListType::COMMA, ...$values );
@@ -233,7 +233,7 @@ class MessageValue {
*
* @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
* is an array of items suitable to pass as $params to ListParam::__construct()
- * @return MessageValue
+ * @return $this
*/
public function semicolonListParams( ...$values ) {
return $this->listParamsOfType( ListType::SEMICOLON, ...$values );
@@ -247,7 +247,7 @@ class MessageValue {
*
* @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
* is an array of items suitable to pass as $params to ListParam::__construct()
- * @return MessageValue
+ * @return $this
*/
public function pipeListParams( ...$values ) {
return $this->listParamsOfType( ListType::PIPE, ...$values );
@@ -261,7 +261,7 @@ class MessageValue {
* two elements joined with "and".
*
* @param (MessageParam|string)[] ...$values
- * @return MessageValue
+ * @return $this
*/
public function textListParams( ...$values ) {
return $this->listParamsOfType( ListType::AND, ...$values );
diff --git a/includes/libs/Message/README.md b/includes/libs/Message/README.md
index 9f6255a3f424..c6d1386c968c 100644
--- a/includes/libs/Message/README.md
+++ b/includes/libs/Message/README.md
@@ -74,6 +74,20 @@ has two implementations:
appropriate separators. It has a "list type" (using constants defined in the
**ListType** class) defining the desired separators.
+#### Machine-readable messages
+
+**DataMessageValue** represents a message with additional machine-readable
+data. In addition to the key and message parameters, it holds a "code" and
+structured data that would be a useful representation of the message in an API
+response or the like.
+
+For example, a message for an "integer out of range" error might have one of
+three different keys depending on whether the range has a minimum, maximum, or
+both. But all should have the same code (representing the concept of "integer
+out of range") and should likely have structured data representing the range
+directly as `[ 'min' => 1, 'max' => 10 ]` rather than as a flat array of
+MessageParam objects.
+
### Formatters
A formatter for a particular language is obtained from an implementation of
@@ -279,6 +293,16 @@ Formatter implementations should be able to consume message data supplied in
this format, either directly via registration of i18n directories to check or
by providing tooling to incorporate it during a build step.
+### Machine-readable data
+
+Libraries producing MessageValues as error messages should generally produce
+DataMessageValues instead. Codes should be similar to message keys but need
+not be prefixed. Data should be restricted to values that will produce valid
+output when passed to `json_encode()`.
+
+Libraries producing MessageValues in other contexts should consider whether the
+same applies to those contexts.
+
---
[jQuery.i18n]: https://github.com/wikimedia/jquery.i18n
diff --git a/tests/phpunit/includes/libs/Message/DataMessageValueTest.php b/tests/phpunit/includes/libs/Message/DataMessageValueTest.php
new file mode 100644
index 000000000000..d72032f99352
--- /dev/null
+++ b/tests/phpunit/includes/libs/Message/DataMessageValueTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Wikimedia\Tests\Message;
+
+use Wikimedia\Message\DataMessageValue;
+use Wikimedia\Message\ParamType;
+use Wikimedia\Message\ScalarParam;
+
+/**
+ * @covers \Wikimedia\Message\DataMessageValue
+ */
+class DataMessageValueTest extends \PHPUnit\Framework\TestCase {
+ public static function provideConstruct() {
+ return [
+ [
+ [ 'key' ],
+ '<datamessage key="key" code="key"></datamessage>',
+ ],
+ [
+ [ 'key', [ 'a' ] ],
+ '<datamessage key="key" code="key"><params><text>a</text></params></datamessage>'
+ ],
+ [
+ [ 'key', [], 'code' ],
+ '<datamessage key="key" code="code"></datamessage>'
+ ],
+ [
+ [ 'key', [ new ScalarParam( ParamType::NUM, 1 ) ], 'code', [ 'value' => 1 ] ],
+ '<datamessage key="key" code="code">'
+ . '<params><num>1</num></params>'
+ . '<data>{"value":1}</data>'
+ . '</datamessage>'
+ ],
+ ];
+ }
+
+ /** @dataProvider provideConstruct */
+ public function testConstruct( $args, $expected ) {
+ $mv = new DataMessageValue( ...$args );
+ $this->assertSame( $expected, $mv->dump() );
+ }
+
+ /** @dataProvider provideConstruct */
+ public function testNew( $args, $expected ) {
+ $mv = DataMessageValue::new( ...$args );
+ $this->assertSame( $expected, $mv->dump() );
+ }
+
+ public function testGetCode() {
+ $mv = new DataMessageValue( 'key', [], 'code' );
+ $this->assertSame( 'code', $mv->getCode() );
+ }
+
+ public function testGetData() {
+ $mv = new DataMessageValue( 'key', [], 'code', [ 'data' => 'foobar' ] );
+ $this->assertSame( [ 'data' => 'foobar' ], $mv->getData() );
+ }
+
+}