1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-24 18:13:00 +01:00

[Service] Moving ApiCommand specific functionality from the Inspector and on to the ApiCommand

This commit is contained in:
Michael Dowling 2012-05-30 23:32:30 -07:00
parent 06834146d7
commit f48f686e7a
9 changed files with 476 additions and 514 deletions

View File

@ -63,7 +63,7 @@ class AsyncPlugin implements EventSubscriberInterface
}
/**
* Event emitted when a curl exception occurs. Ignore the exception and
* Event emitted when a curl exception occurs. Ignore the exception and
* set a mock response.
*
* @param Event $event
@ -79,7 +79,7 @@ class AsyncPlugin implements EventSubscriberInterface
/**
* Event emitted when a request completes because it took less than 1ms.
* Add an X-Guzzle-Async header to notify the caller that there is no
* Add an X-Guzzle-Async header to notify the caller that there is no
* body in the message.
*
* @param Event $event

View File

@ -53,34 +53,14 @@ abstract class AbstractCommand extends Collection implements CommandInterface
/**
* Constructor
*
* @param array|Collection $parameters Collection of parameters
* to set on the command
* @param ApiCommand $apiCommand Command definition from description
* @param array|Collection $parameters Collection of parameters to set on the command
* @param ApiCommand $apiCommand Command definition from description
*/
public function __construct($parameters = null, ApiCommand $apiCommand = null)
{
parent::__construct($parameters);
if ($apiCommand) {
$this->apiCommand = $apiCommand;
} else {
// If this is a concrete command, then build an ApiCommand object
$className = get_class($this);
// Determine the name of the command based on the relation to the
// client that executes the command
$this->apiCommand = new ApiCommand(array(
'name' => str_replace('\\_', '.', Inflector::snake(substr($className, strpos($className, 'Command') + 8))),
'class' => $className,
'params' => $this->getInspector()->getApiParamsForClass($className)
));
}
// Set default and static values on the command
$this->getInspector()->initConfig($this->apiCommand->getParams(), $this);
$this->apiCommand = $apiCommand ?: ApiCommand::fromCommand(get_class($this));
$this->initConfig();
$headers = $this->get('headers');
if (!$headers instanceof Collection) {
@ -324,10 +304,7 @@ abstract class AbstractCommand extends Collection implements CommandInterface
}
// Fail on missing required arguments, and change parameters via filters
// Perform a non-idempotent validation on the parameters. Options that
// change during validation will persist (e.g. injection, filters).
$this->getInspector()->validateConfig($this->apiCommand->getParams(), $this, true);
$this->apiCommand->validate($this, $this->getInspector());
$this->build();
// Add custom request headers set on the command
@ -431,4 +408,20 @@ abstract class AbstractCommand extends Collection implements CommandInterface
}
}
}
/**
* Prepare the default and static settings of the command
*/
protected function initConfig()
{
foreach ($this->apiCommand->getParams() as $name => $arg) {
$currentValue = $this->get($name);
$configValue = $arg->getValue($currentValue);
// If default or static values are set, then this should always be
// updated on the config object
if ($currentValue !== $configValue) {
$this->set($name, $configValue);
}
}
}
}

View File

@ -2,6 +2,11 @@
namespace Guzzle\Service\Description;
use Guzzle\Common\Collection;
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Service\Exception\ValidationException;
use Guzzle\Service\Inflector;
use Guzzle\Service\Inspector;
/**
* Data object holding the information of an API command
@ -13,6 +18,11 @@ class ApiCommand
*/
const DEFAULT_COMMAND_CLASS = 'Guzzle\\Service\\Command\\DynamicCommand';
/**
* @var string Annotation used to specify Guzzle service description info
*/
const GUZZLE_ANNOTATION = '@guzzle';
/**
* @var array Parameters
*/
@ -43,6 +53,11 @@ class ApiCommand
*/
protected $class;
/**
* @var array Cache of parsed Command class ApiCommands
*/
protected static $apiCommandCache = array();
/**
* Constructor
*
@ -89,6 +104,62 @@ class ApiCommand
}
}
/**
* Create an ApiCommand object from a class and its docblock
*
* The following is the format for @guzzle arguments:
* @guzzle argument_name [default="default value"] [required="true|false"] [type="registered constraint name"] [type_args=""] [doc="Description of argument"]
* Example: @guzzle my_argument default="hello" required="true" doc="Set the argument to control the widget..."
*
* @param string $className Name of the class
*
* @return ApiCommand
*/
public static function fromCommand($className)
{
if (!isset(self::$apiCommandCache[$className])) {
$reflection = new \ReflectionClass($className);
// Get all of the @guzzle annotations from the class
$matches = array();
$params = array();
preg_match_all('/' . self::GUZZLE_ANNOTATION . '\s+([A-Za-z0-9_\-\.]+)\s*([A-Za-z0-9]+=".+")*/', $reflection->getDocComment(), $matches);
// Parse the docblock annotations
if (!empty($matches[1])) {
foreach ($matches[1] as $index => $match) {
// Add the matched argument to the array keys
$params[$match] = array();
if (isset($matches[2])) {
// Break up the argument attributes by closing quote
foreach (explode('" ', $matches[2][$index]) as $part) {
$attrs = array();
// Find the attribute and attribute value
preg_match('/([A-Za-z0-9]+)="(.+)"*/', $part, $attrs);
if (isset($attrs[1]) && isset($attrs[0])) {
// Sanitize the strings
if ($attrs[2][strlen($attrs[2]) - 1] == '"') {
$attrs[2] = substr($attrs[2], 0, strlen($attrs[2]) - 1);
}
$params[$match][$attrs[1]] = $attrs[2];
}
}
}
$params[$match] = new ApiParam($params[$match]);
}
}
self::$apiCommandCache[$className] = new ApiCommand(array(
'name' => str_replace('\\_', '.', Inflector::snake(substr($className, strpos($className, 'Command') + 8))),
'class' => $className,
'params' => $params
));
}
return self::$apiCommandCache[$className];
}
/**
* Get as an array
*
@ -177,4 +248,72 @@ class ApiCommand
{
return $this->uri;
}
/**
* Validates that all required args are included in a config object,
* and if not, throws an InvalidArgumentException with a helpful error message. Adds
* default args to the passed config object if the parameter was not
* set in the config object.
*
* @param Collection $config Configuration settings
*
* @throws ValidationException when validation errors occur
*/
public function validate(Collection $config, Inspector $inspector = null)
{
$inspector = $inspector ?: Inspector::getInstance();
$typeValidation = $inspector->getTypeValidation();
$errors = array();
foreach ($this->params as $name => $arg) {
$currentValue = $config->get($name);
$configValue = $arg->getValue($currentValue);
// Inject configuration information into the config value
if ($configValue && is_string($configValue)) {
$configValue = $config->inject($configValue);
}
// Ensure that required arguments are set
if ($arg->getRequired() && ($configValue === null || $configValue === '')) {
$errors[] = 'Requires that the ' . $name . ' argument be supplied.' . ($arg->getDoc() ? ' (' . $arg->getDoc() . ').' : '');
continue;
}
// Ensure that the correct data type is being used
if ($typeValidation && $configValue !== null && $argType = $arg->getType()) {
$validation = $inspector->validateConstraint($argType, $configValue, $arg->getTypeArgs());
if ($validation !== true) {
$errors[] = $name . ': ' . $validation;
$config->set($name, $configValue);
continue;
}
}
$configValue = $arg->filter($configValue);
// Update the config value if it changed
if (!$configValue !== $currentValue) {
$config->set($name, $configValue);
}
// Check the length values if validating data
$argMinLength = $arg->getMinLength();
if ($argMinLength && strlen($configValue) < $argMinLength) {
$errors[] = 'Requires that the ' . $name . ' argument be >= ' . $arg->getMinLength() . ' characters.';
}
$argMaxLength = $arg->getMaxLength();
if ($argMaxLength && strlen($configValue) > $argMaxLength) {
$errors[] = 'Requires that the ' . $name . ' argument be <= ' . $arg->getMaxLength() . ' characters.';
}
}
if (!empty($errors)) {
$e = new ValidationException('Validation errors: ' . implode("\n", $errors));
$e->setErrors($errors);
throw $e;
}
}
}

View File

@ -4,4 +4,27 @@ namespace Guzzle\Service\Exception;
use Guzzle\Common\Exception\RuntimeException;
class ValidationException extends RuntimeException {}
class ValidationException extends RuntimeException
{
protected $errors = array();
/**
* Set the validation error messages
*
* @param array $errors Array of validation errors
*/
public function setErrors(array $errors)
{
$this->errors = $errors;
}
/**
* Get any validation errors
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}

View File

@ -5,39 +5,19 @@ namespace Guzzle\Service;
use Guzzle\Common\Collection;
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Service\Exception\ValidationException;
use Guzzle\Service\Description\ApiParam;
/**
* Inspects configuration options versus defined parameters, adding default
* values where appropriate, performing type validation on config settings, and
* validating input vs output data.
*
* The Reflection class can parse @guzzle specific parameters in a class's
* docblock comment and validate that a specified {@see Collection} object
* has the default arguments specified, has the required arguments set, and
* that the passed arguments that match the typehints specified in the
* annotation.
*
* The following is the format for @guzzle arguments:
* @guzzle argument_name [default="default value"] [required="true|false"] [type="registered constraint name"] [type_args=""] [doc="Description of argument"]
*
* Here's an example:
* @guzzle my_argument default="hello" required="true" doc="Set the argument to control the widget..."
* Prepares configuration settings with default values and ensures that required
* values are set. Holds references to validation constraints and their default
* values.
*/
class Inspector
{
const GUZZLE_ANNOTATION = '@guzzle';
/**
* @var Inspector Cached Inspector instance
*/
private static $instance;
/**
* @var array Cache of parsed doc blocks
*/
protected $cache = array();
/**
* @var array Array of aliased constraints
*/
@ -143,6 +123,16 @@ class Inspector
$this->typeValidation = $typeValidation;
}
/**
* Check if type validation is enabled
*
* @return bool
*/
public function getTypeValidation()
{
return $this->typeValidation;
}
/**
* Get an array of the registered constraints by name
*
@ -209,152 +199,4 @@ class Inspector
return $this->getConstraint($name)->validate($value, $args);
}
/**
* Get an array of ApiParam objects for a class using @guzzle annotations
*
* @param string $class Name of a class to parse
*
* @return array Returns an array of ApiParam objects
*/
public function getApiParamsForClass($class)
{
if (!isset($this->cache[$class])) {
$reflection = new \ReflectionClass($class);
$this->cache[$class] = $this->parseDocBlock($reflection->getDocComment());
}
return $this->cache[$class];
}
/**
* Initialize a configuration collection with static and default parameters
*
* @param array $params Array of ApiParam objects
* @param Collection $config Collection of configuration options
*/
public function initConfig(array $params, Collection $config)
{
foreach ($params as $name => $arg) {
$currentValue = $config->get($name);
$configValue = $arg->getValue($currentValue);
// If default or static values are set, then this should always be
// updated on the config object
if ($currentValue !== $configValue) {
$config->set($name, $configValue);
}
}
}
/**
* Validates that all required args are included in a config object,
* and if not, throws an InvalidArgumentException with a helpful error message. Adds
* default args to the passed config object if the parameter was not
* set in the config object.
*
* @param array $params Params to validate
* @param Collection $config Configuration settings
* @param bool $strict Set to false to allow missing required fields
*
* @return array|bool Returns an array of errors or TRUE on success
*
* @throws InvalidArgumentException if any args are missing and $strict is TRUE
*/
public function validateConfig(array $params, Collection $config, $strict = true)
{
$errors = array();
foreach ($params as $name => $arg) {
$currentValue = $config->get($name);
$configValue = $arg->getValue($currentValue);
// Inject configuration information into the config value
if ($configValue && is_string($configValue)) {
$configValue = $config->inject($configValue);
}
// Ensure that required arguments are set
if ($arg->getRequired() && ($configValue === null || $configValue === '')) {
$errors[] = 'Requires that the ' . $name . ' argument be supplied.' . ($arg->getDoc() ? ' (' . $arg->getDoc() . ').' : '');
continue;
}
// Ensure that the correct data type is being used
if ($this->typeValidation && $configValue !== null && $argType = $arg->getType()) {
$validation = $this->validateConstraint($argType, $configValue, $arg->getTypeArgs());
if ($validation !== true) {
$errors[] = $name . ': ' . $validation;
$config->set($name, $configValue);
continue;
}
}
$configValue = $arg->filter($configValue);
// Update the config value if it changed
if (!$configValue !== $currentValue) {
$config->set($name, $configValue);
}
// Check the length values if validating data
$argMinLength = $arg->getMinLength();
if ($argMinLength && strlen($configValue) < $argMinLength) {
$errors[] = 'Requires that the ' . $name . ' argument be >= ' . $arg->getMinLength() . ' characters.';
}
$argMaxLength = $arg->getMaxLength();
if ($argMaxLength && strlen($configValue) > $argMaxLength) {
$errors[] = 'Requires that the ' . $name . ' argument be <= ' . $arg->getMaxLength() . ' characters.';
}
}
if (empty($errors)) {
return true;
} elseif ($strict) {
throw new ValidationException('Validation errors: ' . implode("\n", $errors));
}
return $errors;
}
/**
* Get the Guzzle arguments from a DocBlock
*
* @param string $doc Docblock to parse
*
* @return array Returns an associative array of ApiParam objects
*/
protected function parseDocBlock($doc)
{
// Get all of the @guzzle annotations from the class
$matches = array();
preg_match_all('/' . self::GUZZLE_ANNOTATION . '\s+([A-Za-z0-9_\-\.]+)\s*([A-Za-z0-9]+=".+")*/', $doc, $matches);
$params = array();
if (!empty($matches[1])) {
foreach ($matches[1] as $index => $match) {
// Add the matched argument to the array keys
$params[$match] = array();
if (isset($matches[2])) {
// Break up the argument attributes by closing quote
foreach (explode('" ', $matches[2][$index]) as $part) {
$attrs = array();
// Find the attribute and attribute value
preg_match('/([A-Za-z0-9]+)="(.+)"*/', $part, $attrs);
if (isset($attrs[1]) && isset($attrs[0])) {
// Sanitize the strings
if ($attrs[2][strlen($attrs[2]) - 1] == '"') {
$attrs[2] = substr($attrs[2], 0, strlen($attrs[2]) - 1);
}
$params[$match][$attrs[1]] = $attrs[2];
}
}
}
$params[$match] = new ApiParam($params[$match]);
}
}
return $params;
}
}

View File

@ -7,6 +7,7 @@ use Guzzle\Service\Client;
use Guzzle\Service\Command\CommandInterface;
use Guzzle\Service\Command\AbstractCommand;
use Guzzle\Service\Description\ApiCommand;
use Guzzle\Service\Description\ApiParam;
use Guzzle\Service\Inspector;
use Guzzle\Http\Plugin\MockPlugin;
use Guzzle\Tests\Service\Mock\Command\MockCommand;
@ -400,4 +401,25 @@ class CommandTest extends AbstractCommandTest
$command->setResult('foo!');
$this->assertEquals('foo!', $command->getResult());
}
/**
* @covers Guzzle\Service\Command\AbstractCommand::initConfig
*/
public function testCanInitConfig()
{
$command = $this->getMockBuilder('Guzzle\\Service\\Command\\AbstractCommand')
->setConstructorArgs(array(array(
'foo' => 'bar'
), new ApiCommand(array(
'params' => array(
'baz' => new ApiParam(array(
'default' => 'baaar'
))
)
))))
->getMockForAbstractClass();
$this->assertEquals('bar', $command['foo']);
$this->assertEquals('baaar', $command['baz']);
}
}

View File

@ -3,12 +3,32 @@
namespace Guzzle\Tests\Service\Description;
use Guzzle\Common\Collection;
use Guzzle\Service\Inspector;
use Guzzle\Service\Description\ApiCommand;
use Guzzle\Service\Description\ApiParam;
use Guzzle\Service\Description\ServiceDescription;
use Guzzle\Service\Exception\ValidationException;
/**
* @guzzle test type="type:object"
* @guzzle bool_1 default="true" type="boolean"
* @guzzle bool_2 default="false"
* @guzzle float type="float"
* @guzzle int type="integer"
* @guzzle date type="date"
* @guzzle timestamp type="time"
* @guzzle string type="string"
* @guzzle username required="true" filters="strtolower"
* @guzzle dynamic default="{username}_{ string }_{ does_not_exist }"
* @guzzle test_function type="string" filters="Guzzle\Tests\Service\Description\ApiCommandTest::strtoupper"
*/
class ApiCommandTest extends \Guzzle\Tests\GuzzleTestCase
{
public static function strtoupper($string)
{
return strtoupper($string);
}
/**
* @covers Guzzle\Service\Description\ApiCommand
*/
@ -97,4 +117,201 @@ class ApiCommandTest extends \Guzzle\Tests\GuzzleTestCase
$c = new ApiCommand($data);
$this->assertEquals($data, $c->toArray());
}
/**
* Clear the class cache of the ApiCommand static factory method
*/
protected function clearCommandCache()
{
$refObject = new \ReflectionClass('Guzzle\Service\Description\ApiCommand');
$refProperty = $refObject->getProperty('apiCommandCache');
$refProperty->setAccessible(true);
$refProperty->setValue(null, array());
}
/**
* @covers Guzzle\Service\Description\ApiCommand::fromCommand
*/
public function testDoesNotErrorWhenNoAnnotationsArePresent()
{
$this->clearCommandCache();
$command = ApiCommand::fromCommand('Guzzle\\Tests\\Service\\Mock\\Command\\Sub\\Sub');
$this->assertEquals(array(), $command->getParams());
// Ensure that the cache returns the same value
$this->assertSame($command, ApiCommand::fromCommand('Guzzle\\Tests\\Service\\Mock\\Command\\Sub\\Sub'));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::fromCommand
*/
public function testBuildsApiParamFromClassDocBlock()
{
$command = ApiCommand::fromCommand('Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand');
$this->assertEquals(3, count($command->getParams()));
$this->assertTrue($command->getParam('test')->getRequired());
$this->assertEquals('123', $command->getParam('test')->getDefault());
$this->assertEquals('Test argument', $command->getParam('test')->getDoc());
$this->assertEquals('abc', $command->getParam('_internal')->getDefault());
}
protected function getApiCommand()
{
return ApiCommand::fromCommand(get_class($this));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testAddsDefaultAndInjectsConfigs()
{
$col = new Collection(array(
'username' => 'user',
'string' => 'test',
'float' => 1.23
));
$this->getApiCommand()->validate($col);
$this->assertEquals(false, $col->get('bool_2'));
$this->assertEquals('user_test_', $col->get('dynamic'));
$this->assertEquals(1.23, $col->get('float'));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
* @expectedException Guzzle\Service\Exception\ValidationException
*/
public function testValidatesTypeHints()
{
$this->getApiCommand()->validate(new Collection(array(
'test' => 'uh oh',
'username' => 'test'
)));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testConvertsBooleanDefaults()
{
$c = new Collection(array(
'test' => $this,
'username' => 'test'
));
$this->getApiCommand()->validate($c);
$this->assertTrue($c->get('bool_1'));
$this->assertFalse($c->get('bool_2'));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testValidatesArgs()
{
$config = new Collection(array(
'data' => 123,
'min' => 'a',
'max' => 'aaa'
));
$command = new ApiCommand(array(
'params' => array(
'data' => new ApiParam(array(
'type' => 'string'
)),
'min' => new ApiParam(array(
'type' => 'string',
'min_length' => 2
)),
'max' => new ApiParam(array(
'type' => 'string',
'max_length' => 2
))
)
));
try {
$command->validate($config);
$this->fail('Did not throw expected exception');
} catch (ValidationException $e) {
$concat = implode("\n", $e->getErrors());
$this->assertContains("Value must be of type string", $concat);
$this->assertContains("Requires that the min argument be >= 2 characters", $concat);
$this->assertContains("Requires that the max argument be <= 2 characters", $concat);
}
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testRunsValuesThroughFilters()
{
$data = new Collection(array(
'username' => 'TEST',
'test_function' => 'foo'
));
$this->getApiCommand()->validate($data);
$this->assertEquals('test', $data->get('username'));
$this->assertEquals('FOO', $data->get('test_function'));
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testTypeValidationCanBeDisabled()
{
$i = Inspector::getInstance();
$i->setTypeValidation(false);
$command = new ApiCommand(array(
'params' => array(
'data' => new ApiParam(array(
'type' => 'string'
))
)
));
$command->validate(new Collection(array(
'data' => new \stdClass()
)), $i);
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
*/
public function testSkipsFurtherValidationIfNotSet()
{
$command = new ApiCommand(array(
'params' => array(
'data' => new ApiParam(array(
'type' => 'string'
))
)
));
$command->validate(new Collection());
}
/**
* @covers Guzzle\Service\Description\ApiCommand::validate
* @expectedException Guzzle\Service\Exception\ValidationException
* @expectedExceptionMessage Validation errors: Requires that the data argument be supplied.
*/
public function testValidatesRequiredFieldsAreSet()
{
$command = new ApiCommand(array(
'params' => array(
'data' => new ApiParam(array(
'required' => true
))
)
));
$command->validate(new Collection());
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Guzzle\Tests\Service\Exception;
use Guzzle\Service\Exception\ValidationException;
class ValidationExceptionTest extends \Guzzle\Tests\GuzzleTestCase
{
public function testCanSetAndRetrieveErrors()
{
$errors = array('foo', 'bar');
$e = new ValidationException('Foo');
$e->setErrors($errors);
$this->assertEquals($errors, $e->getErrors());
}
}

View File

@ -5,28 +5,24 @@ namespace Guzzle\Tests\Common;
use Guzzle\Common\Collection;
use Guzzle\Service\Inspector;
use Guzzle\Service\Description\ApiParam;
use Guzzle\Service\Description\ApiCommand;
use Guzzle\Service\Exception\ValidationException;
/**
* @covers Guzzle\Service\Inspector
*
* @guzzle test type="type:object"
* @guzzle bool_1 default="true" type="boolean"
* @guzzle bool_2 default="false"
* @guzzle float type="float"
* @guzzle int type="integer"
* @guzzle date type="date"
* @guzzle timestamp type="time"
* @guzzle string type="string"
* @guzzle username required="true" filters="strtolower"
* @guzzle dynamic default="{username}_{ string }_{ does_not_exist }"
* @guzzle test_function type="string" filters="Guzzle\Tests\Common\InspectorTest::strtoupper"
*/
class InspectorTest extends \Guzzle\Tests\GuzzleTestCase
{
public static function strtoupper($string)
/**
* @covers Guzzle\Service\Inspector::setTypeValidation
* @covers Guzzle\Service\Inspector::getTypeValidation
*/
public function testTypeValidationCanBeToggled()
{
return strtoupper($string);
$i = new Inspector();
$this->assertTrue($i->getTypeValidation());
$i->setTypeValidation(false);
$this->assertFalse($i->getTypeValidation());
}
/**
@ -38,6 +34,15 @@ class InspectorTest extends \Guzzle\Tests\GuzzleTestCase
$this->assertNotEmpty($inspector->getRegisteredConstraints());
}
/**
* @covers Guzzle\Service\Inspector
* @expectedException InvalidArgumentException
*/
public function testChecksFilterValidity()
{
Inspector::getInstance()->getConstraint('foooo');
}
/**
* @covers Guzzle\Service\Inspector::prepareConfig
*/
@ -65,152 +70,6 @@ class InspectorTest extends \Guzzle\Tests\GuzzleTestCase
}
}
/**
* @covers Guzzle\Service\Inspector
*/
public function testAddsDefaultAndInjectsConfigs()
{
$col = new Collection(array(
'username' => 'user',
'string' => 'test',
'float' => 1.23
));
$inspector = Inspector::getInstance();
$inspector->validateConfig($inspector->getApiParamsForClass(__CLASS__), $col, true);
$this->assertEquals(false, $col->get('bool_2'));
$this->assertEquals('user_test_', $col->get('dynamic'));
$this->assertEquals(1.23, $col->get('float'));
}
/**
* @covers Guzzle\Service\Inspector::validateConfig
* @covers Guzzle\Service\Inspector::getApiParamsForClass
* @expectedException Guzzle\Service\Exception\ValidationException
*/
public function testValidatesTypeHints()
{
$inspector = Inspector::getInstance();
$inspector->validateConfig($inspector->getApiParamsForClass(__CLASS__), new Collection(array(
'test' => 'uh oh',
'username' => 'test'
)));
}
/**
* @covers Guzzle\Service\Inspector::validateConfig
*/
public function testConvertsBooleanDefaults()
{
$c = new Collection(array(
'test' => $this,
'username' => 'test'
));
$inspector = Inspector::getInstance();
$inspector->validateConfig($inspector->getApiParamsForClass(__CLASS__), $c);
$this->assertTrue($c->get('bool_1'));
$this->assertFalse($c->get('bool_2'));
}
/**
* @covers Guzzle\Service\Inspector
*/
public function testInspectsClassArgs()
{
$doc = <<<EOT
/**
* Client for interacting with the Unfuddle webservice
*
* @guzzle username required="true" doc="API username" type="string"
* @guzzle password required="true" doc="API password" type="string"
* @guzzle subdomain required="true" doc="Unfuddle project subdomain" type="string"
* @guzzle api_version required="true" default="v1" doc="API version" type="choice:'v1','v2',v3"
* @guzzle protocol required="true" default="https" doc="HTTP protocol (http or https)" type="string"
* @guzzle base_url required="true" default="{ protocol }://{ subdomain }.unfuddle.com/api/{ api_version }/" doc="Unfuddle API base URL" type="string"
* @guzzle class type="type:object"
*/
EOT;
$inspector = new Inspector();
$method = new \ReflectionMethod($inspector, 'parseDocBlock');
$method->setAccessible(true);
$params = $method->invoke($inspector, $doc);
$this->assertEquals(array(
'required' => true,
'doc' => 'API username',
'type' => 'string'
), array_filter($params['username']->toArray()));
$this->assertEquals(array(
'required' => true,
'default' => 'v1',
'doc' => 'API version',
'type' => 'choice',
'type_args' => array('v1', 'v2', 'v3')
), array_filter($params['api_version']->toArray()));
$this->assertEquals(array(
'required' => true,
'default' => 'https',
'doc' => 'HTTP protocol (http or https)',
'type' => 'string'
), array_filter($params['protocol']->toArray()));
$this->assertEquals(array(
'required' => true,
'default' => '{ protocol }://{ subdomain }.unfuddle.com/api/{ api_version }/',
'doc' => 'Unfuddle API base URL',
'type' => 'string'
), array_filter($params['base_url']->toArray()));
$this->assertEquals(array(
'type' => 'type',
'type_args' => array('object')
), array_filter($params['class']->toArray()));
$config = new Collection(array(
'username' => 'test',
'password' => 'pass',
'subdomain' => 'sub',
'api_version' => 'v2'
));
// Do an idempotent initialization
Inspector::getInstance()->initConfig($params, $config);
// make sure defaults and statics were added, but configs were not injected
$this->assertEquals('{ protocol }://{ subdomain }.unfuddle.com/api/{ api_version }/', $config->get('base_url'));
$this->assertEquals('https', $config->get('protocol'));
// Not do a non-idempotent updated
Inspector::getInstance()->validateConfig($params, $config, true);
// make sure the configs were injected
$this->assertEquals('https://sub.unfuddle.com/api/v2/', $config->get('base_url'));
try {
Inspector::getInstance()->validateConfig($params, new Collection(array(
'base_url' => '',
'username' => '',
'password' => '',
'class' => '123',
'api_version' => 'v10'
)));
$this->fail('Expected exception not thrown when params are invalid');
} catch (ValidationException $e) {
$concat = $e->getMessage();
$this->assertContains("Validation errors: Requires that the username argument be supplied. (API username)", $concat);
$this->assertContains("Requires that the password argument be supplied. (API password)", $concat);
$this->assertContains("Requires that the subdomain argument be supplied. (Unfuddle project subdomain)", $concat);
$this->assertContains("api_version: Value must be one of: v1, v2, v3", $concat);
$this->assertContains("class: Value must be of type object", $concat);
}
}
/**
* @covers Guzzle\Service\Inspector::registerConstraint
* @covers Guzzle\Service\Inspector::getConstraint
@ -231,157 +90,7 @@ EOT;
$this->assertInstanceOf($constraintClass, Inspector::getInstance()->getConstraint('mock'));
$this->assertInstanceOf($constraintClass, Inspector::getInstance()->getConstraint('mock_2'));
$validating = new Collection(array(
'data' => '192.168.16.121',
'test' => '10.1.1.0'
));
$this->assertTrue(Inspector::getInstance()->validateConfig(array(
'data' => new ApiParam(array(
'type' => 'mock',
'name' => 'data'
)),
'test' => new ApiParam(array(
'type' => 'mock_2',
'name' => 'test'
))
), $validating, false));
}
/**
* @covers Guzzle\Service\Inspector
* @expectedException InvalidArgumentException
*/
public function testChecksFilterValidity()
{
Inspector::getInstance()->validateConfig(array(
'data' => new ApiParam(array(
'type' => 'invalid'
))
), new Collection(array(
'data' => 'false'
)));
}
/**
* @covers Guzzle\Service\Inspector
*/
public function testValidatesArgs()
{
$config = new Collection(array(
'data' => 123,
'min' => 'a',
'max' => 'aaa'
));
$result = Inspector::getInstance()->validateConfig(array(
'data' => new ApiParam(array(
'type' => 'string'
)),
'min' => new ApiParam(array(
'type' => 'string',
'min_length' => 2
)),
'max' => new ApiParam(array(
'type' => 'string',
'max_length' => 2
))
), $config, false);
$concat = implode("\n", $result);
$this->assertContains("Value must be of type string", $concat);
$this->assertContains("Requires that the min argument be >= 2 characters", $concat);
$this->assertContains("Requires that the max argument be <= 2 characters", $concat);
}
/**
* @covers Guzzle\Service\Inspector::parseDocBlock
*/
public function testVerifiesGuzzleAnnotations()
{
$inspector = new Inspector();
$method = new \ReflectionMethod($inspector, 'parseDocBlock');
$method->setAccessible(true);
$result = $method->invoke($inspector, 'testing');
$this->assertEquals(array(), $result);
}
/**
* @covers Guzzle\Service\Inspector::validateConfig
* @covers Guzzle\Service\Inspector::getApiParamsForClass
*/
public function testRunsValuesThroughFilters()
{
$data = new Collection(array(
'username' => 'TEST',
'test_function' => 'foo'
));
$inspector = Inspector::getInstance();
$inspector->validateConfig($inspector->getApiParamsForClass(__CLASS__), $data, true, true, false);
$this->assertEquals('test', $data->get('username'));
$this->assertEquals('FOO', $data->get('test_function'));
}
/**
* @covers Guzzle\Service\Inspector::setTypeValidation
* @covers Guzzle\Service\Inspector::validateConfig
*/
public function testTypeValidationCanBeDisabled()
{
$i = Inspector::getInstance();
$i->setTypeValidation(false);
// Ensure that the type is not validated
$i->validateConfig(array(
'data' => new ApiParam(array(
'type' => 'string'
))
), new Collection(array(
'data' => new \stdClass()
)), true);
}
/**
* @covers Guzzle\Service\Inspector::validateConfig
*/
public function testSkipsFurtherValidationIfNotSet()
{
$i = Inspector::getInstance();
// Ensure that the type is not validated
$this->assertEquals(true, $i->validateConfig(array(
'data' => new ApiParam(array(
'type' => 'string'
))
), new Collection(), true));
}
/**
* @covers Guzzle\Service\Inspector::initConfig
*/
public function testCanInitConfig()
{
$i = Inspector::getInstance();
$param = new ApiParam(array(
'type' => 'array',
'filters' => 'json_encode'
));
$config = new Collection(array(
'data' => array(
'foo' => 'bar'
)
));
$i->initConfig(array(
'data' => $param
), $config);
// Ensure it's still an array
$this->assertInternalType('array', $config->get('data'));
$this->assertTrue(Inspector::getInstance()->validateConstraint('mock', '192.168.16.121'));
$this->assertTrue(Inspector::getInstance()->validateConstraint('mock_2', '10.1.1.0'));
}
}