// Retrieve all the values under the MediaWiki config directory
list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->http->run( [
'method' => 'GET',
- 'url' => "{$this->protocol}://{$address}/v2/keys/{$this->directory}/",
+ 'url' => "{$this->protocol}://{$address}/v2/keys/{$this->directory}/?recursive=true",
'headers' => [ 'content-type' => 'application/json' ]
] );
empty( $terminalCodes[$rcode] )
];
}
+ try {
+ return [ $this->parseResponse( $rbody ), null, false ];
+ } catch ( EtcdConfigParseError $e ) {
+ return [ null, $e->getMessage(), false ];
+ }
+ }
+ /**
+ * Parse a response body, throwing EtcdConfigParseError if there is a validation error
+ *
+ * @param string $rbody
+ * @return array
+ */
+ protected function parseResponse( $rbody ) {
$info = json_decode( $rbody, true );
- if ( $info === null || !isset( $info['node']['nodes'] ) ) {
- return [ null, "Unexpected JSON response; missing 'nodes' list.", false ];
+ if ( $info === null ) {
+ throw new EtcdConfigParseError( "Error unserializing JSON response." );
+ }
+ if ( !isset( $info['node'] ) || !is_array( $info['node'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response: Missing or invalid node at top level." );
}
-
$config = [];
- foreach ( $info['node']['nodes'] as $node ) {
+ $this->parseDirectory( '', $info['node'], $config );
+ return $config;
+ }
+
+ /**
+ * Recursively parse a directory node and populate the array passed by
+ * reference, throwing EtcdConfigParseError if there is a validation error
+ *
+ * @param string $dirName The relative directory name
+ * @param array $dirNode The decoded directory node
+ * @param array &$config The output array
+ */
+ protected function parseDirectory( $dirName, $dirNode, &$config ) {
+ if ( !isset( $dirNode['nodes'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
+ }
+ if ( !is_array( $dirNode['nodes'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response in dir '$dirName'; 'nodes' is not an array." );
+ }
+
+ foreach ( $dirNode['nodes'] as $node ) {
+ $baseName = basename( $node['key'] );
+ $fullName = $dirName === '' ? $baseName : "$dirName/$baseName";
if ( !empty( $node['dir'] ) ) {
- continue; // skip directories
- }
+ $this->parseDirectory( $fullName, $node, $config );
+ } else {
+ $value = $this->unserialize( $node['value'] );
+ if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
+ throw new EtcdConfigParseError( "Failed to parse value for '$fullName'." );
+ }
- $name = basename( $node['key'] );
- $value = $this->unserialize( $node['value'] );
- if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
- return [ null, "Failed to parse value for '$name'.", false ];
+ $config[$fullName] = $value['val'];
}
-
- $config[$name] = $value['val'];
}
-
- return [ $config, null, false ];
}
/**
false // retry
],
],
- '200 OK - Skip dir' => [
+ '200 OK - Empty dir' => [
'http' => [
'code' => 200,
'reason' => 'OK',
],
[
'key' => '/example/sub',
- 'dir' => true
+ 'dir' => true,
+ 'nodes' => [],
],
[
'key' => '/example/bar',
false // retry
],
],
+ '200 OK - Recursive' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ 'nodes' => [
+ [
+ 'key' => 'b',
+ 'value' => json_encode( [ 'val' => true ] ),
+ ],
+ [
+ 'key' => 'c',
+ 'value' => json_encode( [ 'val' => false ] ),
+ ],
+ ],
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => [
+ [ 'a/b' => true, 'a/c' => false ], // data
+ null,
+ false // retry
+ ],
+ ],
+ '200 OK - Missing nodes at second level' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => [
+ null,
+ "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
+ false // retry
+ ],
+ ],
+ '200 OK - Correctly encoded garbage response' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'foo' => 'bar' ] ),
+ 'error' => '',
+ ],
+ 'expect' => [
+ null,
+ "Unexpected JSON response: Missing or invalid node at top level.",
+ false // retry
+ ],
+ ],
'200 OK - Bad value' => [
'http' => [
'code' => 200,
],
'expect' => [
null, // data
- "Unexpected JSON response; missing 'nodes' list.",
+ "Error unserializing JSON response.",
false // retry
],
],
/**
* @covers EtcdConfig::fetchAllFromEtcdServer
* @covers EtcdConfig::unserialize
+ * @covers EtcdConfig::parseResponse
+ * @covers EtcdConfig::parseDirectory
+ * @covers EtcdConfigParseError
* @dataProvider provideFetchFromServer
*/
public function testFetchFromServer( array $httpResponse, array $expected ) {