guzzle/guzzle — pull request #398 by Thinkscape (2013-08-14 17:27:10+00:00)
Title: Fix #317 - json array of objects

## Rationale

XML visitor allow for having Models defined as "arrays of objects" - here's an [example from AWS](https://github.com/aws/aws-sdk-php/blob/master/src/Aws/S3/Resources/s3-2006-03-01.php#L3047)

````php
// ...
'models' => array(
// ...
'Errors' => array(
'type' => 'array',
'location' => 'xml',
'sentAs' => 'Error',
'data' => array(
'xmlFlattened' => true,
),
'items' => array(
'type' => 'object',
'sentAs' => 'Error',
'properties' => array(
'Key' => array(
'type' => 'string',
),
'VersionId' => array(
'type' => 'string',
),
//...
),
),
),
),
````

Unfortunately using similar definition with JSON will result in empty (unvisited) models, for example:
````php
// ...
"models" => [
"somemodel" => [
"type" => "array",
"location" => "json",
"items" => [ "type" => "object" ]
]
]
````
Given the following JSON response body:
````
[
{ "id" : 1, "name" : "foo" },
{ "id" : 2, "name" : "bar"}
]
````
... will result in an empty model (no data).

## The fix

I have modified [JsonVisitor](https://github.com/Thinkscape/guzzle/blob/hotfix/317-json-array-of-objects/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php#L34) and added `jsonFlatten` special parameter that will behave similar to `xmlFlatten` used by xml counterpart.

I have also modified [OperationResponseParser](https://github.com/Thinkscape/guzzle/blob/hotfix/317-json-array-of-objects/src/Guzzle/Service/Command/OperationResponseParser.php#L124) to understand `location` set on the topmost element of target model.

## Basic usage

Here is an example usage (which has been incorporated into TestSuite):
````php
$description = ServiceDescription::factory(array(
'operations' => array('test' => array('responseClass' => 'Foo')),
'models' => array(
'Foo' => array(
'type' => 'object',
'properties' => array(
'stuff' => array(
'type' => 'array', // we are expecting an array
'location' => 'json', // look for the array in JSON
'data' => array(
'jsonFlattened' => true // the incoming array is "flat"
),
'items' => array( // the array contains zero
'type' => 'object' // or more objects
)
),
'code' => array('location' => 'statusCode'),
'phrase' => array('location' => 'reasonPhrase'),
)
)
)
));
````
The above definition will be able to read the following Response:
````php
new Response(
200,
array(
'Content-Type' => 'application/json'
),
'[
{ "id" : 1, "name" : "foo" },
{ "id" : 2, "name" : "bar"}
]'
);
````
into a model array of:
````php
array(
'stuff' => array(
array(
'id' => 1,
'name' => 'foo',
),
array(
'id' => 2,
'name' => 'bar',
)
),
'code' => 200,
'phrase' => 'OK'
)
````

## Usage with top-level location definition

In case we just need a single location visited (i.e. we need json data and don't care about headers) we can now specify location directly on the model:

````php
$description = ServiceDescription::factory(array(
'operations' => array('test' => array('responseClass' => 'Foo')),
'models' => array(
'Foo' => array(
'type' => 'array', // the model is just an array
'location' => 'json', // just read the array from JSON
'items' => array(
'type' => 'object' // the array contains objects
)
),
)
));
````
The above definition will be able to read the following JSON:
````json
[
{ "id" : 1, "name" : "foo" },
{ "id" : 2, "name" : "bar"}
]
````
directly into an unnamed array
````php
array(
array(
'baz' => 'bar',
'enigma' => '123',
),
array(
'baz' => 'foo',
'enigma' => '321',
)
)
````