From d3159a21baad74194ff87431571aea3307cfad1d Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 28 Mar 2014 01:34:13 -0700 Subject: [PATCH] CRM-14370 - API Kernel - Move request parser to its own class --- Civi/API/Kernel.php | 124 +------------- Civi/API/Request.php | 152 ++++++++++++++++++ .../API/{KernelTest.php => RequestTest.php} | 57 ++++++- 3 files changed, 203 insertions(+), 130 deletions(-) create mode 100644 Civi/API/Request.php rename tests/phpunit/Civi/API/{KernelTest.php => RequestTest.php} (66%) diff --git a/Civi/API/Kernel.php b/Civi/API/Kernel.php index c9943a8a88..87d65ad54a 100644 --- a/Civi/API/Kernel.php +++ b/Civi/API/Kernel.php @@ -75,7 +75,7 @@ class Kernel { // TODO Define alternative calling convention makes it easier to construct $apiRequest // without the ambiguity of "data" vs "options" - $apiRequest = $this->createRequest($entity, $action, $params, $extra); + $apiRequest = Request::create($entity, $action, $params, $extra); try { if (!is_array($params)) { @@ -110,128 +110,6 @@ class Kernel { return $this->formatResult($apiRequest, $err); } - - } - - /** - * Create a formatted/normalized request object. - * - * @param string $entity - * @param string $action - * @param array $params - * @param mixed $extra - * @return array the request descriptor; keys: - * - version: int - * - entity: string - * - action: string - * - params: array (string $key => mixed $value) [deprecated in v4] - * - extra: unspecified - * - fields: NULL|array (string $key => array $fieldSpec) - * - options: \CRM_Utils_OptionBag derived from params [v4-only] - * - data: \CRM_Utils_OptionBag derived from params [v4-only] - * - chains: unspecified derived from params [v4-only] - */ - public function createRequest($entity, $action, $params, $extra) { - $apiRequest = array(); // new \Civi\API\Request(); - $apiRequest['version'] = $this->parseVersion($params); - $apiRequest['params'] = $params; - $apiRequest['extra'] = $extra; - $apiRequest['fields'] = NULL; - - if ($apiRequest['version'] <= 3) { - // APIv1-v3 munges entity/action names, which means that the same name can be written - // multiple ways. That makes it harder to work with. - $apiRequest['entity'] = \CRM_Utils_String::munge($entity); - $apiRequest['action'] = \CRM_Utils_String::munge($action); - } - else { - // APIv4 requires exact entity/action name; deviations should cause errors - if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $entity)) { - throw new \API_Exception("Malformed entity"); - } - if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $action)) { - throw new \API_Exception("Malformed action"); - } - $apiRequest['entity'] = $entity; - $apiRequest['action'] = $action; - } - - // APIv1-v3 mix data+options in $params which means that each API callback is responsible - // for splitting the two. In APIv4, the split is done systematically so that we don't - // so much parsing logic spread around. - if ($apiRequest['version'] >= 4) { - $options = array(); - $data = array(); - $chains = array(); - foreach ($params as $key => $value) { - if ($key == 'options') { - $options = array_merge($options, $value); - } - elseif ($key == 'return') { - if (!isset($options['return'])) { - $options['return'] = array(); - } - $options['return'] = array_merge($options['return'], $value); - } - elseif (preg_match('/^option\.(.*)$/', $key, $matches)) { - $options[$matches[1]] = $value; - } - elseif (preg_match('/^return\.(.*)$/', $key, $matches)) { - if ($value) { - if (!isset($options['return'])) { - $options['return'] = array(); - } - $options['return'][] = $matches[1]; - } - } - elseif (preg_match('/^format\.(.*)$/', $key, $matches)) { - if ($value) { - if (!isset($options['format'])) { - $options['format'] = $matches[1]; - } - else { - throw new \API_Exception("Too many API formats specified"); - } - } - } - elseif (preg_match('/^api\./', $key)) { - // FIXME: represent subrequests as instances of "Request" - $chains[$key] = $value; - } - elseif ($key == 'debug') { - $options['debug'] = $value; - } - elseif ($key == 'version') { - // ignore - } - else { - $data[$key] = $value; - - } - } - $apiRequest['options'] = new \CRM_Utils_OptionBag($options); - $apiRequest['data'] = new \CRM_Utils_OptionBag($data); - $apiRequest['chains'] = $chains; - } - - return $apiRequest; - } - - /** - * We must be sure that every request uses only one version of the API. - * - * @param array $params - * @return int - */ - protected function parseVersion($params) { - $desired_version = empty($params['version']) ? NULL : (int) $params['version']; - if (isset($desired_version) && is_integer($desired_version)) { - return $desired_version; - } - else { - // we will set the default to version 3 as soon as we find that it works. - return 3; - } } public function boot() { diff --git a/Civi/API/Request.php b/Civi/API/Request.php new file mode 100644 index 0000000000..4c49eb4483 --- /dev/null +++ b/Civi/API/Request.php @@ -0,0 +1,152 @@ + mixed $value) [deprecated in v4] + * - extra: unspecified + * - fields: NULL|array (string $key => array $fieldSpec) + * - options: \CRM_Utils_OptionBag derived from params [v4-only] + * - data: \CRM_Utils_OptionBag derived from params [v4-only] + * - chains: unspecified derived from params [v4-only] + */ + public static function create($entity, $action, $params, $extra) { + $apiRequest = array(); // new \Civi\API\Request(); + $apiRequest['version'] = self::parseVersion($params); + $apiRequest['params'] = $params; + $apiRequest['extra'] = $extra; + $apiRequest['fields'] = NULL; + + if ($apiRequest['version'] <= 3) { + // APIv1-v3 munges entity/action names, which means that the same name can be written + // multiple ways. That makes it harder to work with. + $apiRequest['entity'] = \CRM_Utils_String::munge($entity); + $action = \CRM_Utils_String::munge($action); + $apiRequest['action'] = strtolower($action{0}) . substr($action, 1); + } + else { + // APIv4 requires exact entity/action name; deviations should cause errors + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $entity)) { + throw new \API_Exception("Malformed entity"); + } + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $action)) { + throw new \API_Exception("Malformed action"); + } + $apiRequest['entity'] = $entity; + $apiRequest['action'] = strtolower($action{0}) . substr($action, 1); + } + + // APIv1-v3 mix data+options in $params which means that each API callback is responsible + // for splitting the two. In APIv4, the split is done systematically so that we don't + // so much parsing logic spread around. + if ($apiRequest['version'] >= 4) { + $options = array(); + $data = array(); + $chains = array(); + foreach ($params as $key => $value) { + if ($key == 'options') { + $options = array_merge($options, $value); + } + elseif ($key == 'return') { + if (!isset($options['return'])) { + $options['return'] = array(); + } + $options['return'] = array_merge($options['return'], $value); + } + elseif (preg_match('/^option\.(.*)$/', $key, $matches)) { + $options[$matches[1]] = $value; + } + elseif (preg_match('/^return\.(.*)$/', $key, $matches)) { + if ($value) { + if (!isset($options['return'])) { + $options['return'] = array(); + } + $options['return'][] = $matches[1]; + } + } + elseif (preg_match('/^format\.(.*)$/', $key, $matches)) { + if ($value) { + if (!isset($options['format'])) { + $options['format'] = $matches[1]; + } + else { + throw new \API_Exception("Too many API formats specified"); + } + } + } + elseif (preg_match('/^api\./', $key)) { + // FIXME: represent subrequests as instances of "Request" + $chains[$key] = $value; + } + elseif ($key == 'debug') { + $options['debug'] = $value; + } + elseif ($key == 'version') { + // ignore + } + else { + $data[$key] = $value; + + } + } + $apiRequest['options'] = new \CRM_Utils_OptionBag($options); + $apiRequest['data'] = new \CRM_Utils_OptionBag($data); + $apiRequest['chains'] = $chains; + } + + return $apiRequest; + } + + /** + * We must be sure that every request uses only one version of the API. + * + * @param array $params + * @return int + */ + protected static function parseVersion($params) { + $desired_version = empty($params['version']) ? NULL : (int) $params['version']; + if (isset($desired_version) && is_integer($desired_version)) { + return $desired_version; + } + else { + // we will set the default to version 3 as soon as we find that it works. + return 3; + } + } + +} \ No newline at end of file diff --git a/tests/phpunit/Civi/API/KernelTest.php b/tests/phpunit/Civi/API/RequestTest.php similarity index 66% rename from tests/phpunit/Civi/API/KernelTest.php rename to tests/phpunit/Civi/API/RequestTest.php index 1240aaabbd..e930c4a6fc 100644 --- a/tests/phpunit/Civi/API/KernelTest.php +++ b/tests/phpunit/Civi/API/RequestTest.php @@ -5,7 +5,7 @@ require_once 'CiviTest/CiviUnitTestCase.php'; /** */ -class KernelTest extends \CiviUnitTestCase { +class RequestTest extends \CiviUnitTestCase { function v4options() { $cases = array(); // array(0 => $requestParams, 1 => $expectedOptions, 2 => $expectedData, 3 => $expectedChains) @@ -78,8 +78,7 @@ class KernelTest extends \CiviUnitTestCase { * @dataProvider v4options */ function testCreateRequest_v4Options($inputParams, $expectedOptions, $expectedData, $expectedChains) { - $kernel = new Kernel(NULL); - $apiRequest = $kernel->createRequest('MyEntity', 'MyAction', $inputParams, NULL); + $apiRequest = Request::create('MyEntity', 'MyAction', $inputParams, NULL); $this->assertEquals($expectedOptions, $apiRequest['options']->getArray()); $this->assertEquals($expectedData, $apiRequest['data']->getArray()); $this->assertEquals($expectedChains, $apiRequest['chains']); @@ -89,15 +88,59 @@ class KernelTest extends \CiviUnitTestCase { * @expectedException \API_Exception */ function testCreateRequest_v4BadEntity() { - $kernel = new Kernel(NULL); - $kernel->createRequest('Not!Valid', 'create', array('version' => 4), NULL); + Request::create('Not!Valid', 'create', array('version' => 4), NULL); } /** * @expectedException \API_Exception */ function testCreateRequest_v4BadAction() { - $kernel = new Kernel(NULL); - $kernel->createRequest('MyEntity', 'bad!action', array('version' => 4), NULL); + Request::create('MyEntity', 'bad!action', array('version' => 4), NULL); } + + function validEntityActionPairs() { + $cases = array(); + $cases[] = array( + array('MyEntity', 'MyAction', 3), + array('MyEntity', 'myAction', 3), + ); + $cases[] = array( + array('my+entity', 'MyAction', 3), + array('my_entity', 'myAction', 3), + ); + $cases[] = array( + array('MyEntity', 'MyAction', 4), + array('MyEntity', 'myAction', 4), + ); + return $cases; + } + + /** + * @dataProvider validEntityActionPairs + */ + function testCreateRequest_EntityActionMunging($input, $expected) { + list ($inEntity, $inAction, $inVersion) = $input; + $apiRequest = Request::create($inEntity, $inAction, array('version' => $inVersion), NULL); + $this->assertEquals($expected, array($apiRequest['entity'], $apiRequest['action'], $apiRequest['version'])); + } + + function invalidEntityActionPairs() { + $cases = array(); + $cases[] = array('My+Entity', 'MyAction', 4); + $cases[] = array('My Entity', 'MyAction', 4); + $cases[] = array('2MyEntity', 'MyAction', 4); + $cases[] = array('MyEntity', 'My+Action', 4); + $cases[] = array('MyEntity', 'My Action', 4); + $cases[] = array('MyEntity', '2Action', 4); + return $cases; + } + + /** + * @dataProvider invalidEntityActionPairs + * @expectedException \API_Exception + */ + function testCreateRequest_InvalidEntityAction($inEntity, $inAction, $inVersion) { + Request::create($inEntity, $inAction, array('version' => $inVersion), NULL); + } + } \ No newline at end of file -- 2.25.1