Better accept header handling and lots more test coverage
This commit is contained in:
parent
56d6ac12c8
commit
5e3f011cfa
@ -23,44 +23,40 @@ class Config {
|
|||||||
protected $data = [];
|
protected $data = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the specified config file and return
|
* Load the data into the class member
|
||||||
* the config array
|
|
||||||
*
|
|
||||||
* @throws \InvalidArugmentException
|
|
||||||
* @param string $name
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function load($name)
|
public function __construct()
|
||||||
{
|
{
|
||||||
$file = APPPATH . 'config/'. $name . '.php';
|
$conf_files = glob(APPPATH . 'config/*.php');
|
||||||
|
|
||||||
if (is_file($file))
|
foreach($conf_files as $file)
|
||||||
{
|
{
|
||||||
$conf =require_once($file);
|
$data = require_once($file);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("The config file doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->data[$name] = $conf;
|
$name = str_replace('.php', '', basename($file));
|
||||||
|
$this->data[$name] = $data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the specific parameter from the specified file
|
* Get the specific parameter from the specified file
|
||||||
*
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
* @param string $file
|
* @param string $file
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function get($file, $key=NULL)
|
public function get($file, $key=NULL)
|
||||||
{
|
{
|
||||||
if (is_null($key))
|
if ( ! array_key_exists($file, $this->data))
|
||||||
{
|
{
|
||||||
return $this->data[$file];
|
throw new \InvalidArgumentException("The config file doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->data[$file][$key];
|
return (is_null($key))
|
||||||
|
? $this->data[$file]
|
||||||
|
: $this->data[$file][$key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// End of Core/Config.php
|
// End of Core/Config.php
|
@ -306,26 +306,25 @@ class Input {
|
|||||||
protected function parse_accept_header($value)
|
protected function parse_accept_header($value)
|
||||||
{
|
{
|
||||||
$q_types = [];
|
$q_types = [];
|
||||||
// A fake value so I can shift it off to have a 1-indexed array
|
|
||||||
$high_types = [];
|
$high_types = [];
|
||||||
$count = 1;
|
$count = 1;
|
||||||
|
|
||||||
// Split into segments of different values
|
// Split into segments of different values
|
||||||
$groups = explode(',', $value);
|
$groups = \explode(',', $value);
|
||||||
|
|
||||||
foreach($groups as $group)
|
foreach($groups as $group)
|
||||||
{
|
{
|
||||||
$group = \trim($group);
|
$group = \trim($group);
|
||||||
$pair = explode(';q=', $group);
|
$pair = \explode(';q=', $group);
|
||||||
|
|
||||||
if (count($pair) === 2)
|
if (\count($pair) === 2)
|
||||||
{
|
{
|
||||||
list($val, $q) = $pair;
|
list($val, $q) = $pair;
|
||||||
$q_types[$q] = $val;
|
$q_types[$q] = $val;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$high_types[$count] = current($pair);
|
$high_types[$count] = \current($pair);
|
||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,7 +332,7 @@ class Input {
|
|||||||
// Add an additional fake value so we can
|
// Add an additional fake value so we can
|
||||||
// have a 1-indexed array
|
// have a 1-indexed array
|
||||||
$high_types[$count] = 'foo';
|
$high_types[$count] = 'foo';
|
||||||
$high_types = array_reverse($high_types);
|
$high_types = \array_reverse($high_types);
|
||||||
unset($high_types[0]);
|
unset($high_types[0]);
|
||||||
|
|
||||||
$output = $q_types;
|
$output = $q_types;
|
||||||
@ -345,7 +344,7 @@ class Input {
|
|||||||
$output[$k] = $v;
|
$output[$k] = $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
krsort($output, SORT_NUMERIC);
|
\krsort($output, SORT_NUMERIC);
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
@ -367,16 +366,17 @@ class Input {
|
|||||||
// Return the whole array if the index is null
|
// Return the whole array if the index is null
|
||||||
if ($index === NULL)
|
if ($index === NULL)
|
||||||
{
|
{
|
||||||
return ($filter !== NULL) ? \filter_var_array($var, $filter) : $var;
|
return ($filter !== NULL)
|
||||||
|
? \filter_var_array($var, $filter)
|
||||||
|
: $var;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent errors for non-existant variables
|
// Prevent errors for non-existant variables
|
||||||
if ( ! isset($var[$index]))
|
if ( ! isset($var[$index])) return NULL;
|
||||||
{
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ($filter !== NULL) ? \filter_var($var[$index], $filter) : $var[$index];
|
return ($filter !== NULL)
|
||||||
|
? \filter_var($var[$index], $filter)
|
||||||
|
: $var[$index];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@ class Output {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Output the data to the client
|
* Output the data to the client
|
||||||
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
@ -89,7 +90,12 @@ class Output {
|
|||||||
{
|
{
|
||||||
if (is_array($header))
|
if (is_array($header))
|
||||||
{
|
{
|
||||||
array_merge($this->headers, $header);
|
foreach($header as $type => $val)
|
||||||
|
{
|
||||||
|
$this->headers[] = (is_numeric($type))
|
||||||
|
? $val
|
||||||
|
: "{$type}: {$val}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -100,7 +106,7 @@ class Output {
|
|||||||
/**
|
/**
|
||||||
* Set the data to be output to the endpoint
|
* Set the data to be output to the endpoint
|
||||||
*
|
*
|
||||||
* @param string $type - The data format to send
|
* @param mixed $type - The data format to send
|
||||||
* @param mixed $data - The data to send
|
* @param mixed $data - The data to send
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@ -116,7 +122,7 @@ class Output {
|
|||||||
|
|
||||||
// Get the appropriate output format for the client
|
// Get the appropriate output format for the client
|
||||||
// And set the data
|
// And set the data
|
||||||
$this->get_accepted_type($type, $this->data);
|
return $this->get_accepted_type($type, $this->data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@ -128,85 +134,92 @@ class Output {
|
|||||||
*
|
*
|
||||||
* @param mixed $types
|
* @param mixed $types
|
||||||
* @param mixed $data
|
* @param mixed $data
|
||||||
* @return void
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function get_accepted_type($types, $data)
|
protected function get_accepted_type($types, $data)
|
||||||
{
|
{
|
||||||
|
// Get the mime - type class mapping to use
|
||||||
|
// for determining which classes need to be loaded
|
||||||
$types = (array) $types;
|
$types = (array) $types;
|
||||||
$types = array_map('strtoupper', $types);
|
$types = array_map('strtoupper', $types);
|
||||||
|
$type_map = $this->config->get('type_class_map');
|
||||||
|
$filtered_type_map = $this->filter_mime_types($type_map, $types);
|
||||||
|
|
||||||
|
// Get the accept headers to filter valid mime types
|
||||||
|
// for the current client
|
||||||
$headers = $this->input->header_array();
|
$headers = $this->input->header_array();
|
||||||
$accept = array_flip($headers['accept']);
|
$accept = array_flip($headers['accept']);
|
||||||
|
$valid_mimes = array_keys(array_intersect_key($accept, $filtered_type_map));
|
||||||
|
|
||||||
$type_map = [];
|
// When you don't have a matching mime, send the default
|
||||||
$accepted = [];
|
// data type specified for the output in the type_class_map
|
||||||
|
// config file
|
||||||
|
if (empty($valid_mimes))
|
||||||
|
{
|
||||||
|
$valid_mimes[] = $type_map['*/*'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map type objects to the appropriate
|
||||||
|
// associated mime types
|
||||||
$classes = [];
|
$classes = [];
|
||||||
|
foreach($valid_mimes as $mime)
|
||||||
foreach($types as $t)
|
|
||||||
{
|
{
|
||||||
|
$t = $type_map[$mime];
|
||||||
$type_class = "Sleepy\\Type\\{$t}";
|
$type_class = "Sleepy\\Type\\{$t}";
|
||||||
$classes[$type_class] = new $type_class($data);
|
$classes[$mime] = $type_class;
|
||||||
$mime = $classes[$type_class]->get_mime();
|
|
||||||
|
|
||||||
$type_map[$mime] = $type_class;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order type preference first by
|
|
||||||
// input, then by accept flags
|
|
||||||
foreach($type_map as $type => $obj)
|
|
||||||
{
|
|
||||||
if (\array_key_exists($type, $accept))
|
|
||||||
{
|
|
||||||
$q = $accept[$type];
|
|
||||||
|
|
||||||
if ($q >= 1)
|
|
||||||
{
|
|
||||||
$accepted[] = $type;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$accepted[$q] = $type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to html fow wildcard accept values
|
|
||||||
if (empty($accepted) && \array_key_exists('*/*', $accept))
|
|
||||||
{
|
|
||||||
$accepted[1] = 'text/html';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first output type to output the data
|
// Use the first output type to output the data
|
||||||
$class = $type_map[current($accepted)];
|
$selected_mime = array_shift($valid_mimes);
|
||||||
$this->type_wrapper = $classes[$class];
|
$class = $classes[$selected_mime];
|
||||||
|
$this->type_wrapper = new $class($data);
|
||||||
|
|
||||||
// Make sure to set the content-type header
|
// Make sure to set the content-type header
|
||||||
if (empty($this->headers))
|
if (empty($this->headers))
|
||||||
{
|
{
|
||||||
$mime = $this->type_wrapper->get_mime();
|
$mime = $this->type_wrapper->get_mime();
|
||||||
$this->set_header("Content-type: {$mime}");
|
$this->set_header("Content-type: {$mime};charset=utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [$selected_mime => $class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the list of mime types by their values
|
||||||
|
*
|
||||||
|
* @param array $mime_list
|
||||||
|
* @param array $type_list
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function filter_mime_types($mime_list, $type_list)
|
||||||
|
{
|
||||||
|
$filtered_list = [];
|
||||||
|
|
||||||
|
foreach($type_list as $class)
|
||||||
|
{
|
||||||
|
foreach($mime_list as $mime => $c)
|
||||||
|
{
|
||||||
|
if (strtoupper($c) === strtoupper($class))
|
||||||
|
{
|
||||||
|
$filtered_list[$mime] = $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filtered_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the applicable response headers
|
* Set the applicable response headers
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function _output_headers()
|
protected function _output_headers()
|
||||||
{
|
{
|
||||||
foreach($this->headers as $name => $val)
|
foreach($this->headers as $header)
|
||||||
{
|
{
|
||||||
if (is_numeric($name))
|
@header($header);
|
||||||
{
|
|
||||||
$output_header = $val;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$output_header = implode(": ", [$name, $val]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@header($output_header);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ $di->set('output', function () use ($di) {
|
|||||||
$browser = get_browser();
|
$browser = get_browser();
|
||||||
$browser->browser_name_regex = \utf8_encode($browser->browser_name_regex);
|
$browser->browser_name_regex = \utf8_encode($browser->browser_name_regex);
|
||||||
$i = $di->get('input');
|
$i = $di->get('input');
|
||||||
$di->get('output')->set_data(['json','yaml','xml','html'], [
|
$di->get('output')->set_data(['json','yaml'], [
|
||||||
'$_SERVER' => $i->server(),
|
'$_SERVER' => $i->server(),
|
||||||
'$_GET' => $i->get(),
|
'$_GET' => $i->get(),
|
||||||
'$_POST' => $i->post(),
|
'$_POST' => $i->post(),
|
||||||
|
@ -19,8 +19,11 @@ return [
|
|||||||
'text/yaml' => 'YAML',
|
'text/yaml' => 'YAML',
|
||||||
'application/yaml' => 'YAML',
|
'application/yaml' => 'YAML',
|
||||||
'text/html' => 'HTML',
|
'text/html' => 'HTML',
|
||||||
'application/xhtml+xml' => 'HTML',
|
'text/xml' => 'XML',
|
||||||
'*/*' => 'HTML'
|
'application/xml' => 'XML',
|
||||||
|
|
||||||
|
// Default mime type for cases without an explicit accept header match
|
||||||
|
'*/*' => 'text/html'
|
||||||
];
|
];
|
||||||
|
|
||||||
// End of config/type_class_map.php
|
// End of config/type_class_map.php
|
@ -24,5 +24,4 @@ require BASEPATH . SLEEPY_DIR . '/autoload.php';
|
|||||||
// And...bootstrap
|
// And...bootstrap
|
||||||
require APPPATH . 'bootstrap.php';
|
require APPPATH . 'bootstrap.php';
|
||||||
|
|
||||||
|
|
||||||
// End of index.php
|
// End of index.php
|
@ -21,3 +21,25 @@ class Sleepy_TestCase extends PHPUnit_Framework_TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// ! Simple Base Mocks
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
use Sleepy\Core\Output;
|
||||||
|
use Sleepy\Core\Config;
|
||||||
|
|
||||||
|
class MockOutput extends Output {
|
||||||
|
use Sleepy\Traits\getSet;
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->_output_headers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockConfig extends Config {
|
||||||
|
use Sleepy\Traits\getSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of tests/Bootstrap.php
|
60
tests/Core/ConfigTest.php
Normal file
60
tests/Core/ConfigTest.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Sleepy\Core\Config;
|
||||||
|
|
||||||
|
class ConfigTest extends Sleepy_Testcase {
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->config = new MockConfig();
|
||||||
|
$this->config->setData([
|
||||||
|
'foo' => [
|
||||||
|
'bar' => 'baz',
|
||||||
|
'x' => '-y',
|
||||||
|
'p !=' => 'np'
|
||||||
|
],
|
||||||
|
'apple' => [1,3,5,7,9]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataGet()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'single value' => [
|
||||||
|
'file' => 'foo',
|
||||||
|
'key' => 'x',
|
||||||
|
'expected' => '-y'
|
||||||
|
],
|
||||||
|
'whole array' => [
|
||||||
|
'file' => 'apple',
|
||||||
|
'key' => NULL,
|
||||||
|
'expected' => [1,3,5,7,9]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataGet
|
||||||
|
*/
|
||||||
|
public function testGet($file, $key, $expected)
|
||||||
|
{
|
||||||
|
$res = $this->config->get($file, $key);
|
||||||
|
$this->assertEquals($expected, $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testBadGet()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$this->config->get('bleucheese');
|
||||||
|
}
|
||||||
|
catch (\InvalidArgumentException $e)
|
||||||
|
{
|
||||||
|
$this->assertTrue(TRUE, "Proper exception was caught");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of ConfigTest.php
|
@ -15,14 +15,107 @@ class OutputTest extends Sleepy_Testcase {
|
|||||||
$c = new Config();
|
$c = new Config();
|
||||||
$i = new Input();
|
$i = new Input();
|
||||||
|
|
||||||
$this->output = new Output($c, $i);
|
$this->output = new MockOutput($c, $i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetAcceptedType()
|
public function dataSetData()
|
||||||
{
|
{
|
||||||
|
return [
|
||||||
|
'default mime' => [
|
||||||
|
'accept' => 'text/csv, text/plain;q=0.8, */*;q=0.1',
|
||||||
|
'parsed_accept' => [
|
||||||
|
'1' => 'text/csv',
|
||||||
|
'0.8' => 'text/plain',
|
||||||
|
'0.1' => '*/*'
|
||||||
|
],
|
||||||
|
'types' => 'json',
|
||||||
|
'mapping' => [
|
||||||
|
'text/xml' => 'XML',
|
||||||
|
'foo/bar' => 'HTML',
|
||||||
|
'application/json' => 'JSON',
|
||||||
|
'*/*' => 'application/json'
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'application/json' => 'Sleepy\Type\JSON'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'html' => [
|
||||||
|
'accept' => 'text/html, text/plain;q=0.5',
|
||||||
|
'parsed_accept' => [
|
||||||
|
'1' => 'text/html',
|
||||||
|
'0.5' => 'text/plain'
|
||||||
|
],
|
||||||
|
'types' => ['json', 'xml', 'html'],
|
||||||
|
'mapping' => [
|
||||||
|
'text/html' => 'HTML',
|
||||||
|
'application/json' => 'JSON',
|
||||||
|
'text/plain' => 'YAML'
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'text/html' => 'Sleepy\Type\HTML'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataSetData
|
||||||
|
*/
|
||||||
|
public function testSetData($accept, $parsed_accept, $types, $mapping, $expected)
|
||||||
|
{
|
||||||
|
$_SERVER['HTTP_ACCEPT'] = $accept;
|
||||||
|
|
||||||
|
// Recreate input object to use new
|
||||||
|
// superglobal value
|
||||||
|
$in = new Input();
|
||||||
|
$pheaders = $in->header_array();
|
||||||
|
$this->output->setInput($in);
|
||||||
|
|
||||||
|
// Sanity check of accept header
|
||||||
|
$this->assertEquals($parsed_accept, $pheaders['accept']);
|
||||||
|
|
||||||
|
// Mock config to set the mime/class mapping list
|
||||||
|
$conf = new MockConfig();
|
||||||
|
$conf->setData(['type_class_map' => $mapping]);
|
||||||
|
$this->output->setConfig($conf);
|
||||||
|
|
||||||
|
$this->output->setData(['foo' => 'bar']);
|
||||||
|
$res = $this->output->set_data($types);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataSetHeader()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'single_header' => [
|
||||||
|
'input' => 'Content-type: text/html',
|
||||||
|
'expected' => [
|
||||||
|
'Content-type: text/html'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'header_array' => [
|
||||||
|
'input' => [
|
||||||
|
'Content-type: text/plain',
|
||||||
|
'Accept-encoding' => 'utf-8'
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'Content-type: text/plain',
|
||||||
|
'Accept-encoding: utf-8'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataSetHeader
|
||||||
|
*/
|
||||||
|
public function testSetHeader($input, $expected)
|
||||||
|
{
|
||||||
|
$this->output->set_header($input);
|
||||||
|
$headers = $this->output->getHeaders();
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $headers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// End of OutputTest.php
|
// End of OutputTest.php
|
Reference in New Issue
Block a user