Skip to content

Commit

Permalink
Fixes an issue related to content encoding/decoding & content negotia…
Browse files Browse the repository at this point in the history
…tion..
  • Loading branch information
jails committed Jan 11, 2016
1 parent 0eb68d2 commit d2a9b01
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 104 deletions.
61 changes: 61 additions & 0 deletions spec/Suite/Http/FormatSpec.php
@@ -1,6 +1,7 @@
<?php
namespace Lead\Net\Spec\Suite\Http;

use Lead\Collection\Collection;
use Lead\Net\Http\Format;
use Lead\Net\Http\Response;

Expand Down Expand Up @@ -70,4 +71,64 @@

});

describe("::encode()", function() {

it("encodes in json", function() {

$json = Format::encode('json', ['key' => 'value']);

expect($json)->toBe('{"key":"value"}');

});

it("encodes objects in json", function() {

$json = Format::encode('json', new Collection(['key' => 'value']));

expect($json)->toBe('{"key":"value"}');

});

it("encodes in form data", function() {

$json = Format::encode('form', ['key1' => 'value1', 'key2' => 'value2']);

expect($json)->toBe('key1=value1&key2=value2');

});

});

describe("::decode()", function() {

it("decodes json", function() {

$data = Format::decode('json', '{"key":"value"}');

expect($data)->toBe(['key' => 'value']);

});

it("decodes form data", function() {

$data = Format::decode('form', 'key1=value1&key2=value2');

expect($data)->toBe(['key1' => 'value1', 'key2' => 'value2']);

});

});

describe("::to()", function() {

it("delegates to `::encode()`", function() {

expect(Format::class)->toReceive('::encode')->with('json', '', ['key' => 'value']);

Format::to('json', '', ['key' => 'value']);

});

});

});
37 changes: 28 additions & 9 deletions spec/Suite/Http/ResponseSpec.php
Expand Up @@ -2,6 +2,7 @@
namespace Lead\Net\Spec\Suite\Http;

use Lead\Net\NetException;
use Lead\Net\Http\Request;
use Lead\Net\Http\Response;

use Kahlan\Plugin\Monkey;
Expand Down Expand Up @@ -103,6 +104,7 @@
$response->cache(false);

$expected = <<<EOD
Content-Type: text/html\r
Expires: Mon, 26 Jul 1997 05:00:00 GMT\r
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0\r
Pragma: no-cache\r
Expand All @@ -124,6 +126,7 @@
$response->cache(1451001600);

$expected = <<<EOD
Content-Type: text/html\r
Expires: Fri, 25 Dec 2015 00:00:00 GMT\r
Cache-Control: max-age=600\r
Pragma: no-cache\r
Expand Down Expand Up @@ -175,14 +178,34 @@


$expected =<<<EOD
Content-Type: text/html\r
Set-Cookie: foo1=bar1; Path=/\r
Set-Cookie: foo2=bar2; Path=/\r
Set-Cookie: foo3=bar3; Path=/\r
\r
EOD;

expect((string) $response->headers)->toBe($expected);
expect($response->headers->to('header'))->toBe($expected);

});

});

describe("->negociate()", function() {

it("negociates a format from a request", function() {

$request = new Request();
$response = new Response();

$request->headers['Accept'] = "text/html;q=0.2,application/json,application/xml;q=0.9,*/*;q=0.8";
$response->negotiate($request);
expect($response->format())->toBe('json');

$request->headers['Accept'] = "text/html,application/json;q=0.2,application/xml;q=0.9,*/*;q=0.8";
$response->negotiate($request);
expect($response->format())->toBe('html');

});

Expand All @@ -193,10 +216,8 @@
it("casts the response as a string", function() {

$response = new Response([
'headers' => [
'Content-Type: application/json'
],
'body' => ['hello' => 'world']
'format' => 'json',
'body' => ['hello' => 'world']
]);
$cookies = $response->headers->cookies;

Expand Down Expand Up @@ -225,10 +246,8 @@
it("casts the response as a string", function() {

$response = new Response([
'headers' => [
'Content-Type: application/json'
],
'body' => ['hello' => 'world']
'format' => 'json',
'body' => ['hello' => 'world']
]);
$cookies = $response->headers->cookies;

Expand Down
105 changes: 29 additions & 76 deletions src/Http/Format.php
Expand Up @@ -53,6 +53,7 @@ public static function set($format, $definition = [])
}

$definition += [
'cast' => true,
'type' => ['text/html'],
'decode' => null,
'encode' => null,
Expand Down Expand Up @@ -119,83 +120,38 @@ public static function to($format, $data, $options = [])
}

/**
* Performs Content-Type negotiation on a `Request` object, by iterating over the accepted
* types in sequence, from most preferred to least, and attempting to match a format
* defined by `Format::set()`.
* Iterates through all existing formats to match a compatible one for the provided request.
*
* @param object $request A request instance .
* @return string Returns the first matching format, i.e. `'html'` or `'json'`.
*/
public static function negotiate($request)
{
foreach ($request->accepts() as $type) {
if ($format = static::suitable($type, $request)) {
return $format;
}
}
return static::suitable($request->type(), $request);
}

/**
* Iterates through all existing format to match the one compatible to the provided content type and request.
*
* @param string $type A content type.
* @param object $request An instance of request.
* @param string $type An overriding content type.
* @return boolean Returns a compatible format name or `null` if none matched.
*/
public static function suitable($type, $request)
public static function suitable($request, $type = null)
{
$formats = static::$_formats;

if (func_num_args() === 1) {
$type = $request->type();
}

foreach ($formats as $format => $definition) {
if (!static::_match($type, $definition, $request)) {
if (!in_array($type, $definition['type'], true)) {
continue;
}
return $format;
}
}

/**
* Checks if a request is matchable with specific format.
*
* @param string $format The format to match.
* @param object $request An instance of request.
* @return boolean Returns `true` if the request matches the format, `false` otherwise.
*/
public static function match($format, $request)
{
if (!$definition = static::get($format)) {
return false;
}
return static::_match($request->type(), $definition, $request);
}

/**
* Helper for `suitable()` && `match`.
*
* @param string $type A content type.
* @param string $definition A format definition.
* @param object $request An instance of request.
* @return boolean Returns `true` if the request matches the format definition, `false` otherwise.
*/
public static function _match($type, $definition, $request)
{
if (!in_array($type, $definition['type'], true)) {
return false;
}
foreach ($definition['conditions'] as $key => $value) {
switch (true) {
case strpos($key, ':'):
if ($request->get($key) !== $value) {
return false;
}
break;
case ($request->is($key) !== $value):
return false;
break;
foreach ($definition['conditions'] as $key => $value) {
switch (true) {
case strpos($key, ':'):
if ($request->get($key) !== $value) {
continue 2;
}
break;
case ($request->is($key) !== $value):
continue 2;
break;
}
}
return $format;
}
return true;
}

/**
Expand All @@ -204,24 +160,24 @@ public static function _match($type, $definition, $request)
* @param string $format The media format into which `$data` will be encoded.
* @param mixed $data Arbitrary data you wish to encode.
* @param array $options Handler-specific options.
* @return mixed The encoded data.
* @return string The encoded string data.
*/
public static function encode($format, $data, $options = [])
{
$definition = static::get($format);

if (empty($definition['encode'])) {
return $data;
if (is_string($data)) {
return $data;
}
throw new NetException("The `$format` format requires data needs to be a string.");
}

$cast = function($data) {
if (!is_object($data)) {
return $data;
}
return method_exists($data, 'to') ? $data->to('array') : get_object_vars($data);
};

if (!empty($handler['cast'])) {
if (!empty($definition['cast'])) {
$data = is_object($data) ? $cast($data) : $data;
}

Expand All @@ -233,9 +189,9 @@ public static function encode($format, $data, $options = [])
* Decodes data according to the specified media format.
*
* @param string $format The media format into which `$data` will be decoded.
* @param mixed $data Arbitrary data you wish to decode.
* @param string $data String data to decode.
* @param array $options Handler-specific options.
* @return mixed The decoded data.
* @return mixed The arbitrary decoded data.
*/
public static function decode($format, $data, $options = [])
{
Expand Down Expand Up @@ -271,9 +227,6 @@ public static function reset()
'type' => ['application/json', 'application/x-json'],
'encode' => 'json_encode',
'decode' => function($data) {
if ($data === '') {
return '""';
}
return json_decode($data, true);
}
]);
Expand Down
22 changes: 8 additions & 14 deletions src/Http/Message.php
Expand Up @@ -56,6 +56,7 @@ public function __construct($config = [])
'version' => '1.1',
'type' => null,
'encoding' => null,
'format' => null,
'headers' => [],
'classes' => [
'auth' => 'Lead\Net\Http\Auth',
Expand All @@ -70,7 +71,6 @@ public function __construct($config = [])

$this->version($config['version']);


if (is_object($config['headers'])) {
$this->headers = $config['headers'];
} else {
Expand All @@ -87,6 +87,8 @@ public function __construct($config = [])
$this->encoding($config['encoding']);
}

$this->format($config['format']);

parent::__construct($config);
}

Expand Down Expand Up @@ -154,27 +156,16 @@ public function type($type = null)
*/
public function format($format = null)
{
if (!func_num_args()) {
if (!$this->_format) {
$format = $this->_classes['format'];
$this->format($format::suitable($this->type(), $this));
}
return $this->_format;
}
if (!$format) {
return;
return $this->_format;
}

$formatter = $this->_classes['format'];
if (!$type = $formatter::type($format)){
throw new NetException("The `'$format'` format is undefiened or has no valid Content-Type defined.");
}
$this->type($type);

if (!$formatter::match($format, $this)) {
throw new NetException("The request is not compatible with `'$format'` requirements, can't set the format to `'$format'`.");
}
$this->_format = $format;
$this->type($type);

return $this;
}
Expand Down Expand Up @@ -216,6 +207,9 @@ public function body($value = null)
$format = $this->format();

if (func_num_args() === 1) {
if (!$format && !is_string($value)) {
throw new NetException("The data must be a string when no format is defined.");
}
$this->stream($format ? $formatter::encode($format, $value) : $value);
return $this;
}
Expand Down

0 comments on commit d2a9b01

Please sign in to comment.