Merge branch 'develop' into loic

This commit is contained in:
manzerbredes 2016-03-15 16:17:39 +01:00
commit 26d10bc0fa
108 changed files with 6223 additions and 177 deletions

View file

@ -1,6 +1,12 @@
<?php <?php
include_once("../core/Plugin_Api.php"); include_once("../core/Plugin_Api.php");
include_once("../core/LibOverride/genTokenOptions.php"); include_once("../core/LibOverride/genTokenOptions.php");
include_once("../core/ErrorManagement.php");
use OpenStack\Common\Error\BadResponseError;
use OpenStack\Common\Error\BaseError;
use OpenStack\Common\Error\NotImplementedError;
use OpenStack\Common\Error\UserInputError;
class AppTest{ class AppTest{
@ -10,6 +16,7 @@ class AppTest{
protected $tokenClass; protected $tokenClass;
protected $tokenPost; protected $tokenPost;
protected $output; protected $output;
protected $errorClass;
public function __construct($args){ public function __construct($args){
@ -19,6 +26,9 @@ class AppTest{
$this->pluginsApi = plugin_api::getInstance(); $this->pluginsApi = plugin_api::getInstance();
$this->errorClass = new errorManagement($this); $this->errorClass = new errorManagement($this);
$this->output = array(); $this->output = array();
$this->errorClass = new errorManagement($this);
$this->postParams = $_POST; $this->postParams = $_POST;
} }
@ -94,4 +104,12 @@ class AppTest{
return json_encode($this->output); return json_encode($this->output);
} }
public function getErrorInstance(){
return $this->errorClass;
}
} }

View file

@ -1,5 +1,6 @@
{ {
"require": { "require": {
"php-opencloud/openstack": "dev-master" "php-opencloud/openstack": "dev-master",
"php-opencloud/common": "dev-master"
} }
} }

54
server/composer.lock generated
View file

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "e7cbd5d3df36fb1a3f3378b837068196", "hash": "7647ed348aee68f2db6b71e4d2fb1fe7",
"content-hash": "125e4702f7a417475a4150c889ac6c3d", "content-hash": "82731500d050a65f09e92b70c1f96ea9",
"packages": [ "packages": [
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
@ -244,6 +244,51 @@
], ],
"time": "2016-01-25 15:43:01" "time": "2016-01-25 15:43:01"
}, },
{
"name": "php-opencloud/common",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/php-opencloud/common.git",
"reference": "fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-opencloud/common/zipball/fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62",
"reference": "fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~6.1",
"justinrainbow/json-schema": "~1.3",
"php": "~7.0"
},
"require-dev": {
"fabpot/php-cs-fixer": "~1.0",
"jakub-onderka/php-parallel-lint": "0.*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0",
"sami/sami": "dev-master",
"satooshi/php-coveralls": "~1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"OpenCloud\\": "src/",
"OpenCloud\\Test\\": "tests/unit/",
"OpenCloud\\Integration\\": "tests/integration/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Jamie Hannaford",
"email": "jamie.hannaford@rackspace.com",
"homepage": "https://github.com/jamiehannaford"
}
],
"time": "2016-03-08 12:56:34"
},
{ {
"name": "php-opencloud/openstack", "name": "php-opencloud/openstack",
"version": "dev-master", "version": "dev-master",
@ -254,7 +299,7 @@
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-opencloud/openstack/zipball/15aca73f423166c7ef8337ba08615c103c66e931", "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/f2ee77024843659d970817a9e7055bb40a3724f9",
"reference": "15aca73f423166c7ef8337ba08615c103c66e931", "reference": "15aca73f423166c7ef8337ba08615c103c66e931",
"shasum": "" "shasum": ""
}, },
@ -344,7 +389,8 @@
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": { "stability-flags": {
"php-opencloud/openstack": 20 "php-opencloud/openstack": 20,
"php-opencloud/common": 20
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,

View file

@ -3,10 +3,10 @@ include_once("core/Plugin_Api.php");
include_once("core/LibOverride/genTokenOptions.php"); include_once("core/LibOverride/genTokenOptions.php");
include_once("core/ErrorManagement.php"); include_once("core/ErrorManagement.php");
use OpenStack\Common\Error\BadResponseError; use OpenCloud\Common\Error\BadResponseError;
use OpenStack\Common\Error\BaseError; use OpenCloud\Common\Error\BaseError;
use OpenStack\Common\Error\NotImplementedError; use OpenCloud\Common\Error\NotImplementedError;
use OpenStack\Common\Error\UserInputError; use OpenCloud\Common\Error\UserInputError;
class App{ class App{

View file

@ -1,9 +1,9 @@
<?php <?php
use OpenStack\Common\Error\BadResponseError; use OpenCloud\Common\Error\BadResponseError;
use OpenStack\Common\Error\BaseError; use OpenCloud\Common\Error\BaseError;
use OpenStack\Common\Error\NotImplementedError; use OpenCloud\Common\Error\NotImplementedError;
use OpenStack\Common\Error\UserInputError; use OpenCloud\Common\Error\UserInputError;
Class errorManagement{ Class errorManagement{
@ -36,4 +36,4 @@ Class errorManagement{
} }
?> ?>

View file

@ -9,7 +9,7 @@
* *
* @todo Complete the functions and finish the descriptions * @todo Complete the functions and finish the descriptions
*/ */
use OpenStack\Common\Error; use OpenCloud\Common\Error;
/** /**
* Identity Class of the back-end application * Identity Class of the back-end application

View file

@ -43,18 +43,8 @@ class image implements Core{
if(!isset($app)){ if(!isset($app)){
$this->app->setOutput("Error", "Incorrect parameter"); $this->app->setOutput("Error", "Incorrect parameter");
} }
try{ $this->app = $app;
$this->app = $app; $this->libClass = $app->getLibClass("Image");
$this->libClass = $app->getLibClass("Image");
}catch(BadResponseError $e){
$this->app->getErrorInstance()->BadResponseHandler($e);
}catch(UserInputError $e){
$this->app->getErrorInstance->UserInputHandler($e);
}catch(BaseError $e){
$this->app->getErrorInstance->BaseErrorHandler($e);
}catch(NotImplementedError $e){
$this->app->getErrorInstance->NotImplementedHandler($e);
}
} }
@ -78,7 +68,7 @@ class image implements Core{
* options for the image creation * options for the image creation
* *
**/ **/
private function createImage(array $opt){ private function createImage(){
$opt = $this->app->getPostParam("opt"); $opt = $this->app->getPostParam("opt");
if(!isset($opt)){ if(!isset($opt)){
@ -363,7 +353,7 @@ class image implements Core{
* @param string $file_name * @param string $file_name
* path of the image * path of the image
**/ **/
private function uploadImage($id, $file_name){ private function uploadImage(){
$id = $this->app->getPostParam("id"); $id = $this->app->getPostParam("id");
$file_name = $this->app->getPostParam("file_name"); $file_name = $this->app->getPostParam("file_name");
@ -399,7 +389,7 @@ class image implements Core{
* @param string $id * @param string $id
* identifier of the image * identifier of the image
**/ **/
private function downloadImage($id){ private function downloadImage(){
$id = $this->app->getPostParam("id"); $id = $this->app->getPostParam("id");
if(!isset($id)){ if(!isset($id)){
$this->app->setOutput("Error", "Incorrect parameter"); $this->app->setOutput("Error", "Incorrect parameter");
@ -600,7 +590,7 @@ class image implements Core{
* @param string $status * @param string $status
* new status for the member * new status for the member
**/ **/
private function updateMemberImage($image_id, $member_id, $status){ private function updateMemberImage(){
$image_id = $this->app->getPostParam("image_id"); $image_id = $this->app->getPostParam("image_id");
$member_id = $this->app->getPostParam("member_id"); $member_id = $this->app->getPostParam("member_id");
$status = $this->app->getPostParam("status"); $status = $this->app->getPostParam("status");

View file

@ -1,12 +1,12 @@
<?php <?php
use GuzzleHttp\Client; use GuzzleHttp\Client;
use OpenStack\Common\Transport\HandlerStack; use OpenCloud\Common\Transport\HandlerStack;
use OpenStack\Common\Transport\Middleware; use OpenCloud\Common\Transport\Middleware;
use OpenStack\Identity\v3\Service; use OpenStack\Identity\v3\Service;
use OpenStack\Identity\v3\Api; use OpenStack\Identity\v3\Api;
use OpenStack\Common\Auth\Token; use OpenCloud\Common\Auth\Token;
use OpenStack\Common\Transport\Utils; use OpenCloud\Common\Transport\Utils;
use OpenStack\Identity\v3\Models; use OpenStack\Identity\v3\Models;
class genTokenOptions class genTokenOptions

View file

@ -7,9 +7,10 @@ $baseDir = dirname($vendorDir);
return array( return array(
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
'OpenStack\\Test\\' => array($vendorDir . '/php-opencloud/openstack/tests/unit'),
'OpenStack\\Integration\\' => array($vendorDir . '/php-opencloud/openstack/tests/integration'),
'OpenStack\\' => array($vendorDir . '/php-opencloud/openstack/src'), 'OpenStack\\' => array($vendorDir . '/php-opencloud/openstack/src'),
'OpenCloud\\Test\\' => array($vendorDir . '/php-opencloud/common/tests/unit'),
'OpenCloud\\Integration\\' => array($vendorDir . '/php-opencloud/common/tests/integration'),
'OpenCloud\\' => array($vendorDir . '/php-opencloud/common/src'),
'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'), 'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),

View file

@ -1,57 +1,4 @@
[ [
{
"name": "guzzlehttp/promises",
"version": "1.0.3",
"version_normalized": "1.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/b1e1c0d55f8083c71eda2c28c12a228d708294ea",
"reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"time": "2015-10-15 22:28:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
]
},
{ {
"name": "psr/http-message", "name": "psr/http-message",
"version": "1.0", "version": "1.0",
@ -167,54 +114,6 @@
"web service" "web service"
] ]
}, },
{
"name": "php-opencloud/openstack",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/php-opencloud/openstack.git",
"reference": "15aca73f423166c7ef8337ba08615c103c66e931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-opencloud/openstack/zipball/15aca73f423166c7ef8337ba08615c103c66e931",
"reference": "15aca73f423166c7ef8337ba08615c103c66e931",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~6.1",
"justinrainbow/json-schema": "~1.3",
"php": ">=5.6"
},
"require-dev": {
"fabpot/php-cs-fixer": "~1.0",
"jakub-onderka/php-parallel-lint": "0.*",
"phpspec/prophecy-phpunit": "~1.0",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0",
"sami/sami": "dev-master",
"satooshi/php-coveralls": "~1.0"
},
"time": "2016-01-25 10:35:10",
"type": "library",
"installation-source": "source",
"autoload": {
"psr-4": {
"OpenStack\\": "src/",
"OpenStack\\Test\\": "tests/unit/",
"OpenStack\\Integration\\": "tests/integration/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Jamie Hannaford",
"email": "jamie.hannaford@rackspace.com",
"homepage": "https://github.com/jamiehannaford"
}
]
},
{ {
"name": "justinrainbow/json-schema", "name": "justinrainbow/json-schema",
"version": "1.6.1", "version": "1.6.1",
@ -284,18 +183,118 @@
] ]
}, },
{ {
"name": "guzzlehttp/psr7", "name": "php-opencloud/common",
"version": "1.2.2", "version": "dev-master",
"version_normalized": "1.2.2.0", "version_normalized": "9999999-dev",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/psr7.git", "url": "https://github.com/php-opencloud/common.git",
"reference": "f5d04bdd2881ac89abde1fb78cc234bce24327bb" "reference": "fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5d04bdd2881ac89abde1fb78cc234bce24327bb", "url": "https://api.github.com/repos/php-opencloud/common/zipball/fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62",
"reference": "f5d04bdd2881ac89abde1fb78cc234bce24327bb", "reference": "fd027b817c3dd8f83b0c8d9fb1df34ebf25f8c62",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~6.1",
"justinrainbow/json-schema": "~1.3",
"php": "~7.0"
},
"require-dev": {
"fabpot/php-cs-fixer": "~1.0",
"jakub-onderka/php-parallel-lint": "0.*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0",
"sami/sami": "dev-master",
"satooshi/php-coveralls": "~1.0"
},
"time": "2016-03-08 12:56:34",
"type": "library",
"installation-source": "source",
"autoload": {
"psr-4": {
"OpenCloud\\": "src/",
"OpenCloud\\Test\\": "tests/unit/",
"OpenCloud\\Integration\\": "tests/integration/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Jamie Hannaford",
"email": "jamie.hannaford@rackspace.com",
"homepage": "https://github.com/jamiehannaford"
}
]
},
{
"name": "guzzlehttp/promises",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/bb9024c526b22f3fe6ae55a561fd70653d470aa8",
"reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"time": "2016-03-08 01:15:46",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
]
},
{
"name": "guzzlehttp/psr7",
"version": "1.2.3",
"version_normalized": "1.2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/2e89629ff057ebb49492ba08e6995d3a6a80021b",
"reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -308,7 +307,7 @@
"require-dev": { "require-dev": {
"phpunit/phpunit": "~4.0" "phpunit/phpunit": "~4.0"
}, },
"time": "2016-01-23 01:23:02", "time": "2016-02-18 21:54:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@ -342,5 +341,48 @@
"stream", "stream",
"uri" "uri"
] ]
},
{
"name": "php-opencloud/openstack",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/php-opencloud/openstack.git",
"reference": "f2ee77024843659d970817a9e7055bb40a3724f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-opencloud/openstack/zipball/f2ee77024843659d970817a9e7055bb40a3724f9",
"reference": "f2ee77024843659d970817a9e7055bb40a3724f9",
"shasum": ""
},
"require": {
"php-opencloud/common": "dev-master"
},
"require-dev": {
"fabpot/php-cs-fixer": "~1.0",
"jakub-onderka/php-parallel-lint": "0.*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0",
"sami/sami": "dev-master",
"satooshi/php-coveralls": "~1.0"
},
"time": "2016-03-08 14:37:14",
"type": "library",
"installation-source": "source",
"autoload": {
"psr-4": {
"OpenStack\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Jamie Hannaford",
"email": "jamie.hannaford@rackspace.com",
"homepage": "https://github.com/jamiehannaford"
}
]
} }
] ]

View file

@ -1,5 +1,11 @@
# CHANGELOG # CHANGELOG
## 1.1.0 - 2016-03-07
* Update EachPromise to prevent recurring on a iterator when advancing, as this
could trigger fatal generator errors.
* Update Promise to allow recursive waiting without unwrapping exceptions.
## 1.0.3 - 2015-10-15 ## 1.0.3 - 2015-10-15
* Update EachPromise to immediately resolve when the underlying promise iterator * Update EachPromise to immediately resolve when the underlying promise iterator

View file

@ -24,6 +24,9 @@ class EachPromise implements PromisorInterface
/** @var Promise */ /** @var Promise */
private $aggregate; private $aggregate;
/** @var bool */
private $mutex;
/** /**
* Configuration hash can include the following key value pairs: * Configuration hash can include the following key value pairs:
* *
@ -81,6 +84,7 @@ class EachPromise implements PromisorInterface
private function createPromise() private function createPromise()
{ {
$this->mutex = false;
$this->aggregate = new Promise(function () { $this->aggregate = new Promise(function () {
reset($this->pending); reset($this->pending);
if (empty($this->pending) && !$this->iterable->valid()) { if (empty($this->pending) && !$this->iterable->valid()) {
@ -169,11 +173,21 @@ class EachPromise implements PromisorInterface
private function advanceIterator() private function advanceIterator()
{ {
// Place a lock on the iterator so that we ensure to not recurse,
// preventing fatal generator errors.
if ($this->mutex) {
return false;
}
$this->mutex = true;
try { try {
$this->iterable->next(); $this->iterable->next();
$this->mutex = false;
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->aggregate->reject($e); $this->aggregate->reject($e);
$this->mutex = false;
return false; return false;
} }
} }
@ -186,9 +200,11 @@ class EachPromise implements PromisorInterface
} }
unset($this->pending[$idx]); unset($this->pending[$idx]);
$this->advanceIterator();
if (!$this->checkIfFinished()) { // Only refill pending promises if we are not locked, preventing the
// EachPromise to recursively invoke the provided iterator, which
// cause a fatal error: "Cannot resume an already running generator"
if ($this->advanceIterator() && !$this->checkIfFinished()) {
// Add more pending promises if possible. // Add more pending promises if possible.
$this->refillPending(); $this->refillPending();
} }

View file

@ -61,17 +61,19 @@ class Promise implements PromiseInterface
{ {
$this->waitIfPending(); $this->waitIfPending();
if (!$unwrap) { $inner = $this->result instanceof PromiseInterface
return null; ? $this->result->wait($unwrap)
} : $this->result;
if ($this->result instanceof PromiseInterface) { if ($unwrap) {
return $this->result->wait($unwrap); if ($this->result instanceof PromiseInterface
} elseif ($this->state === self::FULFILLED) { || $this->state === self::FULFILLED
return $this->result; ) {
} else { return $inner;
// It's rejected so "unwrap" and throw an exception. } else {
throw exception_for($this->result); // It's rejected so "unwrap" and throw an exception.
throw exception_for($inner);
}
} }
} }
@ -257,11 +259,10 @@ class Promise implements PromiseInterface
$this->waitList = null; $this->waitList = null;
foreach ($waitList as $result) { foreach ($waitList as $result) {
descend:
$result->waitIfPending(); $result->waitIfPending();
if ($result->result instanceof Promise) { while ($result->result instanceof Promise) {
$result = $result->result; $result = $result->result;
goto descend; $result->waitIfPending();
} }
} }
} }

View file

@ -56,6 +56,7 @@ class TaskQueue
*/ */
public function run() public function run()
{ {
/** @var callable $task */
while ($task = array_shift($this->queue)) { while ($task = array_shift($this->queue)) {
$task(); $task();
} }

View file

@ -146,9 +146,9 @@ function inspect(PromiseInterface $promise)
'value' => $promise->wait() 'value' => $promise->wait()
]; ];
} catch (RejectionException $e) { } catch (RejectionException $e) {
return ['state' => 'rejected', 'reason' => $e->getReason()]; return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
} catch (\Exception $e) { } catch (\Exception $e) {
return ['state' => 'rejected', 'reason' => $e]; return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
} }
} }
@ -304,10 +304,10 @@ function settle($promises)
return each( return each(
$promises, $promises,
function ($value, $idx) use (&$results) { function ($value, $idx) use (&$results) {
$results[$idx] = ['state' => 'fulfilled', 'value' => $value]; $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
}, },
function ($reason, $idx) use (&$results) { function ($reason, $idx) use (&$results) {
$results[$idx] = ['state' => 'rejected', 'reason' => $reason]; $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
} }
)->then(function () use (&$results) { )->then(function () use (&$results) {
ksort($results); ksort($results);

View file

@ -39,8 +39,8 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
public function testIsWaitable() public function testIsWaitable()
{ {
$a = new Promise(function () use (&$a) { $a->resolve('a'); }); $a = $this->createSelfResolvingPromise('a');
$b = new Promise(function () use (&$b) { $b->resolve('b'); }); $b = $this->createSelfResolvingPromise('b');
$called = []; $called = [];
$each = new EachPromise([$a, $b], [ $each = new EachPromise([$a, $b], [
'fulfilled' => function ($value) use (&$called) { $called[] = $value; } 'fulfilled' => function ($value) use (&$called) { $called[] = $value; }
@ -54,7 +54,7 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
public function testCanResolveBeforeConsumingAll() public function testCanResolveBeforeConsumingAll()
{ {
$called = 0; $called = 0;
$a = new Promise(function () use (&$a) { $a->resolve('a'); }); $a = $this->createSelfResolvingPromise('a');
$b = new Promise(function () { $this->fail(); }); $b = new Promise(function () { $this->fail(); });
$each = new EachPromise([$a, $b], [ $each = new EachPromise([$a, $b], [
'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called) { 'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called) {
@ -291,4 +291,46 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
} }
$this->assertEquals(range(0, 9), $results); $this->assertEquals(range(0, 9), $results);
} }
private function createSelfResolvingPromise($value)
{
$p = new Promise(function () use (&$p, $value) {
$p->resolve($value);
});
return $p;
}
public function testMutexPreventsGeneratorRecursion()
{
$results = $promises = [];
for ($i = 0; $i < 20; $i++) {
$p = $this->createSelfResolvingPromise($i);
$pending[] = $p;
$promises[] = $p;
}
$iter = function () use (&$promises, &$pending) {
foreach ($promises as $promise) {
// Resolve a promises, which will trigger the then() function,
// which would cause the EachPromise to try to add more
// promises to the queue. Without a lock, this would trigger
// a "Cannot resume an already running generator" fatal error.
if ($p = array_pop($pending)) {
$p->wait();
}
yield $promise;
}
};
$each = new EachPromise($iter(), [
'concurrency' => 5,
'fulfilled' => function ($r) use (&$results, &$pending) {
$results[] = $r;
}
]);
$each->promise()->wait();
$this->assertCount(20, $results);
}
} }

View file

@ -172,6 +172,18 @@ class PromiseTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Whoop', $p->wait()); $this->assertEquals('Whoop', $p->wait());
} }
public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped()
{
$p2 = new Promise(function () use (&$p2) {
$p2->reject('Fail');
});
$p = new Promise(function () use ($p2, &$p) {
$p->resolve($p2);
});
$p->wait(false);
$this->assertSame(Promise::REJECTED, $p2->getState());
}
public function testCannotCancelNonPending() public function testCannotCancelNonPending()
{ {
$p = new Promise(); $p = new Promise();

View file

@ -1,5 +1,11 @@
# CHANGELOG # CHANGELOG
## 1.2.3 - 2016-02-18
* Fixed support in `GuzzleHttp\Psr7\CachingStream` for seeking forward on remote
streams, which can sometimes return fewer bytes than requested with `fread`.
* Fixed handling of gzipped responses with FNAME headers.
## 1.2.2 - 2016-01-22 ## 1.2.2 - 2016-01-22
* Added support for URIs without any authority. * Added support for URIs without any authority.

View file

@ -38,7 +38,7 @@ echo $composed(); // abc, 123. Above all listen to me.
`GuzzleHttp\Psr7\BufferStream` `GuzzleHttp\Psr7\BufferStream`
Provides a buffer stream that can be written to to fill a buffer, and read Provides a buffer stream that can be written to fill a buffer, and read
from to remove bytes from the buffer. from to remove bytes from the buffer.
This stream returns a "hwm" metadata value that tells upstream consumers This stream returns a "hwm" metadata value that tells upstream consumers
@ -106,7 +106,7 @@ echo $stream; // 0123456789
Compose stream implementations based on a hash of functions. Compose stream implementations based on a hash of functions.
Allows for easy testing and extension of a provided stream without needing to Allows for easy testing and extension of a provided stream without needing
to create a concrete class for a simple extension point. to create a concrete class for a simple extension point.
```php ```php

View file

@ -60,9 +60,12 @@ class CachingStream implements StreamInterface
$diff = $byte - $this->stream->getSize(); $diff = $byte - $this->stream->getSize();
if ($diff > 0) { if ($diff > 0) {
// If the seek byte is greater the number of read bytes, then read // Read the remoteStream until we have read in at least the amount
// the difference of bytes to cache the bytes and inherently seek. // of bytes requested, or we reach the end of the file.
$this->read($diff); while ($diff > 0 && !$this->remoteStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else { } else {
// We can just do a normal seek since we've already seen this byte. // We can just do a normal seek since we've already seen this byte.
$this->stream->seek($byte); $this->stream->seek($byte);

View file

@ -20,10 +20,33 @@ class InflateStream implements StreamInterface
public function __construct(StreamInterface $stream) public function __construct(StreamInterface $stream)
{ {
// Skip the first 10 bytes // read the first 10 bytes, ie. gzip header
$stream = new LimitStream($stream, -1, 10); $header = $stream->read(10);
$filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header);
// Skip the header, that is 10 + length of filename + 1 (nil) bytes
$stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength);
$resource = StreamWrapper::getResource($stream); $resource = StreamWrapper::getResource($stream);
stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ);
$this->stream = new Stream($resource); $this->stream = new Stream($resource);
} }
/**
* @param StreamInterface $stream
* @param $header
* @return int
*/
private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header)
{
$filename_header_length = 0;
if (substr(bin2hex($header), 6, 2) === '08') {
// we have a filename, read until nil
$filename_header_length = 1;
while ($stream->read(1) !== chr(0)) {
$filename_header_length++;
}
}
return $filename_header_length;
}
} }

View file

@ -98,6 +98,33 @@ class CachingStreamTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('ing', $this->body->read(3)); $this->assertEquals('ing', $this->body->read(3));
} }
public function testCanSeekToReadBytesWithPartialBodyReturned()
{
$stream = fopen('php://temp', 'r+');
fwrite($stream, 'testing');
fseek($stream, 0);
$this->decorated = $this->getMockBuilder('\GuzzleHttp\Psr7\Stream')
->setConstructorArgs([$stream])
->setMethods(['read'])
->getMock();
$this->decorated->expects($this->exactly(2))
->method('read')
->willReturnCallback(function($length) use ($stream){
return fread($stream, 2);
});
$this->body = new CachingStream($this->decorated);
$this->assertEquals(0, $this->body->tell());
$this->body->seek(4, SEEK_SET);
$this->assertEquals(4, $this->body->tell());
$this->body->seek(0);
$this->assertEquals('test', $this->body->read(4));
}
public function testWritesToBufferStream() public function testWritesToBufferStream()
{ {
$this->body->read(2); $this->body->read(2);

View file

@ -13,4 +13,27 @@ class InflateStreamtest extends \PHPUnit_Framework_TestCase
$b = new InflateStream($a); $b = new InflateStream($a);
$this->assertEquals('test', (string) $b); $this->assertEquals('test', (string) $b);
} }
public function testInflatesStreamsWithFilename()
{
$content = $this->getGzipStringWithFilename('test');
$a = Psr7\stream_for($content);
$b = new InflateStream($a);
$this->assertEquals('test', (string) $b);
}
private function getGzipStringWithFilename($original_string)
{
$gzipped = bin2hex(gzencode($original_string));
$header = substr($gzipped, 0, 20);
// set FNAME flag
$header[6]=0;
$header[7]=8;
// make a dummy filename
$filename = "64756d6d7900";
$rest = substr($gzipped, 20);
return hex2bin($header . $filename . $rest);
}
} }

View file

@ -154,7 +154,7 @@ class UriTest extends \PHPUnit_Framework_TestCase
[self::RFC3986_BASE, 'g;x=1/../y', 'http://a/b/c/y'], [self::RFC3986_BASE, 'g;x=1/../y', 'http://a/b/c/y'],
['http://u@a/b/c/d;p?q', '.', 'http://u@a/b/c/'], ['http://u@a/b/c/d;p?q', '.', 'http://u@a/b/c/'],
['http://u:p@a/b/c/d;p?q', '.', 'http://u:p@a/b/c/'], ['http://u:p@a/b/c/d;p?q', '.', 'http://u:p@a/b/c/'],
//[self::RFC3986_BASE, 'http:g', 'http:g'], ['http://a/b/c/d/', 'e', 'http://a/b/c/d/e'],
]; ];
} }

View file

@ -0,0 +1,10 @@
.idea/
.test/
coverage/
vendor/
*.pyc
phpunit.xml
coverage.xml
composer.lock

View file

@ -0,0 +1,36 @@
{
"name": "php-opencloud/common",
"authors": [
{
"name": "Jamie Hannaford",
"email": "jamie.hannaford@rackspace.com",
"homepage" : "https://github.com/jamiehannaford"
}
],
"autoload": {
"psr-4": {
"OpenCloud\\": "src/",
"OpenCloud\\Test\\": "tests/unit/",
"OpenCloud\\Integration\\": "tests/integration/"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/php-opencloud/Sami"
}
],
"require": {
"php": "~7.0",
"guzzlehttp/guzzle": "~6.1",
"justinrainbow/json-schema": "~1.3"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"sami/sami": "dev-master",
"psr/log": "~1.0",
"satooshi/php-coveralls": "~1.0",
"jakub-onderka/php-parallel-lint": "0.*",
"fabpot/php-cs-fixer": "~1.0"
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="OpenStack">
<directory>tests/unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
<exclude>
<directory suffix="Interface.php">./src</directory>
<directory suffix="Api.php">./src</directory>
<directory suffix="Params.php">./src</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View file

@ -0,0 +1,33 @@
<?php
namespace OpenCloud\Common\Api;
abstract class AbstractApi implements ApiInterface
{
protected $params;
protected function isRequired(array $param)
{
return array_merge($param, ['required' => true]);
}
protected function notRequired(array $param)
{
return array_merge($param, ['required' => false]);
}
protected function query(array $param)
{
return array_merge($param, ['location' => AbstractParams::QUERY]);
}
protected function url(array $param)
{
return array_merge($param, ['location' => AbstractParams::URL]);
}
public function documented(array $param)
{
return array_merge($param, ['required' => true]);
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace OpenCloud\Common\Api;
abstract class AbstractParams
{
// locations
const QUERY = 'query';
const HEADER = 'header';
const URL = 'url';
const JSON = 'json';
const RAW = 'raw';
// types
const STRING_TYPE = "string";
const BOOL_TYPE = "boolean";
const BOOLEAN_TYPE = self::BOOL_TYPE;
const OBJECT_TYPE = "object";
const ARRAY_TYPE = "array";
const NULL_TYPE = "NULL";
const INT_TYPE = 'integer';
const INTEGER_TYPE = self::INT_TYPE;
public static function isSupportedLocation($val)
{
return in_array($val, [self::QUERY, self::HEADER, self::URL, self::JSON, self::RAW]);
}
public function limit()
{
return [
'type' => self::INT_TYPE,
'location' => 'query',
'description' => <<<DESC
This will limit the total amount of elements returned in a list up to the number specified. For example, specifying a
limit of 10 will return 10 elements, regardless of the actual count.
DESC
];
}
public function marker()
{
return [
'type' => 'string',
'location' => 'query',
'description' => <<<DESC
Specifying a marker will begin the list from the value specified. Elements will have a particular attribute that
identifies them, such as a name or ID. The marker value will search for an element whose identifying attribute matches
the marker value, and begin the list from there.
DESC
];
}
public function id($type)
{
return [
'description' => sprintf("The unique ID, or identifier, for the %s", $type),
'type' => self::STRING_TYPE,
'location' => self::JSON,
];
}
public function idPath()
{
return [
'type' => self::STRING_TYPE,
'location' => self::URL,
'description' => 'The unique ID of the resource',
];
}
public function name($resource)
{
return [
'description' => sprintf("The name of the %s", $resource),
'type' => self::STRING_TYPE,
'location' => self::JSON,
];
}
public function sortDir()
{
return [
'type' => self::STRING_TYPE,
'location' => self::QUERY,
'description' => "Sorts by one or more sets of attribute and sort direction combinations.",
'enum' => ['asc', 'desc']
];
}
public function sortKey()
{
return [
'type' => self::STRING_TYPE,
'location' => self::QUERY,
'description' => "Sorts by one or more sets of attribute and sort direction combinations.",
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace OpenCloud\Common\Api;
/**
* All classes which implement this interface are a data representation of a remote OpenCloud API.
* They do not execute functionality, but instead return data for each API operation for other parts
* of the SDK to use. Usually, the data is injected into {@see OpenCloud\Common\Api\Operation} objects.
* The operation is then serialized into a {@see GuzzleHttp\Message\Request} and sent to the API.
*
* The reason for storing all the API-specific data is to decouple service information from client
* HTTP functionality. Too often it is mixed all across different layers, leading to duplication and
* no separation of concerns. The choice was made for storage in PHP classes, rather than YAML or JSON
* syntax, due to performance concerns.
*
* @package OpenCloud\Common\Api
*/
interface ApiInterface
{
}

View file

@ -0,0 +1,136 @@
<?php
namespace OpenCloud\Common\Api;
use GuzzleHttp\Utils;
/**
* This class represents an OpenCloud API operation. It encapsulates most aspects of the REST operation: its HTTP
* method, the URL path, its top-level JSON key, and all of its {@see Parameter} objects.
*
* An operation not only represents a remote operation, but it also provides the mechanism for executing it
* over HTTP. To do this, it uses a {@see ClientInterface} that allows a {@see GuzzleHttp\Message\Request}
* to be created from the user values provided. Once this request is assembled, it is then sent to the
* remote API and the response is returned to whoever first invoked the Operation class.
*
* @package OpenCloud\Common\Api
*/
class Operation
{
/** @var string The HTTP method */
private $method;
/** @var string The URL path */
private $path;
/** @var string The top-level JSON key */
private $jsonKey;
/** @var []Parameter The parameters of this operation */
private $params;
/**
* @param array $definition The data definition (in array form) that will populate this
* operation. Usually this is retrieved from an {@see ApiInterface}
* object method.
*/
public function __construct(array $definition)
{
$this->method = $definition['method'];
$this->path = $definition['path'];
if (isset($definition['jsonKey'])) {
$this->jsonKey = $definition['jsonKey'];
}
$this->params = self::toParamArray($definition['params']);
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* Indicates whether this operation supports a parameter.
*
* @param $key The name of a parameter
*
* @return bool
*/
public function hasParam($key)
{
return isset($this->params[$key]);
}
/**
* @param $name
*
* @return Parameter
*/
public function getParam($name)
{
return isset($this->params[$name]) ? $this->params[$name] : null;
}
/**
* @return string
*/
public function getJsonKey()
{
return $this->jsonKey;
}
/**
* A convenience method that will take a generic array of data and convert it into an array of
* {@see Parameter} objects.
*
* @param array $data A generic data array
*
* @return array
*/
public static function toParamArray(array $data)
{
$params = [];
foreach ($data as $name => $param) {
$params[$name] = new Parameter($param + ['name' => $name]);
}
return $params;
}
/**
* This method will validate all of the user-provided values and throw an exception if any
* failures are detected. This is useful for basic sanity-checking before a request is
* serialized and sent to the API.
*
* @param array $userValues The user-defined values
*
* @return bool TRUE if validation passes
* @throws \Exception If validate fails
*/
public function validate(array $userValues)
{
foreach ($this->params as $paramName => $param) {
if (array_key_exists($paramName, $userValues)) {
$param->validate($userValues[$paramName]);
} elseif ($param->isRequired()) {
throw new \Exception(sprintf('"%s" is a required option, but it was not provided', $paramName));
}
}
return true;
}
}

View file

@ -0,0 +1,173 @@
<?php
namespace OpenCloud\Common\Api;
use function GuzzleHttp\uri_template;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\Promise;
use OpenCloud\Common\Resource\ResourceInterface;
use OpenCloud\Common\Transport\RequestSerializer;
use Psr\Http\Message\ResponseInterface;
/**
* {@inheritDoc}
*/
abstract class Operator implements OperatorInterface
{
/** @var ClientInterface */
protected $client;
/** @var ApiInterface */
protected $api;
/**
* {@inheritDoc}
*/
public function __construct(ClientInterface $client, ApiInterface $api)
{
$this->client = $client;
$this->api = $api;
}
/**
* Magic method for dictating how objects are rendered when var_dump is called.
* For the benefit of users, extremely verbose and heavy properties (such as HTTP clients) are
* removed to provide easier access to normal state, such as resource attributes.
*
* @codeCoverageIgnore
* @return array
*/
public function __debugInfo()
{
$excludedVars = ['client', 'errorBuilder', 'api'];
$output = [];
foreach (get_object_vars($this) as $key => $val) {
if (!in_array($key, $excludedVars)) {
$output[$key] = $val;
}
}
return $output;
}
/**
* Retrieves a populated Operation according to the definition and values provided. A
* HTTP client is also injected into the object to allow it to communicate with the remote API.
*
* @param array $definition The data that dictates how the operation works
*
* @return Operation
*/
public function getOperation(array $definition)
{
return new Operation($definition);
}
protected function sendRequest(Operation $operation, array $userValues = [], $async = false)
{
$operation->validate($userValues);
$options = (new RequestSerializer)->serializeOptions($operation, $userValues);
$method = $async ? 'requestAsync' : 'request';
$uri = uri_template($operation->getPath(), $userValues);
return $this->client->$method($operation->getMethod(), $uri, $options);
}
/**
* {@inheritDoc}
*/
public function execute(array $definition, array $userValues = [])
{
return $this->sendRequest($this->getOperation($definition), $userValues);
}
/**
* {@inheritDoc}
*/
public function executeAsync(array $definition, array $userValues = [])
{
return $this->sendRequest($this->getOperation($definition), $userValues, true);
}
/**
* {@inheritDoc}
*/
public function model($class, $data = null)
{
$model = new $class($this->client, $this->api);
// @codeCoverageIgnoreStart
if (!$model instanceof ResourceInterface) {
throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class));
}
// @codeCoverageIgnoreEnd
if ($data instanceof ResponseInterface) {
$model->populateFromResponse($data);
} elseif (is_array($data)) {
$model->populateFromArray($data);
}
return $model;
}
/**
* Will create a new instance of this class with the current HTTP client and API injected in. This
* is useful when enumerating over a collection since multiple copies of the same resource class
* are needed.
*
* @return static
*/
public function newInstance()
{
return new static($this->client, $this->api);
}
/**
* @return \GuzzleHttp\Psr7\Uri
*/
protected function getHttpBaseUrl()
{
return $this->client->getConfig('base_uri');
}
/**
* Magic method which intercepts async calls, finds the sequential version, and wraps it in a
* {@see Promise} object. In order for this to happen, the called methods need to be in the
* following format: `createAsync`, where `create` is the sequential method being wrapped.
*
* @param $methodName The name of the method being invoked.
* @param $args The arguments to be passed to the sequential method.
*
* @throws \RuntimeException If method does not exist
*
* @return Promise
*/
public function __call($methodName, $args)
{
$e = function ($name) {
return new \RuntimeException(sprintf('%s::%s is not defined', get_class($this), $name));
};
if (substr($methodName, -5) === 'Async') {
$realMethod = substr($methodName, 0, -5);
if (!method_exists($this, $realMethod)) {
throw $e($realMethod);
}
$promise = new Promise(
function () use (&$promise, $realMethod, $args) {
$value = call_user_func_array([$this, $realMethod], $args);
$promise->resolve($value);
}
);
return $promise;
}
throw $e($methodName);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace OpenCloud\Common\Api;
use GuzzleHttp\ClientInterface;
/**
* An operator is any resource or service that can invoke and send REST operations. In other words, it
* is any class that can send requests and receive responses with a HTTP client. To do this
* it needs two things: a {@see ClientInterface} for handling HTTP transactions and an {@see ApiInterface}
* for handling how operations are created.
*
* @package OpenCloud\Common\Api
*/
interface OperatorInterface
{
/**
* @param ClientInterface $client The HTTP client responsible for handling HTTP transactions
* @param ApiInterface $api The data API class that dictates how REST operations are structured
*/
public function __construct(ClientInterface $client, ApiInterface $api);
/**
* A convenience method that assembles an operation and sends it to the remote API
*
* @param array $definition The data that dictates how the operation works
* @param array $userValues The user-defined values that populate the request
*
* @return \Psr\Http\Message\ResponseInterface
*/
public function execute(array $definition, array $userValues = []);
/**
* A convenience method that assembles an operation and asynchronously sends it to the remote API
*
* @param array $definition The data that dictates how the operation works
* @param array $userValues The user-defined values that populate the request
*
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function executeAsync(array $definition, array $userValues = []);
/**
* @param string $name The name of the model class.
* @param mixed $data Either a {@see ResponseInterface} or data array that will populate the newly
* created model class.
*
* @return \OpenCloud\Common\Resource\ResourceInterface
*/
public function model($name, $data = null);
}

View file

@ -0,0 +1,389 @@
<?php
namespace OpenCloud\Common\Api;
use OpenCloud\Common\HydratorStrategyTrait;
/**
* Represents an individual request parameter in a RESTful operation. A parameter can take on many forms:
* in a URL path, in a URL query, in a JSON body, and in a HTTP header. It is worth documenting brifly each
* variety of parameter:
*
* * Header parameters are those which populate a HTTP header in a request. Header parameters can have
* aliases; for example, a user-facing name of "Foo" can be sent over the wire as "X-Foo_Bar", as defined
* by ``sentAs``. Prefixes can also be used.
*
* * Query parameters are those which populate a URL query parameter. The value is therefore usually
* confined to a string.
*
* * JSON parameters are those which populate a JSON request body. These are the most complex variety
* of Parameter, since there are so many different ways a JSON document can be constructed. The SDK
* supports deep-nesting according to a XPath syntax; for more information, see {@see \OpenCloud\Common\JsonPath}.
* Nested object and array properties are also supported since JSON is a recursive data type. What
* this means is that a Parameter can have an assortment of child Parameters, one for each object
* property or array element.
*
* * Raw parameters are those which populate a non-JSON request body. This is typically used for
* uploading payloads (such as Swift object data) to a remote API.
*
* * Path parameters are those which populate a URL path. They are serialized according to URL
* placeholders.
*
* @package OpenCloud\Common\Api
*/
class Parameter
{
use HydratorStrategyTrait;
const DEFAULT_LOCATION = 'json';
/**
* The human-friendly name of the parameter. This is what the user will input.
*
* @var string
*/
private $name;
/**
* The alias for this parameter. Although the user will always interact with the human-friendly $name property,
* the $sentAs is what's used over the wire.
*
* @var string
*/
private $sentAs;
/**
* For array parameters (for example, an array of security group names when creating a server), each array element
* will need to adhere to a common schema. For the aforementioned example, each element will need to be a string.
* For more complicated parameters, you might be validated an array of complicated objects.
*
* @var Parameter
*/
private $itemSchema;
/**
* For object parameters, each property will need to adhere to a specific schema. For every property in the
* object, it has its own schema - meaning that this property is a hash of name/schema pairs.
*
* The *only* exception to this rule is for metadata parameters, which are arbitrary key/value pairs. Since it does
* not make sense to have a schema for each metadata key, a common schema is use for every one. So instead of this
* property being a hash of schemas, it is a single Parameter object instead. This single Parameter schema will
* then be applied to each metadata key provided.
*
* @var []Parameter|Parameter
*/
private $properties;
/**
* The value's PHP type which this parameter represents; either "string", "bool", "object", "array", "NULL".
*
* @var string
*/
private $type;
/**
* Indicates whether this parameter requires a value from the user.
*
* @var bool
*/
private $required;
/**
* The location in the HTTP request where this parameter will populate; either "header", "url", "query", "raw" or
* "json".
*
* @var string
*/
private $location;
/**
* Relevant to "json" location parameters only. This property allows for deep nesting through the use of
* {@see OpenCloud\Common\JsonPath}.
*
* @var string
*/
private $path;
/**
* Allows for the prefixing of parameter names.
*
* @var string
*/
private $prefix;
/**
* The enum values for which this param is restricted.
*
* @var array
*/
private $enum;
/**
* @param array $data
*/
public function __construct(array $data)
{
$this->hydrate($data);
$this->required = (bool)$this->required;
$this->stockLocation($data);
$this->stockItemSchema($data);
$this->stockProperties($data);
}
private function stockLocation(array $data)
{
$this->location = isset($data['location']) ? $data['location'] : self::DEFAULT_LOCATION;
if (!AbstractParams::isSupportedLocation($this->location)) {
throw new \RuntimeException(sprintf("%s is not a permitted location", $this->location));
}
}
private function stockItemSchema(array $data)
{
if (isset($data['items'])) {
$this->itemSchema = new Parameter($data['items']);
}
}
private function stockProperties(array $data)
{
if (isset($data['properties'])) {
if (stripos($this->name, 'metadata') !== false) {
$this->properties = new Parameter($data['properties']);
} else {
foreach ($data['properties'] as $name => $property) {
$this->properties[$name] = new Parameter($property + ['name' => $name]);
}
}
}
}
/**
* Retrieve the name that will be used over the wire.
*
* @return string
*/
public function getName()
{
return $this->sentAs ?: $this->name;
}
/**
* Indicates whether the user must provide a value for this parameter.
*
* @return bool
*/
public function isRequired()
{
return $this->required === true;
}
/**
* Validates a given user value and checks whether it passes basic sanity checking, such as types.
*
* @param $userValues The value provided by the user
*
* @return bool TRUE if the validation passes
* @throws \Exception If validation fails
*/
public function validate($userValues)
{
$this->validateEnums($userValues);
$this->validateType($userValues);
if ($this->isArray()) {
$this->validateArray($userValues);
} elseif ($this->isObject()) {
$this->validateObject($userValues);
}
return true;
}
private function validateEnums($userValues)
{
if (!empty($this->enum) && $this->type == 'string' && !in_array($userValues, $this->enum)) {
throw new \Exception(sprintf(
'The only permitted values are %s. You provided %s', implode(', ', $this->enum), print_r($userValues, true)
));
}
}
private function validateType($userValues)
{
if (!$this->hasCorrectType($userValues)) {
throw new \Exception(sprintf(
'The key provided "%s" has the wrong value type. You provided %s (%s) but was expecting %s',
$this->name, print_r($userValues, true), gettype($userValues), $this->type
));
}
}
private function validateArray($userValues)
{
foreach ($userValues as $userValue) {
$this->itemSchema->validate($userValue);
}
}
private function validateObject($userValues)
{
foreach ($userValues as $key => $userValue) {
$property = $this->getNestedProperty($key);
$property->validate($userValue);
}
}
/**
* Internal method which retrieves a nested property for object parameters.
*
* @param $key The name of the child parameter
*
* @returns Parameter
* @throws \Exception
*/
private function getNestedProperty($key)
{
if (stripos($this->name, 'metadata') !== false && $this->properties instanceof Parameter) {
return $this->properties;
} elseif (isset($this->properties[$key])) {
return $this->properties[$key];
} else {
throw new \Exception(sprintf('The key provided "%s" is not defined', $key));
}
}
/**
* Internal method which indicates whether the user value is of the same type as the one expected
* by this parameter.
*
* @param $userValue The value being checked
*
* @return bool
*/
private function hasCorrectType($userValue)
{
// Helper fn to see whether an array is associative (i.e. a JSON object)
$isAssociative = function ($value) {
return is_array($value) && array_keys($value) !== range(0, count($value) - 1);
};
// For params defined as objects, we'll let the user get away with
// passing in an associative array - since it's effectively a hash
if ($this->type == 'object' && $isAssociative($userValue)) {
return true;
}
if (class_exists($this->type) || interface_exists($this->type)) {
return is_a($userValue, $this->type);
}
if (!$this->type) {
return true;
}
return gettype($userValue) == $this->type;
}
/**
* Indicates whether this parameter represents an array type
*
* @return bool
*/
public function isArray()
{
return $this->type == 'array' && $this->itemSchema instanceof Parameter;
}
/**
* Indicates whether this parameter represents an object type
*
* @return bool
*/
public function isObject()
{
return $this->type == 'object' && !empty($this->properties);
}
public function getLocation()
{
return $this->location;
}
/**
* Verifies whether the given location matches the parameter's location.
*
* @param $value
*
* @return bool
*/
public function hasLocation($value)
{
return $this->location == $value;
}
/**
* Retrieves the parameter's path.
*
* @return string|null
*/
public function getPath()
{
return $this->path;
}
/**
* Retrieves the common schema that an array parameter applies to all its child elements.
*
* @return Parameter
*/
public function getItemSchema()
{
return $this->itemSchema;
}
/**
* Sets the name of the parameter to a new value
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Retrieves the child parameter for an object parameter.
*
* @param string $name The name of the child property
*
* @return null|Parameter
*/
public function getProperty($name)
{
if ($this->properties instanceof Parameter) {
$this->properties->setName($name);
return $this->properties;
}
return isset($this->properties[$name]) ? $this->properties[$name] : null;
}
/**
* Retrieves the prefix for a parameter, if any.
*
* @return string|null
*/
public function getPrefix()
{
return $this->prefix;
}
public function getPrefixedName()
{
return $this->prefix . $this->getName();
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace OpenCloud\Common;
/**
* Encapsulates common logic for classes which implement the SPL \ArrayAccess interface.
*
* @package OpenCloud\Common
*/
trait ArrayAccessTrait
{
/**
* The internal state that this object represents
*
* @var array
*/
private $internalState = [];
/**
* Sets an internal key with a value.
*
* @param string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->internalState[] = $value;
} else {
$this->internalState[$offset] = $value;
}
}
/**
* Checks whether an internal key exists.
*
* @param string $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->internalState[$offset]);
}
/**
* Unsets an internal key.
*
* @param string $offset
*/
public function offsetUnset($offset)
{
unset($this->internalState[$offset]);
}
/**
* Retrieves an internal key.
*
* @param string $offset
*
* @return mixed|null
*/
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->internalState[$offset] : null;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace OpenCloud\Common\Auth;
use function GuzzleHttp\Psr7\modify_request;
use Psr\Http\Message\RequestInterface;
/**
* This class is responsible for three tasks:
*
* 1. performing the initial authentication for OpenCloud services
* 2. populating the ``X-Auth-Token`` header for every HTTP request
* 3. checking the token expiry before each request, and re-authenticating if necessary
*/
class AuthHandler
{
private $nextHandler;
private $tokenGenerator;
private $token;
/**
* @param callable $nextHandler
* @param callable $tokenGenerator
*/
public function __construct(callable $nextHandler, callable $tokenGenerator, Token $token = null)
{
$this->nextHandler = $nextHandler;
$this->tokenGenerator = $tokenGenerator;
$this->token = $token;
}
/**
* This method is invoked before every HTTP request is sent to the API. When this happens, it
* checks to see whether a token is set and valid, and then sets the ``X-Auth-Token`` header
* for the HTTP request before letting it continue on its merry way.
*
* @param RequestInterface $request
* @param array $options
*
* @return mixed|void
*/
public function __invoke(RequestInterface $request, array $options)
{
$fn = $this->nextHandler;
if ($this->shouldIgnore($request)) {
return $fn($request, $options);
}
if (!$this->token || $this->token->hasExpired()) {
$this->token = call_user_func($this->tokenGenerator);
}
$modify = ['set_headers' => ['X-Auth-Token' => $this->token->getId()]];
return $fn(modify_request($request, $modify), $options);
}
/**
* Internal method which prevents infinite recursion. For certain requests, like the initial
* auth call itself, we do NOT want to send a token.
*
* @param RequestInterface $request
*
* @return bool
*/
private function shouldIgnore(RequestInterface $request)
{
return strpos((string) $request->getUri(), 'tokens') !== false && $request->getMethod() == 'POST';
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace OpenCloud\Common\Auth;
interface Catalog
{
/**
* Attempts to retrieve the base URL for a service from the catalog according to the arguments provided.
*
* @param string $name The name of the service as it appears in the catalog
* @param string $type The type of the service as it appears in the catalog
* @param string $region The region of the service as it appears in the catalog
* @param string $urlType The URL type of the service as it appears in the catalog
*
* @throws \RuntimeException If no endpoint is matched
*
* @returns string
*/
public function getServiceUrl($name, $type, $region, $urlType);
}

View file

@ -0,0 +1,13 @@
<?php
namespace OpenCloud\Common\Auth;
interface IdentityService
{
/**
* Authenticates and retrieves back a token and catalog.
*
* @return array The FIRST key is {@see Token} instance, the SECOND key is a {@see Catalog} instance
*/
public function authenticate(array $options);
}

View file

@ -0,0 +1,15 @@
<?php
namespace OpenCloud\Common\Auth;
interface Token
{
public function getId();
/**
* Indicates whether the token has expired or not.
*
* @return bool TRUE if the token has expired, FALSE if it is still valid
*/
public function hasExpired();
}

View file

@ -0,0 +1,40 @@
<?php
namespace OpenCloud\Common\Error;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Represents a HTTP-specific error, caused by 4xx or 5xx response statuses.
*
* @package OpenCloud\Common\Error
*/
class BadResponseError extends BaseError
{
/** @var RequestInterface */
private $request;
/** @var ResponseInterface */
private $response;
public function setRequest(RequestInterface $request)
{
$this->request = $request;
}
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
public function getRequest()
{
return $this->request;
}
public function getResponse()
{
return $this->response;
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace OpenCloud\Common\Error;
/**
* Base error class.
*
* @package OpenCloud\Common\Error
*/
class BaseError extends \Exception
{
}

View file

@ -0,0 +1,179 @@
<?php
namespace OpenCloud\Common\Error;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class responsible for building meaningful exceptions. For HTTP problems, it produces a {@see HttpError}
* exception, and supplies a error message with reasonable defaults. For user input problems, it produces a
* {@see UserInputError} exception. For both, the problem is described, a potential solution is offered and
* a link to further information is included.
*
* @package OpenCloud\Common\Error
*/
class Builder
{
/**
* The default domain to use for further link documentation.
*
* @var string
*/
private $docDomain = 'http://docs.php-opencloud.com/en/latest/';
/**
* The HTTP client required to validate the further links.
*
* @var ClientInterface
*/
private $client;
/**
* @param ClientInterface $client
*/
public function __construct(ClientInterface $client = null)
{
$this->client = $client ?: new Client();
}
/**
* Internal method used when outputting headers in the error description.
*
* @param $name
*
* @return string
*/
private function header($name)
{
return sprintf("%s\n%s\n", $name, str_repeat('~', strlen($name)));
}
/**
* Before outputting custom links, it is validated to ensure that the user is not
* directed off to a broken link. If a 404 is detected, it is hidden.
*
* @param $link The proposed link
*
* @return bool
*/
private function linkIsValid($link)
{
$link = $this->docDomain . $link;
try {
return $this->client->request('HEAD', $link)->getStatusCode() < 400;
} catch (ClientException $e) {
return false;
}
}
/**
* @param MessageInterface $message
*
* @codeCoverageIgnore
* @return string
*/
public function str(MessageInterface $message)
{
if ($message instanceof RequestInterface) {
$msg = trim($message->getMethod() . ' '
. $message->getRequestTarget())
. ' HTTP/' . $message->getProtocolVersion();
if (!$message->hasHeader('host')) {
$msg .= "\r\nHost: " . $message->getUri()->getHost();
}
} elseif ($message instanceof ResponseInterface) {
$msg = 'HTTP/' . $message->getProtocolVersion() . ' '
. $message->getStatusCode() . ' '
. $message->getReasonPhrase();
}
foreach ($message->getHeaders() as $name => $values) {
$msg .= "\r\n{$name}: " . implode(', ', $values);
}
if ($message->getBody()->getSize() < ini_get('memory_limit')) {
$msg .= "\r\n\r\n" . $message->getBody();
}
return $msg;
}
/**
* Helper method responsible for constructing and returning {@see BadResponseError} exceptions.
*
* @param RequestInterface $request The faulty request
* @param ResponseInterface $response The error-filled response
*
* @return BadResponseError
*/
public function httpError(RequestInterface $request, ResponseInterface $response)
{
$message = $this->header('HTTP Error');
$message .= sprintf("The remote server returned a \"%d %s\" error for the following transaction:\n\n",
$response->getStatusCode(), $response->getReasonPhrase());
$message .= $this->header('Request');
$message .= trim($this->str($request)) . PHP_EOL . PHP_EOL;
$message .= $this->header('Response');
$message .= trim($this->str($response)) . PHP_EOL . PHP_EOL;
$message .= $this->header('Further information');
$message .= $this->getStatusCodeMessage($response->getStatusCode());
$message .= "Visit http://docs.php-opencloud.com/en/latest/http-codes for more information about debugging "
. "HTTP status codes, or file a support issue on https://github.com/php-opencloud/openstack/issues.";
$e = new BadResponseError($message);
$e->setRequest($request);
$e->setResponse($response);
return $e;
}
private function getStatusCodeMessage($statusCode)
{
$errors = [
400 => 'Please ensure that your input values are valid and well-formed. ',
401 => 'Please ensure that your authentication credentials are valid. ',
404 => "Please ensure that the resource you're trying to access actually exists. ",
500 => 'Please try this operation again once you know the remote server is operational. ',
];
return isset($errors[$statusCode]) ? $errors[$statusCode] : '';
}
/**
* Helper method responsible for constructing and returning {@see UserInputError} exceptions.
*
* @param string $expectedType The type that was expected from the user
* @param mixed $userValue The incorrect value the user actually provided
* @param string|null $furtherLink A link to further information if necessary (optional).
*
* @return UserInputError
*/
public function userInputError($expectedType, $userValue, $furtherLink = null)
{
$message = $this->header('User Input Error');
$message .= sprintf("%s was expected, but the following value was passed in:\n\n%s\n",
$expectedType, print_r($userValue, true));
$message .= "Please ensure that the value adheres to the expectation above. ";
if ($furtherLink && $this->linkIsValid($furtherLink)) {
$message .= sprintf("Visit %s for more information about input arguments. ", $this->docDomain . $furtherLink);
}
$message .= 'If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.';
return new UserInputError($message);
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace OpenCloud\Common\Error;
/**
* Error to indicate functionality which has not been implemented yet.
*
* @package OpenCloud\Common\Error
*/
class NotImplementedError extends BaseError
{
}

View file

@ -0,0 +1,12 @@
<?php
namespace OpenCloud\Common\Error;
/**
* Represents a user input error, caused by an incorrect type or malformed value.
*
* @package OpenCloud\Common\Error
*/
class UserInputError extends BaseError
{
}

View file

@ -0,0 +1,35 @@
<?php
namespace OpenCloud\Common;
/**
* Represents common functionality for populating, or "hydrating", an object with arbitrary data.
*
* @package OpenCloud\Common
*/
trait HydratorStrategyTrait
{
/**
* Hydrates an object with set data
*
* @param array $data The data to set
* @param array $aliases Any aliases
*/
private function hydrate(array $data, array $aliases = [])
{
foreach ($data as $key => $val) {
$key = isset($aliases[$key]) ? $aliases[$key] : $key;
if (property_exists($this, $key)) {
$this->$key = $val;
}
}
}
private function set($key, $property, array $data, callable $fn = null)
{
if (isset($data[$key]) && property_exists($this, $property)) {
$value = $fn ? call_user_func($fn, $data[$key]) : $data[$key];
$this->$property = $value;
}
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace OpenCloud\Common;
/**
* This class allows arbitrary data structures to be inserted into, and extracted from, deep arrays
* and JSON-serialized strings. Say, for example, that you have this array as an input:
*
* <pre><code>['foo' => ['bar' => ['baz' => 'some_value']]]</code></pre>
*
* and you wanted to insert or extract an element. Usually, you would use:
*
* <pre><code>$array['foo']['bar']['baz'] = 'new_value';</code></pre>
*
* but sometimes you do not have access to the variable - so a string representation is needed. Using
* XPath-like syntax, this class allows you to do this:
*
* <pre><code>$jsonPath = new JsonPath($array);
* $jsonPath->set('foo.bar.baz', 'new_value');
* $val = $jsonPath->get('foo.bar.baz');
* </code></pre>
*
* @package OpenCloud\Common
*/
class JsonPath
{
/** @var array */
private $jsonStructure;
/**
* @param $structure The initial data structure to extract from and insert into. Typically this will be a
* multidimensional associative array; but well-formed JSON strings are also acceptable.
*/
public function __construct($structure)
{
$this->jsonStructure = is_string($structure) ? json_decode($structure, true) : $structure;
}
/**
* Set a node in the structure
*
* @param $path The XPath to use
* @param $value The new value of the node
*/
public function set($path, $value)
{
$this->jsonStructure = $this->setPath($path, $value, $this->jsonStructure);
}
/**
* Internal method for recursive calls.
*
* @param $path
* @param $value
* @param $json
* @return mixed
*/
private function setPath($path, $value, $json)
{
$nodes = explode('.', $path);
$point = array_shift($nodes);
if (!isset($json[$point])) {
$json[$point] = [];
}
if (!empty($nodes)) {
$json[$point] = $this->setPath(implode('.', $nodes), $value, $json[$point]);
} else {
$json[$point] = $value;
}
return $json;
}
/**
* Return the updated structure.
*
* @return mixed
*/
public function getStructure()
{
return $this->jsonStructure;
}
/**
* Get a path's value. If no path can be matched, NULL is returned.
*
* @param $path
* @return mixed|null
*/
public function get($path)
{
return $this->getPath($path, $this->jsonStructure);
}
/**
* Internal method for recursion.
*
* @param $path
* @param $json
* @return null
*/
private function getPath($path, $json)
{
$nodes = explode('.', $path);
$point = array_shift($nodes);
if (!isset($json[$point])) {
return null;
}
if (empty($nodes)) {
return $json[$point];
} else {
return $this->getPath(implode('.', $nodes), $json[$point]);
}
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace OpenCloud\Common\JsonSchema;
class JsonPatch
{
const OP_ADD = 'add';
const OP_REPLACE = 'replace';
const OP_REMOVE = 'remove';
public static function diff($src, $dest)
{
return (new static)->makeDiff($src, $dest);
}
public function makeDiff($srcStruct, $desStruct, $path = '')
{
$changes = [];
if (is_object($srcStruct)) {
$changes = $this->handleObject($srcStruct, $desStruct, $path);
} elseif (is_array($srcStruct)) {
$changes = $this->handleArray($srcStruct, $desStruct, $path);
} elseif ($srcStruct != $desStruct) {
$changes[] = $this->makePatch(self::OP_REPLACE, $path, $desStruct);
}
return $changes;
}
protected function handleArray($srcStruct, $desStruct, $path)
{
$changes = [];
if ($diff = $this->arrayDiff($desStruct, $srcStruct)) {
foreach ($diff as $key => $val) {
if (is_object($val)) {
$changes = array_merge($changes, $this->makeDiff($srcStruct[$key], $val, $this->path($path, $key)));
} else {
$op = array_key_exists($key, $srcStruct) && !in_array($srcStruct[$key], $desStruct, true)
? self::OP_REPLACE : self::OP_ADD;
$changes[] = $this->makePatch($op, $this->path($path, $key), $val);
}
}
} elseif ($srcStruct != $desStruct) {
foreach ($srcStruct as $key => $val) {
if (!in_array($val, $desStruct, true)) {
$changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key));
}
}
}
return $changes;
}
protected function handleObject($srcStruct, $desStruct, $path)
{
$changes = [];
if ($this->shouldPartiallyReplace($srcStruct, $desStruct)) {
foreach ($desStruct as $key => $val) {
if (!property_exists($srcStruct, $key)) {
$changes[] = $this->makePatch(self::OP_ADD, $this->path($path, $key), $val);
} elseif ($srcStruct->$key != $val) {
$changes = array_merge($changes, $this->makeDiff($srcStruct->$key, $val, $this->path($path, $key)));
}
}
} elseif ($this->shouldPartiallyReplace($desStruct, $srcStruct)) {
foreach ($srcStruct as $key => $val) {
if (!property_exists($desStruct, $key)) {
$changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key));
}
}
}
return $changes;
}
protected function shouldPartiallyReplace($o1, $o2)
{
return count(array_diff_key((array) $o1, (array) $o2)) < count($o1);
}
protected function arrayDiff(array $a1, array $a2)
{
$result = [];
foreach ($a1 as $key => $val) {
if (!in_array($val, $a2, true)) {
$result[$key] = $val;
}
}
return $result;
}
protected function path($root, $path)
{
if ($path === '_empty_') {
$path = '';
}
return rtrim($root, '/') . '/' . ltrim($path, '/');
}
protected function makePatch($op, $path, $val = null)
{
switch ($op) {
default:
return ['op' => $op, 'path' => $path, 'value' => $val];
case self::OP_REMOVE:
return ['op' => $op, 'path' => $path];
}
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace OpenCloud\Common\JsonSchema;
use JsonSchema\Validator;
class Schema
{
private $body;
private $validator;
public function __construct($body, Validator $validator = null)
{
$this->body = (object) $body;
$this->validator = $validator ?: new Validator();
}
public function getPropertyPaths()
{
$paths = [];
foreach ($this->body->properties as $propertyName => $property) {
$paths[] = sprintf("/%s", $propertyName);
}
return $paths;
}
public function normalizeObject($subject, array $aliases)
{
$out = new \stdClass;
foreach ($this->body->properties as $propertyName => $property) {
$name = isset($aliases[$propertyName]) ? $aliases[$propertyName] : $propertyName;
if (isset($property->readOnly) && $property->readOnly === true) {
continue;
} elseif (property_exists($subject, $name)) {
$out->$propertyName = $subject->$name;
} elseif (property_exists($subject, $propertyName)) {
$out->$propertyName = $subject->$propertyName;
}
}
return $out;
}
public function validate($data)
{
$this->validator->check($data, $this->body);
}
public function isValid()
{
return $this->validator->isValid();
}
public function getErrors()
{
return $this->validator->getErrors();
}
public function getErrorString()
{
$msg = "Provided values do not validate. Errors:\n";
foreach ($this->getErrors() as $error) {
$msg .= sprintf("[%s] %s\n", $error['property'], $error['message']);
}
return $msg;
}
}

View file

@ -0,0 +1,243 @@
<?php
namespace OpenCloud\Common\Resource;
use OpenCloud\Common\Api\Operator;
use OpenCloud\Common\Transport\Utils;
use Psr\Http\Message\ResponseInterface;
/**
* Represents a top-level abstraction of a remote API resource. Usually a resource represents a discrete
* entity such as a Server, Container, Load Balancer. Apart from a representation of state, a resource can
* also execute RESTFul operations on itself (updating, deleting, listing) or on other models.
*
* @package OpenCloud\Common\Resource
*/
abstract class AbstractResource extends Operator implements ResourceInterface
{
const DEFAULT_MARKER_KEY = 'id';
/**
* The JSON key that indicates how the API nests singular resources. For example, when
* performing a GET, it could respond with ``{"server": {"id": "12345"}}``. In this case,
* "server" is the resource key, since the essential state of the server is nested inside.
*
* @var string
*/
protected $resourceKey;
/**
* The key that indicates how the API nests resource collections. For example, when
* performing a GET, it could respond with ``{"servers": [{}, {}]}``. In this case, "servers"
* is the resources key, since the array of servers is nested inside.
*
* @var string
*/
protected $resourcesKey;
/**
* Indicates which attribute of the current resource should be used for pagination markers.
*
* @var string
*/
protected $markerKey;
/**
* An array of aliases that will be checked when the resource is being populated. For example,
*
* 'FOO_BAR' => 'fooBar'
*
* will extract FOO_BAR from the response, and save it as 'fooBar' in the resource.
*
* @var array
*/
protected $aliases = [];
/**
* Populates the current resource from a response object.
*
* @param ResponseInterface $response
*
* @return $this|ResourceInterface
*/
public function populateFromResponse(ResponseInterface $response)
{
if (strpos($response->getHeaderLine('Content-Type'), 'application/json') === 0) {
$json = Utils::jsonDecode($response);
if (!empty($json)) {
$this->populateFromArray(Utils::flattenJson($json, $this->resourceKey));
}
}
return $this;
}
/**
* Populates the current resource from a data array.
*
* @param array $array
*
* @return mixed|void
*/
public function populateFromArray(array $array)
{
$reflClass = new \ReflectionClass($this);
foreach ($array as $key => $val) {
$propertyName = isset($this->aliases[$key]) ? $this->aliases[$key] : $key;
if (property_exists($this, $propertyName)) {
if ($type = $this->extractTypeFromDocBlock($reflClass, $propertyName)) {
$val = $this->parseDocBlockValue($type, $val);
}
$this->$propertyName = $val;
}
}
}
private function parseDocBlockValue($type, $val)
{
if (strpos($type, '[]') === 0 && is_array($val)) {
$array = [];
foreach ($val as $subVal) {
$array[] = $this->model($this->normalizeModelClass(substr($type, 2)), $subVal);
}
$val = $array;
} elseif (strcasecmp($type, '\datetimeimmutable') === 0) {
$val = new \DateTimeImmutable($val);
} elseif ($this->isNotNativeType($type)) {
$val = $this->model($this->normalizeModelClass($type), $val);
}
return $val;
}
private function isNotNativeType($type)
{
return !in_array($type, [
'string', 'bool', 'boolean', 'double', 'null', 'array', 'object', 'int', 'integer', 'float', 'numeric',
'mixed'
]);
}
private function normalizeModelClass($class)
{
if (strpos($class, '\\') === false) {
$currentNamespace = (new \ReflectionClass($this))->getNamespaceName();
$class = sprintf("%s\\%s", $currentNamespace, $class);
}
return $class;
}
private function extractTypeFromDocBlock(\ReflectionClass $reflClass, $propertyName)
{
$docComment = $reflClass->getProperty($propertyName)->getDocComment();
if (!$docComment) {
return false;
}
$matches = [];
preg_match('#@var ((\[\])?[\w|\\\]+)#', $docComment, $matches);
return isset($matches[1]) ? $matches[1] : null;
}
/**
* Internal method which retrieves the values of provided keys.
*
* @param array $keys
*
* @return array
*/
protected function getAttrs(array $keys)
{
$output = [];
foreach ($keys as $key) {
if (property_exists($this, $key) && $this->$key !== null) {
$output[$key] = $this->$key;
}
}
return $output;
}
/**
* @param array $definition
*
* @return mixed
*/
public function executeWithState(array $definition)
{
return $this->execute($definition, $this->getAttrs(array_keys($definition['params'])));
}
private function getResourcesKey()
{
$resourcesKey = $this->resourcesKey;
if (!$resourcesKey) {
$class = substr(static::class, strrpos(static::class, '\\') + 1);
$resourcesKey = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $class)) . 's';
}
return $resourcesKey;
}
/**
* {@inheritDoc}
*/
public function enumerate(array $def, array $userVals = [], callable $mapFn = null)
{
$operation = $this->getOperation($def);
$requestFn = function ($marker) use ($operation, $userVals) {
if ($marker) {
$userVals['marker'] = $marker;
}
return $this->sendRequest($operation, $userVals);
};
$resourceFn = function (array $data) {
$resource = $this->newInstance();
$resource->populateFromArray($data);
return $resource;
};
$opts = [
'limit' => isset($userVals['limit']) ? $userVals['limit'] : null,
'resourcesKey' => $this->getResourcesKey(),
'markerKey' => $this->markerKey,
'mapFn' => $mapFn,
];
$iterator = new Iterator($opts, $requestFn, $resourceFn);
return $iterator();
}
public function extractMultipleInstances(ResponseInterface $response, $key = null)
{
$key = $key ?: $this->getResourcesKey();
$resourcesData = Utils::jsonDecode($response)[$key];
$resources = [];
foreach ($resourcesData as $resourceData) {
$resource = $this->newInstance();
$resource->populateFromArray($resourceData);
$resources[] = $resource;
}
return $resources;
}
protected function getService()
{
$class = static::class;
$service = substr($class, 0, strpos($class, 'Models') - 1) . '\\Service';
return new $service($this->client, $this->api);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace OpenCloud\Common\Resource;
/**
* Represents a resource that can be created.
*
* @package OpenCloud\Common\Resource
*/
interface Creatable
{
/**
* Create a new resource according to the configuration set in the options.
*
* @param array $userOptions
* @return self
*/
public function create(array $userOptions);
}

View file

@ -0,0 +1,18 @@
<?php
namespace OpenCloud\Common\Resource;
/**
* Represents a resource that can be deleted.
*
* @package OpenCloud\Common\Resource
*/
interface Deletable
{
/**
* Permanently delete this resource.
*
* @return void
*/
public function delete();
}

View file

@ -0,0 +1,67 @@
<?php
namespace OpenCloud\Common\Resource;
use Psr\Http\Message\ResponseInterface;
interface HasMetadata
{
/**
* Retrieves the metadata for the resource in the form of an associative array or hash. Each key represents the
* metadata item's name, and each value represents the metadata item's remote value.
*
* @return array
*/
public function getMetadata();
/**
* Merges a set of new values with those which already exist (on the remote API) for a resource. For example, if
* the resource has this metadata already set:
*
* Foo: val1
* Bar: val2
*
* and mergeMetadata(['Foo' => 'val3', 'Baz' => 'val4']); is called, then the resource will have the following
* metadata:
*
* Foo: val3
* Bar: val2
* Baz: val4
*
* You will notice that any metadata items which are not specified in the call are preserved.
*
* @param array $metadata The new metadata items
*
* @return mixed
*/
public function mergeMetadata(array $metadata);
/**
* Replaces all of the existing metadata items for a resource with a new set of values. Any metadata items which
* are not provided in the call are removed from the resource. For example, if the resource has this metadata
* already set:
*
* Foo: val1
* Bar: val2
*
* and resetMetadata(['Foo' => 'val3', 'Baz' => 'val4']); is called, then the resource will have the following
* metadata:
*
* Foo: val3
* Baz: val4
*
* @param array $metadata The new metadata items
*
* @return mixed
*/
public function resetMetadata(array $metadata);
/**
* Extracts metadata from a response object and returns it in the form of an associative array.
*
* @param ResponseInterface $response
*
* @return array
*/
public function parseMetadata(ResponseInterface $response);
}

View file

@ -0,0 +1,124 @@
<?php
namespace OpenCloud\Common\Resource;
use OpenCloud\Common\Error\BadResponseError;
/**
* Contains reusable functionality for resources that have long operations which require waiting in
* order to reach a particular state.
*
* @codeCoverageIgnore
*
* @package OpenCloud\Common\Resource
*/
trait HasWaiterTrait
{
/**
* Provides a blocking operation until the resource has reached a particular state. The method
* will enter a loop, requesting feedback from the remote API until it sends back an appropriate
* status.
*
* @param string $status The state to be reached
* @param int $timeout The maximum timeout. If the total time taken by the waiter has reached
* or exceed this timeout, the blocking operation will immediately cease.
* @param int $sleepPeriod The amount of time to pause between each HTTP request.
*/
public function waitUntil($status, $timeout = 60, $sleepPeriod = 1)
{
$startTime = time();
while (true) {
$this->retrieve();
if ($this->status == $status || $this->shouldHalt($timeout, $startTime)) {
break;
}
sleep($sleepPeriod);
}
}
/**
* Provides a blocking operation until the resource has reached a particular state. The method
* will enter a loop, executing the callback until TRUE is returned. This provides great
* flexibility.
*
* @param callable $fn An anonymous function that will be executed on every iteration. You can
* encapsulate your own logic to determine whether the resource has
* successfully transitioned. When TRUE is returned by the callback,
* the loop will end.
* @param int|bool $timeout The maximum timeout in seconds. If the total time taken by the waiter has reached
* or exceed this timeout, the blocking operation will immediately cease. If FALSE
* is provided, the timeout will never be considered.
* @param int $sleepPeriod The amount of time to pause between each HTTP request.
*/
public function waitWithCallback(callable $fn, $timeout = 60, $sleepPeriod = 1)
{
$startTime = time();
while (true) {
$this->retrieve();
$response = call_user_func_array($fn, [$this]);
if ($response === true || $this->shouldHalt($timeout, $startTime)) {
break;
}
sleep($sleepPeriod);
}
}
/**
* Internal method used to identify whether a timeout has been exceeded.
*
* @param bool|int $timeout
* @param int $startTime
*
* @return bool
*/
private function shouldHalt($timeout, $startTime)
{
if ($timeout === false) {
return false;
}
return time() - $startTime >= $timeout;
}
/**
* Convenience method providing a blocking operation until the resource transitions to an
* ``ACTIVE`` status.
*
* @param int|bool $timeout The maximum timeout in seconds. If the total time taken by the waiter has reached
* or exceed this timeout, the blocking operation will immediately cease. If FALSE
* is provided, the timeout will never be considered.
*/
public function waitUntilActive($timeout = false)
{
$this->waitUntil('ACTIVE', $timeout);
}
public function waitUntilDeleted($timeout = 60, $sleepPeriod = 1)
{
$startTime = time();
while (true) {
try {
$this->retrieve();
} catch (BadResponseError $e) {
if ($e->getResponse()->getStatusCode() === 404) {
break;
}
throw $e;
}
if ($this->shouldHalt($timeout, $startTime)) {
break;
}
sleep($sleepPeriod);
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace OpenCloud\Common\Resource;
use OpenCloud\Common\Transport\Utils;
class Iterator
{
private $requestFn;
private $resourceFn;
private $limit;
private $count;
private $resourcesKey;
private $markerKey;
private $mapFn;
private $currentMarker;
public function __construct(array $options, callable $requestFn, callable $resourceFn)
{
$this->limit = isset($options['limit']) ? $options['limit'] : false;
$this->count = 0;
if (isset($options['resourcesKey'])) {
$this->resourcesKey = $options['resourcesKey'];
}
if (isset($options['markerKey'])) {
$this->markerKey = $options['markerKey'];
}
if (isset($options['mapFn']) && is_callable($options['mapFn'])) {
$this->mapFn = $options['mapFn'];
}
$this->requestFn = $requestFn;
$this->resourceFn = $resourceFn;
}
private function fetchResources()
{
if ($this->shouldNotSendAnotherRequest()) {
return false;
}
$response = call_user_func($this->requestFn, $this->currentMarker);
$json = Utils::flattenJson(Utils::jsonDecode($response), $this->resourcesKey);
if ($response->getStatusCode() === 204 || empty($json)) {
return false;
}
return $json;
}
private function assembleResource(array $data)
{
$resource = call_user_func($this->resourceFn, $data);
// Invoke user-provided fn if provided
if ($this->mapFn) {
call_user_func_array($this->mapFn, [&$resource]);
}
// Update marker if operation supports it
if ($this->markerKey) {
$this->currentMarker = $resource->{$this->markerKey};
}
return $resource;
}
private function totalReached()
{
return $this->limit && $this->count >= $this->limit;
}
private function shouldNotSendAnotherRequest()
{
return $this->totalReached() || ($this->count > 0 && !$this->markerKey);
}
public function __invoke()
{
while ($resources = $this->fetchResources()) {
foreach ($resources as $resourceData) {
if ($this->totalReached()) {
break;
}
$this->count++;
yield $this->assembleResource($resourceData);
}
}
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace OpenCloud\Common\Resource;
/**
* Represents a resource that can be enumerated (listed over).
*
* @package OpenCloud\Common\Resource
*/
interface Listable
{
/**
* This method iterates over a collection of resources. It sends the operation's request to the API,
* parses the response, converts each element into {@see self} and - if pagination is supported - continues
* to send requests until an empty collection is received back.
*
* For paginated collections, it sends subsequent requests according to a marker URL query. The value
* of the marker will depend on the last element returned in the previous response. If a limit is
* provided, the loop will continue up until that point.
*
* @param array $def The operation definition
* @param array $userVals The user values
* @param callable $mapFn An optional callback that will be executed on every resource iteration.
*
* @returns void
*/
public function enumerate(array $def, array $userVals = [], callable $mapFn = null);
}

View file

@ -0,0 +1,29 @@
<?php
namespace OpenCloud\Common\Resource;
use Psr\Http\Message\ResponseInterface;
/**
* Represents an API resource.
*
* @package OpenCloud\Common\Resource
*/
interface ResourceInterface
{
/**
* All models which represent an API resource should be able to be populated
* from a {@see ResponseInterface} object.
*
* @param ResponseInterface $response
*
* @return self
*/
public function populateFromResponse(ResponseInterface $response);
/**
* @param array $data
* @return mixed
*/
public function populateFromArray(array $data);
}

View file

@ -0,0 +1,18 @@
<?php
namespace OpenCloud\Common\Resource;
/**
* A resource that supports a GET or HEAD operation to retrieve more details.
*
* @package OpenCloud\Common\Resource
*/
interface Retrievable
{
/**
* Retrieve details of the current resource from the remote API.
*
* @return void
*/
public function retrieve();
}

View file

@ -0,0 +1,18 @@
<?php
namespace OpenCloud\Common\Resource;
/**
* Represents a resource that can be updated.
*
* @package OpenCloud\Common\Resource
*/
interface Updateable
{
/**
* Update the current resource with the configuration set out in the user options.
*
* @return void
*/
public function update();
}

View file

@ -0,0 +1,14 @@
<?php
namespace OpenCloud\Common\Service;
use OpenCloud\Common\Api\Operator;
/**
* Represents the top-level abstraction of a service.
*
* @package OpenCloud\Common\Service
*/
abstract class AbstractService extends Operator implements ServiceInterface
{
}

View file

@ -0,0 +1,170 @@
<?php
namespace OpenCloud\Common\Service;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Middleware as GuzzleMiddleware;
use OpenCloud\Common\Auth\Token;
use OpenCloud\Common\Transport\HandlerStack;
use OpenCloud\Common\Transport\Middleware;
use OpenCloud\Common\Transport\Utils;
use OpenCloud\Identity\v3\Service;
/**
* A Builder for easily creating OpenCloud services.
*
* @package OpenCloud\Common\Service
*/
class Builder
{
/**
* Global options that will be applied to every service created by this builder.
*
* @var array
*/
private $globalOptions = [];
/** @var string */
private $rootNamespace;
/**
* Defaults that will be applied to options if no values are provided by the user.
*
* @var array
*/
private $defaults = ['urlType' => 'publicURL'];
/**
* @param array $globalOptions Options that will be applied to every service created by this builder.
* Eventually they will be merged (and if necessary overridden) by the
* service-specific options passed in.
*/
public function __construct(array $globalOptions = [], $rootNamespace = 'OpenCloud')
{
$this->globalOptions = $globalOptions;
$this->rootNamespace = $rootNamespace;
}
/**
* Internal method which resolves the API and Service classes for a service.
*
* @param string $serviceName The name of the service, e.g. Compute
* @param int $serviceVersion The major version of the service, e.g. 2
*
* @return array
*/
private function getClasses($serviceName, $serviceVersion)
{
$rootNamespace = sprintf("%s\\%s\\v%d", $this->rootNamespace, $serviceName, $serviceVersion);
return [
sprintf("%s\\Api", $rootNamespace),
sprintf("%s\\Service", $rootNamespace),
];
}
/**
* This method will return an OpenCloud service ready fully built and ready for use. There is
* some initial setup that may prohibit users from directly instantiating the service class
* directly - this setup includes the configuration of the HTTP client's base URL, and the
* attachment of an authentication handler.
*
* @param $serviceName The name of the service as it appears in the OpenCloud\* namespace
* @param $serviceVersion The major version of the service
* @param array $serviceOptions The service-specific options to use
*
* @return \OpenCloud\Common\Service\ServiceInterface
*
* @throws \Exception
*/
public function createService($serviceName, $serviceVersion, array $serviceOptions = [])
{
$options = $this->mergeOptions($serviceOptions);
$this->stockIdentityService($options);
$this->stockAuthHandler($options);
$this->stockHttpClient($options, $serviceName);
list($apiClass, $serviceClass) = $this->getClasses($serviceName, $serviceVersion);
return new $serviceClass($options['httpClient'], new $apiClass());
}
private function stockHttpClient(array &$options, $serviceName)
{
if (!isset($options['httpClient']) || !($options['httpClient'] instanceof ClientInterface)) {
if (strcasecmp($serviceName, 'identity') === 0) {
$baseUrl = $options['authUrl'];
$stack = $this->getStack($options['authHandler']);
} else {
list($token, $baseUrl) = $options['identityService']->authenticate($options);
$stack = $this->getStack($options['authHandler'], $token);
}
$this->addDebugMiddleware($options, $stack);
$options['httpClient'] = $this->httpClient($baseUrl, $stack);
}
}
/**
* @codeCoverageIgnore
*/
private function addDebugMiddleware(array $options, HandlerStack &$stack)
{
if (!empty($options['debugLog'])
&& !empty($options['logger'])
&& !empty($options['messageFormatter'])
) {
$stack->push(GuzzleMiddleware::log($options['logger'], $options['messageFormatter']));
}
}
private function stockIdentityService(array &$options)
{
if (!isset($options['identityService'])) {
$httpClient = $this->httpClient($options['authUrl'], HandlerStack::create());
$options['identityService'] = Service::factory($httpClient);
}
}
/**
* @param array $options
* @codeCoverageIgnore
*/
private function stockAuthHandler(array &$options)
{
if (!isset($options['authHandler'])) {
$options['authHandler'] = function () use ($options) {
return $options['identityService']->generateToken($options);
};
}
}
private function getStack(callable $authHandler, Token $token = null)
{
$stack = HandlerStack::create();
$stack->push(Middleware::authHandler($authHandler, $token));
return $stack;
}
private function httpClient($baseUrl, HandlerStack $stack)
{
return new Client([
'base_uri' => Utils::normalizeUrl($baseUrl),
'handler' => $stack,
]);
}
private function mergeOptions(array $serviceOptions)
{
$options = array_merge($this->defaults, $this->globalOptions, $serviceOptions);
if (!isset($options['authUrl'])) {
throw new \InvalidArgumentException('"authUrl" is a required option');
}
return $options;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace OpenCloud\Common\Service;
use OpenCloud\Common\Api\OperatorInterface;
/**
* Service interface.
*
* @package OpenCloud\Common\Service
*/
interface ServiceInterface extends OperatorInterface
{
}

View file

@ -0,0 +1,19 @@
<?php
namespace OpenCloud\Common\Transport;
use function GuzzleHttp\choose_handler;
use GuzzleHttp\HandlerStack as GuzzleStack;
class HandlerStack extends GuzzleStack
{
public static function create(callable $handler = null)
{
$stack = new self($handler ?: choose_handler());
$stack->push(Middleware::httpErrors());
$stack->push(Middleware::prepareBody());
return $stack;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace OpenCloud\Common\Transport;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Common\JsonPath;
/**
* Class responsible for populating the JSON body of a {@see GuzzleHttp\Message\Request} object.
*
* @package OpenCloud\Common\Transport
*/
class JsonSerializer
{
/**
* Populates the actual value into a JSON field, i.e. it has reached the end of the line and no
* further nesting is required.
*
* @param Parameter $param The schema that defines how the JSON field is being populated
* @param mixed $userValue The user value that is populating a JSON field
* @param array $json The existing JSON structure that will be populated
*
* @return array|mixed
*/
private function stockValue(Parameter $param, $userValue, $json)
{
$name = $param->getName();
if ($path = $param->getPath()) {
$jsonPath = new JsonPath($json);
$jsonPath->set(sprintf("%s.%s", $path, $name), $userValue);
$json = $jsonPath->getStructure();
} elseif ($name) {
$json[$name] = $userValue;
} else {
$json[] = $userValue;
}
return $json;
}
/**
* Populates a value into an array-like structure.
*
* @param Parameter $param The schema that defines how the JSON field is being populated
* @param mixed $userValue The user value that is populating a JSON field
*
* @return array|mixed
*/
private function stockArrayJson(Parameter $param, $userValue)
{
$elems = [];
foreach ($userValue as $item) {
$elems = $this->stockJson($param->getItemSchema(), $item, $elems);
}
return $elems;
}
/**
* Populates a value into an object-like structure.
*
* @param Parameter $param The schema that defines how the JSON field is being populated
* @param mixed $userValue The user value that is populating a JSON field
*
* @return array
*/
private function stockObjectJson(Parameter $param, $userValue)
{
$object = [];
foreach ($userValue as $key => $val) {
$object = $this->stockJson($param->getProperty($key), $val, $object);
}
return $object;
}
/**
* A generic method that will populate a JSON structure with a value according to a schema. It
* supports multiple types and will delegate accordingly.
*
* @param Parameter $param The schema that defines how the JSON field is being populated
* @param mixed $userValue The user value that is populating a JSON field
* @param array $json The existing JSON structure that will be populated
*
* @return array
*/
public function stockJson(Parameter $param, $userValue, $json)
{
if ($param->isArray()) {
$userValue = $this->stockArrayJson($param, $userValue);
} elseif ($param->isObject()) {
$userValue = $this->stockObjectJson($param, $userValue);
}
// Populate the final value
return $this->stockValue($param, $userValue, $json);
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace OpenCloud\Common\Transport;
use function GuzzleHttp\Psr7\modify_request;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware as GuzzleMiddleware;
use OpenCloud\Common\Auth\AuthHandler;
use OpenCloud\Common\Auth\Token;
use OpenCloud\Common\Error\Builder;
use Psr\Http\Message\ResponseInterface;
final class Middleware
{
/**
* @return callable
*/
public static function httpErrors()
{
return function (callable $handler) {
return function ($request, array $options) use ($handler) {
return $handler($request, $options)->then(
function (ResponseInterface $response) use ($request, $handler) {
if ($response->getStatusCode() < 400) {
return $response;
}
throw (new Builder())->httpError($request, $response);
}
);
};
};
}
/**
* @param callable $tokenGenerator
* @param Token $token
*
* @return callable
*/
public static function authHandler(callable $tokenGenerator, Token $token = null)
{
return function (callable $handler) use ($tokenGenerator, $token) {
return new AuthHandler($handler, $tokenGenerator, $token);
};
}
/**
* @codeCoverageIgnore
*/
public static function history(array &$container)
{
return GuzzleMiddleware::history($container);
}
/**
* @codeCoverageIgnore
*/
public static function retry(callable $decider, callable $delay = null)
{
return GuzzleMiddleware::retry($decider, $delay);
}
/**
* @codeCoverageIgnore
*/
public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO)
{
return GuzzleMiddleware::log($logger, $formatter, $logLevel);
}
/**
* @codeCoverageIgnore
*/
public static function prepareBody()
{
return GuzzleMiddleware::prepareBody();
}
/**
* @codeCoverageIgnore
*/
public static function mapRequest(callable $fn)
{
return GuzzleMiddleware::mapRequest($fn);
}
/**
* @codeCoverageIgnore
*/
public static function mapResponse(callable $fn)
{
return GuzzleMiddleware::mapResponse($fn);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace OpenCloud\Common\Transport;
use function GuzzleHttp\uri_template;
use function GuzzleHttp\Psr7\build_query;
use function GuzzleHttp\Psr7\modify_request;
use OpenCloud\Common\Api\Operation;
use OpenCloud\Common\Api\Parameter;
class RequestSerializer
{
private $jsonSerializer;
public function __construct(JsonSerializer $jsonSerializer = null)
{
$this->jsonSerializer = $jsonSerializer ?: new JsonSerializer();
}
public function serializeOptions(Operation $operation, array $userValues = [])
{
$options = ['headers' => []];
foreach ($userValues as $paramName => $paramValue) {
if (null === ($schema = $operation->getParam($paramName))) {
continue;
}
$method = sprintf('stock%s', ucfirst($schema->getLocation()));
$this->$method($schema, $paramValue, $options);
}
if (!empty($options['json'])) {
if ($key = $operation->getJsonKey()) {
$options['json'] = [$key => $options['json']];
}
if (strpos(json_encode($options['json']), '\/') !== false) {
$options['body'] = json_encode($options['json'], JSON_UNESCAPED_SLASHES);
$options['headers']['Content-Type'] = 'application/json';
unset($options['json']);
}
}
return $options;
}
private function stockUrl()
{
}
private function stockQuery(Parameter $schema, $paramValue, array &$options)
{
$options['query'][$schema->getName()] = $paramValue;
}
private function stockHeader(Parameter $schema, $paramValue, array &$options)
{
$paramName = $schema->getName();
if (stripos($paramName, 'metadata') !== false) {
return $this->stockMetadataHeader($schema, $paramValue, $options);
}
$options['headers'] += is_scalar($paramValue) ? [$schema->getPrefixedName() => $paramValue] : [];
}
private function stockMetadataHeader(Parameter $schema, $paramValue, array &$options)
{
foreach ($paramValue as $key => $keyVal) {
$schema = $schema->getItemSchema() ?: new Parameter(['prefix' => $schema->getPrefix(), 'name' => $key]);
$this->stockHeader($schema, $keyVal, $options);
}
}
private function stockJson(Parameter $schema, $paramValue, array &$options)
{
$json = isset($options['json']) ? $options['json'] : [];
$options['json'] = $this->jsonSerializer->stockJson($schema, $paramValue, $json);
}
private function stockRaw(Parameter $schema, $paramValue, array &$options)
{
$options['body'] = $paramValue;
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace OpenCloud\Common\Transport;
use function GuzzleHttp\Psr7\uri_for;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
class Utils
{
public static function jsonDecode(ResponseInterface $response, $assoc = true)
{
$jsonErrors = [
JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH - Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH - Underflow or the modes mismatch',
JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR - Unexpected control character found',
JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX - Syntax error, malformed JSON',
JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8 - Malformed UTF-8 characters, possibly incorrectly encoded'
];
$responseBody = (string) $response->getBody();
if (strlen($responseBody) === 0) {
return $responseBody;
}
$data = json_decode($responseBody, $assoc);
if (JSON_ERROR_NONE !== json_last_error()) {
$last = json_last_error();
throw new \InvalidArgumentException(
'Unable to parse JSON data: ' . (isset($jsonErrors[$last]) ? $jsonErrors[$last] : 'Unknown error')
);
}
return $data;
}
/**
* Method for flattening a nested array.
*
* @param array $data The nested array
* @param null $key The key to extract
*
* @return array
*/
public static function flattenJson($data, $key = null)
{
return (!empty($data) && $key && isset($data[$key])) ? $data[$key] : $data;
}
/**
* Method for normalize an URL string.
*
* Append the http:// prefix if not present, and add a
* closing url separator when missing.
*
* @param string $url The url representation.
*
* @return string
*/
public static function normalizeUrl($url)
{
if (strpos($url, 'http') === false) {
$url = 'http://' . $url;
}
return rtrim($url, '/') . '/';
}
/**
* Add an unlimited list of paths to a given URI.
*
* @param UriInterface $uri
* @param ...$paths
*
* @return UriInterface
*/
public static function addPaths(UriInterface $uri, ...$paths)
{
return uri_for(rtrim((string) $uri, '/') . '/' . implode('/', $paths));
}
public static function appendPath(UriInterface $uri, $path)
{
return uri_for(rtrim((string) $uri, '/') . '/' . $path);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace OpenCloud\integration;
use Psr\Log\AbstractLogger;
class DefaultLogger extends AbstractLogger
{
public function log($level, $message, array $context = [])
{
echo $this->format($level, $message, $context);
}
private function format($level, $message, $context)
{
$msg = strtr($message, $context);
return sprintf("%s: %s\n", strtoupper($level), $msg);
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace OpenCloud\integration;
class Runner
{
private $basePath;
private $logger;
private $services = [];
public function __construct($basePath)
{
$this->basePath = $basePath;
$this->logger = new DefaultLogger();
$this->assembleServicesFromSamples();
}
private function traverse($path)
{
return new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
}
private function assembleServicesFromSamples()
{
foreach ($this->traverse($this->basePath) as $servicePath) {
if ($servicePath->isDir()) {
foreach ($this->traverse($servicePath) as $versionPath) {
$this->services[$servicePath->getBasename()][] = $versionPath->getBasename();
}
}
}
}
private function getOpts()
{
$opts = getopt('s:v:t:', ['service:', 'version:', 'test::', 'debug::', 'help::']);
$getOpt = function (array $keys, $default) use ($opts) {
foreach ($keys as $key) {
if (isset($opts[$key])) {
return $opts[$key];
}
}
return $default;
};
return [
$getOpt(['s', 'service'], 'all'),
$getOpt(['n', 'version'], 'all'),
$getOpt(['t', 'test'], ''),
isset($opts['debug']) ? (int) $opts['debug'] : 0,
];
}
private function getRunnableServices($service, $version)
{
$services = $this->services;
if ($service != 'all') {
if (!isset($this->services[$service])) {
throw new \InvalidArgumentException(sprintf("%s service does not exist", $service));
}
$versions = ($version == 'all') ? $this->services[$service] : [$version];
$services = [$service => $versions];
}
return $services;
}
/**
* @return TestInterface
*/
private function getTest($serviceName, $version, $verbosity)
{
$namespace = (new \ReflectionClass($this))->getNamespaceName();
$className = sprintf("%s\\%s\\%sTest", $namespace, Utils::toCamelCase($serviceName), ucfirst($version));
if (!class_exists($className)) {
throw new \RuntimeException(sprintf("%s does not exist", $className));
}
$basePath = $this->basePath . DIRECTORY_SEPARATOR . $serviceName . DIRECTORY_SEPARATOR . $version;
$smClass = sprintf("%s\\SampleManager", $namespace);
$class = new $className($this->logger, new $smClass($basePath, $verbosity));
if (!($class instanceof TestInterface)) {
throw new \RuntimeException(sprintf("%s does not implement TestInterface", $className));
}
return $class;
}
public function runServices()
{
list($serviceOpt, $versionOpt, $testMethodOpt, $verbosityOpt) = $this->getOpts();
foreach ($this->getRunnableServices($serviceOpt, $versionOpt) as $serviceName => $versions) {
foreach ($versions as $version) {
$testRunner = $this->getTest($serviceName, $version, $verbosityOpt);
if ($testMethodOpt) {
$testRunner->runOneTest($testMethodOpt);
} else {
$testRunner->runTests();
}
$testRunner->teardown();
}
}
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace OpenCloud\integration;
class SampleManager implements SampleManagerInterface
{
protected $basePath;
protected $paths = [];
protected $verbosity;
public function __construct($basePath, $verbosity)
{
$this->basePath = $basePath;
$this->verbosity = $verbosity;
}
public function deletePaths()
{
if (!empty($this->paths)) {
foreach ($this->paths as $path) {
unlink($path);
}
}
}
protected function getGlobalReplacements()
{
return [
'{userId}' => getenv('OS_USER_ID'),
'{username}' => getenv('OS_USERNAME'),
'{password}' => getenv('OS_PASSWORD'),
'{domainId}' => getenv('OS_DOMAIN_ID'),
'{authUrl}' => getenv('OS_AUTH_URL'),
'{tenantId}' => getenv('OS_TENANT_ID'),
'{region}' => getenv('OS_REGION'),
'{projectId}' => getenv('OS_PROJECT_ID'),
'{projectName}' => getenv('OS_PROJECT_NAME'),
];
}
protected function getConnectionTemplate()
{
if ($this->verbosity === 1) {
$subst = <<<'EOL'
use OpenCloud\Integration\DefaultLogger;
use OpenCloud\Integration\Utils;
use GuzzleHttp\MessageFormatter;
$options = [
'debugLog' => true,
'logger' => new DefaultLogger(),
'messageFormatter' => new MessageFormatter(),
];
$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts($options));
EOL;
} elseif ($this->verbosity === 2) {
$subst = <<<'EOL'
use OpenCloud\Integration\DefaultLogger;
use OpenCloud\Integration\Utils;
use GuzzleHttp\MessageFormatter;
$options = [
'debugLog' => true,
'logger' => new DefaultLogger(),
'messageFormatter' => new MessageFormatter(MessageFormatter::DEBUG),
];
$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts($options));
EOL;
} else {
$subst = <<<'EOL'
use OpenCloud\Integration\Utils;
$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts());
EOL;
}
return $subst;
}
public function write($path, array $replacements)
{
$replacements = array_merge($this->getGlobalReplacements(), $replacements);
$sampleFile = rtrim($this->basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
if (!file_exists($sampleFile) || !is_readable($sampleFile)) {
throw new \RuntimeException(sprintf("%s either does not exist or is not readable", $sampleFile));
}
$content = strtr(file_get_contents($sampleFile), $replacements);
$content = str_replace("'vendor/'", "'" . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . "vendor'", $content);
$subst = $this->getConnectionTemplate();
$content = preg_replace('/\([^)]+\)/', '', $content, 1);
$content = str_replace('$openstack = new OpenCloud\OpenCloud;', $subst, $content);
$tmp = tempnam(sys_get_temp_dir(), 'openstack');
file_put_contents($tmp, $content);
$this->paths[] = $tmp;
return $tmp;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace OpenCloud\integration;
interface SampleManagerInterface
{
public function write($path, array $replacements);
public function deletePaths();
}

View file

@ -0,0 +1,115 @@
<?php
namespace OpenCloud\integration;
use Psr\Log\LoggerInterface;
abstract class TestCase extends \PHPUnit_Framework_TestCase implements TestInterface
{
private $logger;
private $startPoint;
private $lastPoint;
private $sampleManager;
public function __construct(LoggerInterface $logger, SampleManagerInterface $sampleManager)
{
$this->logger = $logger;
$this->sampleManager = $sampleManager;
}
public function teardown()
{
$this->sampleManager->deletePaths();
}
public function runOneTest($name)
{
if (!method_exists($this, $name)) {
throw new \InvalidArgumentException(sprintf("%s method does not exist", $name));
}
$this->startTimer();
$this->$name();
$this->outputTimeTaken();
}
protected function startTimer()
{
$this->startPoint = $this->lastPoint = microtime(true);
}
private function wrapColor($message, $colorPrefix)
{
return sprintf("%s%s", $colorPrefix, $message) . "\033[0m\033[1;0m";
}
protected function logStep($message, array $context = [])
{
$duration = microtime(true) - $this->lastPoint;
$stepTimeTaken = sprintf('(%s)', $this->formatSecDifference($duration));
if ($duration >= 10) {
$color = "\033[0m\033[1;31m"; // red
} elseif ($duration >= 2) {
$color = "\033[0m\033[1;33m"; // yellow
} else {
$color = "\033[0m\033[1;32m"; // green
}
$message = '{timeTaken} ' . $message;
$context['{timeTaken}'] = $this->wrapColor($stepTimeTaken, $color);
$this->logger->info($message, $context);
$this->lastPoint = microtime(true);
}
protected function randomStr($length = 5)
{
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charsLen = strlen($chars);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $chars[rand(0, $charsLen - 1)];
}
return 'phptest_' . $randomString;
}
private function formatMinDifference($duration)
{
$output = '';
if (($minutes = floor($duration / 60)) > 0) {
$output .= $minutes . 'min' . (($minutes > 1) ? 's' : '');
}
if (($seconds = number_format(fmod($duration, 60), 2)) > 0) {
if ($minutes > 0) {
$output .= ' ';
}
$output .= $seconds . 's';
}
return $output;
}
private function formatSecDifference($duration)
{
return number_format($duration, 2) . 's';
}
protected function outputTimeTaken()
{
$output = $this->formatMinDifference(microtime(true) - $this->startPoint);
$this->logger->info('Finished all tests! Time taken: {output}.', ['{output}' => $output]);
}
protected function sampleFile(array $replacements, $path)
{
return $this->sampleManager->write($path, $replacements);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace OpenCloud\integration;
use Psr\Log\LoggerInterface;
interface TestInterface
{
public function __construct(LoggerInterface $logger, SampleManagerInterface $sampleManager);
public function runTests();
public function runOneTest($name);
public function teardown();
}

View file

@ -0,0 +1,59 @@
<?php
namespace OpenCloud\integration;
use GuzzleHttp\Client;
use OpenCloud\Identity\v2\Api;
use OpenCloud\Identity\v2\Service;
use OpenCloud\Common\Transport\HandlerStack;
use OpenCloud\Common\Transport\Utils as CommonUtils;
class Utils
{
public static function getAuthOptsV3()
{
return [
'authUrl' => getenv('OS_AUTH_URL'),
'region' => getenv('OS_REGION_NAME'),
'user' => [
'id' => getenv('OS_USER_ID'),
'password' => getenv('OS_PASSWORD'),
],
'scope' => [
'project' => [
'id' => getenv('OS_PROJECT_ID'),
]
]
];
}
public static function getAuthOptsV2()
{
$httpClient = new Client([
'base_uri' => CommonUtils::normalizeUrl(getenv('OS_AUTH_URL')),
'handler' => HandlerStack::create(),
]);
return [
'authUrl' => getenv('OS_AUTH_URL'),
'region' => getenv('OS_REGION_NAME'),
'username' => getenv('OS_USERNAME'),
'password' => getenv('OS_PASSWORD'),
'tenantName' => getenv('OS_TENANT_NAME'),
'identityService' => new Service($httpClient, new Api),
];
}
public static function getAuthOpts(array $options = [])
{
$authOptions = getenv('OS_IDENTITY_API_VERSION') == '2.0'
? self::getAuthOptsV2()
: self::getAuthOptsV3();
return array_merge($authOptions, $options);
}
public static function toCamelCase($word, $separator = '_')
{
return str_replace($separator, '', ucwords($word, $separator));
}
}

View file

@ -0,0 +1,10 @@
<?php
$rootDir = dirname(dirname(__DIR__));
require_once $rootDir . '/vendor/autoload.php';
$basePath = $rootDir . '/samples';
$runner = new \OpenCloud\Integration\Runner($basePath);
$runner->runServices();

View file

@ -0,0 +1,3 @@
#!/bin/bash
php tests/integration/Runner.php -s compute -v v2

View file

@ -0,0 +1,3 @@
#!/bin/bash
php tests/integration/Runner.php -s identity -v v3

View file

@ -0,0 +1,3 @@
#!/bin/bash
php tests/integration/Runner.php -s networking -v v2

View file

@ -0,0 +1,3 @@
#!/bin/bash
php tests/integration/Runner.php -s objectstore -v v2

View file

@ -0,0 +1,74 @@
<?php
namespace OpenCloud\Test\Common\Api;
use OpenCloud\Common\Api\Operation;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Test\Fixtures\ComputeV2Api;
class OperationTest extends \PHPUnit_Framework_TestCase
{
private $operation;
public function setUp()
{
$def = (new ComputeV2Api())->postServer();
$this->operation = new Operation($def);
}
public function test_it_reveals_whether_params_are_set_or_not()
{
$this->assertFalse($this->operation->hasParam('foo'));
$this->assertTrue($this->operation->hasParam('name'));
}
public function test_it_gets_params()
{
$this->assertInstanceOf(Parameter::class, $this->operation->getParam('name'));
}
public function test_it_validates_params()
{
$this->assertTrue($this->operation->validate([
'name' => 'foo',
'imageId' => 'bar',
'flavorId' => 'baz',
]));
}
/**
* @expectedException \Exception
*/
public function test_exceptions_are_propagated()
{
$this->assertFalse($this->operation->validate([
'name' => true,
'imageId' => 'bar',
'flavorId' => 'baz',
]));
}
/**
* @expectedException \Exception
*/
public function test_an_exception_is_thrown_when_user_does_not_provide_required_options()
{
$this->operation->validate([]);
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_user_provides_undefined_options()
{
$userData = ['name' => 'new_server', 'undefined_opt' => 'bah'];
$this->operation->validate($userData);
}
public function test_it_gets_json_key()
{
$this->assertEquals('server', $this->operation->getJsonKey());
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace OpenCloud\Test\Common\Api;
use function GuzzleHttp\Psr7\uri_for;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use OpenCloud\Common\Api\Operator;
use OpenCloud\Common\Resource\AbstractResource;
use OpenCloud\Common\Resource\ResourceInterface;
use OpenCloud\Compute\v2\Models\Server;
use OpenCloud\Test\Fixtures\ComputeV2Api;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class OperatorTest extends TestCase
{
private $operator;
private $def;
public function setUp()
{
parent::setUp();
$this->rootFixturesDir = __DIR__;
$this->def = [
'method' => 'GET',
'path' => 'test',
'params' => [],
];
$this->operator = new TestOperator($this->client->reveal(), new ComputeV2Api());
}
public function test_it_returns_operations()
{
$this->assertInstanceOf(
'OpenCloud\Common\Api\Operation',
$this->operator->getOperation($this->def, [])
);
}
public function test_it_sends_a_request_when_operations_are_executed()
{
$this->client->request('GET', 'test', ['headers' => []])->willReturn(new Request('GET', 'test'));
$this->operator->execute($this->def, []);
}
public function test_it_sends_a_request_when_async_operations_are_executed()
{
$this->client->requestAsync('GET', 'test', ['headers' => []])->willReturn(new Promise());
$this->operator->executeAsync($this->def, []);
}
public function test_it_returns_a_model_instance()
{
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class));
}
public function test_it_populates_models_from_response()
{
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, new Response(200)));
}
public function test_it_populates_models_from_arrays()
{
$data = ['flavor' => [], 'image' => []];
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, $data));
}
public function test_it_wraps_sequential_ops_in_promise_when_async_is_appended_to_method_name()
{
$promise = $this->operator->createAsync('something');
$this->assertInstanceOf(Promise::class, $promise);
$promise->then(function ($val) {
$this->assertEquals('Created something', $val);
});
$promise->wait();
}
/**
* @expectedException \RuntimeException
*/
public function test_it_throws_exception_when_async_is_called_on_a_non_existent_method()
{
$this->operator->fooAsync();
}
public function test_it_retrieves_base_http_url()
{
$returnedUri = uri_for('http://foo.com');
$this->client->getConfig('base_uri')->shouldBeCalled()->willReturn($returnedUri);
$uri = $this->operator->testBaseUri();
$this->assertInstanceOf(Uri::class, $uri);
$this->assertEquals($returnedUri, $uri);
}
/**
* @expectedException \Exception
*/
public function test_undefined_methods_result_in_error()
{
$this->operator->foo();
}
}
class TestResource extends AbstractResource
{
}
class TestOperator extends Operator
{
public function testBaseUri()
{
return $this->getHttpBaseUrl();
}
public function create($str)
{
return 'Created ' . $str;
}
public function fail()
{
}
}

View file

@ -0,0 +1,203 @@
<?php
namespace OpenCloud\Test\Common\Api;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Test\Fixtures\ComputeV2Api;
class ParameterTest extends \PHPUnit_Framework_TestCase
{
const PARAMETER_CLASS = 'OpenCloud\Common\Api\Parameter';
private $param;
private $data;
private $api;
public function setUp()
{
$this->api = new ComputeV2Api();
$this->data = $this->api->postServer()['params']['name'] + ['name' => 'name'];
$this->param = new Parameter($this->data);
}
/**
* @expectedException \RuntimeException
*/
public function test_exception_is_thrown_for_invalid_locations()
{
$data = $this->data;
$data['location'] = 'foo';
new Parameter($data);
}
public function test_it_should_provide_access_to_a_name()
{
$this->assertEquals($this->data['name'], $this->param->getName());
}
public function test_it_should_use_sentAs_alias_for_name_if_one_is_set()
{
$data = $this->data + ['sentAs' => 'foo'];
$param = new Parameter($data);
$this->assertEquals($data['sentAs'], $param->getName());
}
public function test_it_indicates_whether_it_is_required_or_not()
{
$this->assertTrue($this->param->isRequired());
}
public function test_it_indicates_its_item_schema()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$this->assertInstanceOf(self::PARAMETER_CLASS, $param->getItemSchema());
}
public function test_it_allows_property_retrieval()
{
$definition = $this->api->postServer()['params']['networks']['items'] + ['name' => 'network'];
$param = new Parameter($definition);
$this->assertInstanceOf(self::PARAMETER_CLASS, $param->getProperty('uuid'));
}
public function test_it_indicates_its_path()
{
$path = 'foo.bar.baz';
$param = new Parameter($this->data + ['path' => $path]);
$this->assertEquals($path, $param->getPath());
}
public function test_it_verifies_a_given_location_with_a_boolean()
{
$this->assertFalse($this->param->hasLocation('foo'));
$this->assertTrue($this->param->hasLocation('json'));
}
public function test_it_should_return_true_when_required_attributes_are_provided_and_match_their_definitions()
{
$this->assertTrue($this->param->validate('TestName'));
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_values_do_not_match_their_definition_types()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$param->validate('a_network!'); // should be an array
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_deeply_nested_values_have_wrong_types()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$param->validate(['name' => false]); // value should be a string, not bool
}
public function test_metadata_properties_are_handled_differently()
{
$params = [
'name' => 'metadata',
'type' => 'object',
'properties' => [
'type' => 'string',
],
];
$userValues = ['some' => 'value'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userValues));
}
public function test_it_passes_validation_when_array_values_pass()
{
$params = [
'name' => 'foo',
'type' => 'array',
'items' => ['type' => 'string'],
];
$userVals = ['1', '2', '3'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userVals));
}
/**
* @expectedException \Exception
*/
public function test_an_exception_is_thrown_when_an_undefined_property_is_provided()
{
$params = ['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]];
$userVals = ['bar' => 'baz'];
$param = new Parameter($params);
$param->validate($userVals);
}
public function test_it_passes_validation_when_all_subproperties_pass()
{
$params = ['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]];
$userVals = ['foo' => 'baz'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userVals));
}
public function test_it_sets_name()
{
$this->param->setName('foo');
$this->assertEquals($this->param->getName(), 'foo');
}
public function test_it_gets_property()
{
$property = new Parameter([
'name' => 'metadata',
'properties' => [
'type' => 'string',
'prefix' => 'foo',
],
]);
$prop = $property->getProperty('metadata');
$this->assertInstanceOf(Parameter::class, $prop);
$this->assertEquals('foo', $prop->getPrefix());
}
public function test_it_gets_prefixed_name()
{
$property = new Parameter([
'name' => 'metadata',
'prefix' => 'foo-',
]);
$this->assertEquals('foo-metadata', $property->getPrefixedName());
}
/**
* @expectedException \Exception
*/
public function test_exception_is_thrown_when_value_is_not_in_enum_list()
{
$data = $this->data;
$data['enum'] = ['foo'];
$param = new Parameter($data);
$param->validate('blah');
}
}

View file

@ -0,0 +1,25 @@
<?php
return [
'method' => 'POST',
'path' => 'something',
'params' => [
'name' => [
'type' => 'string',
'location' => 'header',
'sentAs' => 'X-Foo-Name'
],
'age' => [
'type' => 'integer',
'location' => 'header'
],
'metadata' => [
'type' => 'object',
'location' => 'header',
'items' => [
'prefix' => 'X-Meta-'
]
],
'other' => ['type' => 'string'] // should not be a header
],
];

View file

@ -0,0 +1,27 @@
<?php
return [
'method' => 'POST',
'path' => 'something',
'params' => [
'name' => [
'type' => 'string',
'sentAs' => 'server_name',
],
'other' => [
'type' => 'array',
'sentAs' => 'other_params',
'items' => [
'type' => 'string'
]
],
'etc' => [
'type' => 'object',
'sentAs' => 'etcetc',
'properties' => [
'dob' => ['type' => 'string'],
'age' => ['type' => 'integer', 'sentAs' => 'current_age'],
]
],
],
];

View file

@ -0,0 +1,59 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\ArrayAccessTrait;
use OpenCloud\Test\TestCase;
class ArrayAccessTraitTest extends TestCase
{
private $aa;
public function setUp()
{
$this->aa = new ArrayAccess();
}
public function test_offset_is_set()
{
$this->aa->offsetSet('foo', 'bar');
$this->assertEquals(['foo' => 'bar'], $this->aa->getElements());
}
public function test_it_appends_if_no_key_is_set()
{
$this->aa->offsetSet(null, 'bar');
$this->assertEquals(['bar'], $this->aa->getElements());
}
public function test_if_checks_if_offset_exists()
{
$this->aa->offsetSet('bar', 'foo');
$this->assertTrue($this->aa->offsetExists('bar'));
$this->assertFalse($this->aa->offsetExists('baz'));
}
public function test_if_gets_offset()
{
$this->aa->offsetSet('bar', 'foo');
$this->assertEquals('foo', $this->aa->offsetGet('bar'));
$this->assertNull($this->aa->offsetGet('baz'));
}
public function test_it_unsets_offset()
{
$this->aa->offsetSet('bar', 'foo');
$this->aa->offsetUnset('bar');
$this->assertNull($this->aa->offsetGet('bar'));
}
}
class ArrayAccess
{
use ArrayAccessTrait;
public function getElements()
{
return $this->internalState;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace OpenCloud\Test\Common\Auth;
use GuzzleHttp\Psr7\Request;
use OpenCloud\Common\Auth\AuthHandler;
use OpenCloud\Common\Auth\Token;
use OpenCloud\Test\TestCase;
use Psr\Http\Message\RequestInterface;
class AuthHandlerTest extends TestCase
{
const TOKEN_ID = 'tokenId';
private $generator;
private $handler;
public function setUp()
{
$this->generator = function () {
$token = $this->prophesize(FakeToken::class);
$token->getId()->shouldBeCalled()->willReturn(self::TOKEN_ID);
return $token->reveal();
};
$this->handler = function (RequestInterface $r) {
return $r;
};
$this->handler = new AuthHandler($this->handler, $this->generator);
}
public function test_it_should_bypass_auth_http_requests()
{
// Fake a Keystone request
$request = new Request('POST', 'https://my-openstack.org:5000/v2.0/tokens');
$this->assertEquals($request, call_user_func_array($this->handler, [$request, []]));
}
public function test_it_should_generate_a_new_token_if_the_current_token_is_either_expired_or_not_set()
{
$token = $this->prophesize(Token::class);
// force the mock token to indicate that its expired
$token->getId()->willReturn('');
$token->hasExpired()->willReturn(true);
$request = new Request('GET', '');
$handler = new AuthHandler($this->handler, $this->generator, $token->reveal());
$handler($request, []);
}
}
class FakeToken implements Token {
public function getId() {}
public function hasExpired() {}
}

View file

@ -0,0 +1,16 @@
<?php
namespace OpenCloud\Test\Common;
class DateTime extends \DateTime
{
public static function factory($time)
{
return new static($time);
}
public function toIso8601()
{
return $this->format(self::ISO8601);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace OpenCloud\Test\Common\Error;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use OpenCloud\Common\Error\BadResponseError;
use OpenCloud\Test\TestCase;
class BadResponseErrorTest extends TestCase
{
private $e;
public function setUp()
{
$this->e = new BadResponseError();
}
public function test_it_gets_request()
{
$r = new Request('GET', '');
$this->e->setRequest($r);
$this->assertEquals($this->e->getRequest(), $r);
}
public function test_it_gets_response()
{
$r = new Response(500);
$this->e->setResponse($r);
$this->assertEquals($this->e->getResponse(), $r);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace OpenCloud\Test\Common\Error;
use function GuzzleHttp\Psr7\{stream_for,str};
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\{Request,Response};
use OpenCloud\Common\Error\{BadResponseError,Builder,UserInputError};
class BuilderTest extends \PHPUnit_Framework_TestCase
{
private $builder;
private $client;
public function __construct()
{
$this->client = $this->prophesize(ClientInterface::class);
$this->builder = new Builder($this->client->reveal());
}
public function test_it_injects_client()
{
$this->assertInstanceOf(Builder::class, new Builder($this->client->reveal()));
}
public function test_it_builds_http_errors()
{
$request = new Request('POST', '/servers');
$response = new Response(400, [], stream_for('Invalid parameters'));
$requestStr = trim($this->builder->str($request));
$responseStr = trim($this->builder->str($response));
$errorMessage = <<<EOT
HTTP Error
~~~~~~~~~~
The remote server returned a "400 Bad Request" error for the following transaction:
Request
~~~~~~~
$requestStr
Response
~~~~~~~~
$responseStr
Further information
~~~~~~~~~~~~~~~~~~~
Please ensure that your input values are valid and well-formed. Visit http://docs.php-opencloud.com/en/latest/http-codes for more information about debugging HTTP status codes, or file a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$e = new BadResponseError($errorMessage);
$e->setRequest($request);
$e->setResponse($response);
$this->assertEquals($e, $this->builder->httpError($request, $response));
}
public function test_it_builds_user_input_errors()
{
$expected = 'A well-formed string';
$value = ['foo' => true];
$link = 'http://docs.php-opencloud.com/en/latest/index.html';
$errorMessage = <<<EOT
User Input Error
~~~~~~~~~~~~~~~~
A well-formed string was expected, but the following value was passed in:
Array
(
[foo] => 1
)
Please ensure that the value adheres to the expectation above. Visit $link for more information about input arguments. If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$this->client
->request('HEAD', $link)
->shouldBeCalled()
->willReturn(new Response(200));
$e = new UserInputError($errorMessage);
$this->assertEquals($e, $this->builder->userInputError($expected, $value, 'index.html'));
}
public function test_dead_links_are_ignored()
{
$expected = 'A well-formed string';
$value = ['foo' => true];
$errorMessage = <<<EOT
User Input Error
~~~~~~~~~~~~~~~~
A well-formed string was expected, but the following value was passed in:
Array
(
[foo] => 1
)
Please ensure that the value adheres to the expectation above. If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$this->client
->request('HEAD', 'http://docs.php-opencloud.com/en/latest/sdffsda')
->shouldBeCalled()
->willThrow(ClientException::class);
$e = new UserInputError($errorMessage);
$this->assertEquals($e, $this->builder->userInputError($expected, $value, 'sdffsda'));
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\HydratorStrategyTrait;
use OpenCloud\Test\TestCase;
class HydratorStrategyTraitTest extends TestCase
{
private $fixture;
public function setUp()
{
$this->fixture = new Fixture();
}
public function test_it_hydrates()
{
$data = ['foo' => 1, 'bar' => 2, 'baz' => 3, 'boo' => 4];
$this->fixture->hydrate($data);
$this->assertEquals(1, $this->fixture->foo);
$this->assertEquals(2, $this->fixture->getBar());
$this->assertEquals(3, $this->fixture->getBaz());
}
public function test_it_hydrates_aliases()
{
$this->fixture->hydrate(['FOO!' => 1], ['FOO!' => 'foo']);
$this->assertEquals(1, $this->fixture->foo);
}
}
class Fixture
{
public $foo;
protected $bar;
private $baz;
use HydratorStrategyTrait;
public function getBar()
{
return $this->bar;
}
public function getBaz()
{
return $this->baz;
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\JsonPath;
class JsonPathTest extends \PHPUnit_Framework_TestCase
{
private $jsonPath;
public function setUp()
{
$this->jsonPath = new JsonPath([]);
}
public function test_it_sets_values_according_to_paths()
{
$this->jsonPath->set('foo.bar.baz', 'VALUE');
$expected = [
'foo' => [
'bar' => [
'baz' => 'VALUE',
]
]
];
$this->assertEquals($expected, $this->jsonPath->getStructure());
}
public function test_it_sets_arrays_according_to_paths()
{
$jsonPath = new JsonPath([
'foo' => [
'bar' => [
'value' => 'VALUE',
]
]
]);
$jsonPath->set('foo.bar.items', ['item_1', 'item_2']);
$expected = [
'foo' => [
'bar' => [
'value' => 'VALUE',
'items' => ['item_1', 'item_2'],
]
]
];
$this->assertEquals($expected, $jsonPath->getStructure());
}
public function test_it_gets_values_according_to_paths()
{
$jsonPath = new JsonPath([
'foo' => [
'bar' => [
'baz' => 'VALUE_1',
'lol' => 'VALUE_2',
]
]
]);
$this->assertEquals('VALUE_1', $jsonPath->get('foo.bar.baz'));
$this->assertEquals('VALUE_2', $jsonPath->get('foo.bar.lol'));
$this->assertNull($jsonPath->get('foo.bar.boo'));
}
}

View file

@ -0,0 +1,231 @@
[
{ "comment": "empty list, empty docs",
"doc": {},
"patch": [],
"expected": {} },
{ "comment": "empty patch list",
"doc": {"foo": 1},
"patch": [],
"expected": {"foo": 1} },
{ "comment": "rearrangements OK?",
"doc": {"foo": 1, "bar": 2},
"patch": [],
"expected": {"bar":2, "foo": 1} },
{ "comment": "rearrangements OK? How about one level down ... array",
"doc": [{"foo": 1, "bar": 2}],
"patch": [],
"expected": [{"bar":2, "foo": 1}] },
{ "comment": "rearrangements OK? How about one level down...",
"doc": {"foo":{"foo": 1, "bar": 2}},
"patch": [],
"expected": {"foo":{"bar":2, "foo": 1}} },
{ "comment": "toplevel array",
"doc": [],
"patch": [{"op": "add", "path": "/0", "value": "foo"}],
"expected": ["foo"] },
{ "comment": "toplevel array, no change",
"doc": ["foo"],
"patch": [],
"expected": ["foo"] },
{ "comment": "toplevel object, numeric string",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": "1"}],
"expected": {"foo":"1"} },
{ "comment": "toplevel object, integer",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": 1}],
"expected": {"foo":1} },
{ "comment": "Toplevel scalar values OK?",
"doc": "foo",
"patch": [{"op": "replace", "path": "", "value": "bar"}],
"expected": "bar",
"disabled": true },
{ "comment": "Add, / target",
"doc": {},
"patch": [ {"op": "add", "path": "/", "value":1 } ],
"expected": {"":1} },
{ "comment": "Add composite value at top level",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": [1, 2]}],
"expected": {"foo": 1, "bar": [1, 2]} },
{ "comment": "Add into composite value",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}],
"expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/8", "value": "5"}],
"error": "Out of bounds (upper)" },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/-1", "value": "5"}],
"error": "Out of bounds (lower)" },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": true}],
"expected": {"foo": 1, "bar": true} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": false}],
"expected": {"foo": 1, "bar": false} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": null}],
"expected": {"foo": 1, "bar": null} },
{ "comment": "0 can be an array index or object element name",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": {"foo": 1, "0": "bar" } },
{ "doc": ["foo"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar", "sil"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": ["bar", "foo", "sil"] },
{ "comment": "push item to array via last index + 1",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/2", "value": "bar"}],
"expected": ["foo", "sil", "bar"] },
{ "comment": "add item to array at index > length should fail",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/3", "value": "bar"}],
"error": "index is greater than number of items in array" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/bar", "value": 42}],
"error": "Object operation on array target" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"], "sil"],
"comment": "value in array add not flattened" },
{ "doc": {"foo": 1, "bar": [1, 2, 3, 4]},
"patch": [{"op": "remove", "path": "/bar"}],
"expected": {"foo": 1} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/0/qux"}],
"expected": {"foo": 1, "baz": [{}]} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} },
{ "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} },
{ "doc": ["foo"],
"patch": [{"op": "replace", "path": "/0", "value": "bar"}],
"expected": ["bar"] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": 0}],
"expected": [0] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": true}],
"expected": [true] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": false}],
"expected": [false] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": null}],
"expected": [null] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"]],
"comment": "value in array replace not flattened" },
{ "comment": "replace whole document",
"disabled": true,
"doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}],
"expected": {"baz": "qux"} },
{ "doc": {"foo": null},
"patch": [{"op": "replace", "path": "/foo", "value": "truthy"}],
"expected": {"foo": "truthy"},
"comment": "null value should be valid obj property to be replaced with something truthy" },
{ "doc": {"foo": null},
"patch": [{"op": "remove", "path": "/foo"}],
"expected": {},
"comment": "null value should be valid obj property to be removed" },
{ "doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "/foo", "value": null}],
"expected": {"foo": null},
"comment": "null value should still be valid obj property replace other value" },
{ "comment": "test remove with bad number should fail",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/1e0/qux"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test remove on array",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/0"}],
"expected": [2, 3, 4] },
{ "comment": "test repeated removes",
"doc": [1, 2, 3, 4],
"patch": [{ "op": "remove", "path": "/1" },
{ "op": "remove", "path": "/3" }],
"expected": [1, 3] },
{ "comment": "test remove with bad index should fail",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/1e0"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test replace with bad number should fail",
"doc": [""],
"patch": [{"op": "replace", "path": "/1e0", "value": false}],
"error": "replace op shouldn't replace in array with bad number" },
{ "comment": "test add with bad number should fail",
"doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1e0", "value": "bar"}],
"error": "add op shouldn't add to array with bad number" },
{ "comment": "missing 'value' parameter to add",
"doc": [ 1 ],
"patch": [ { "op": "add", "path": "/-" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing 'value' parameter to replace",
"doc": [ 1 ],
"patch": [ { "op": "replace", "path": "/0" } ],
"error": "missing 'value' parameter" },
{ "comment": "unrecognized op should fail",
"doc": {"foo": 1},
"patch": [{"op": "spam", "path": "/foo", "value": 1}],
"error": "Unrecognized op 'spam'" }
]

View file

@ -0,0 +1,28 @@
<?php
namespace OpenCloud\Test\Common\JsonSchema;
use OpenCloud\Common\JsonSchema\JsonPatch;
use OpenCloud\Test\TestCase;
class JsonPatchTest extends TestCase
{
public function testAll()
{
$fixtures = json_decode(file_get_contents(__DIR__ . '/Fixtures/jsonPatchTests.json'));
foreach ($fixtures as $fixture) {
if (isset($fixture->disabled) || !isset($fixture->expected)) {
continue;
}
$actual = JsonPatch::diff($fixture->doc, $fixture->expected);
$this->assertEquals(
json_encode($fixture->patch, JSON_UNESCAPED_SLASHES),
json_encode($actual, JSON_UNESCAPED_SLASHES),
isset($fixture->comment) ? sprintf("Failed asserting test: %s\n", $fixture->comment) : ''
);
}
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace OpenCloud\Test\Common\JsonSchema;
use JsonSchema\Validator;
use OpenCloud\Common\JsonSchema\Schema;
use OpenCloud\Test\TestCase;
class SchemaTest extends TestCase
{
/** @var Schema */
private $schema;
/** @var Validator */
private $validator;
private $body;
public function setUp()
{
$this->body = [
'properties' => [
'foo' => (object)[],
'bar' => (object)[],
'baz' => (object)['readOnly' => true],
],
];
$this->validator = $this->prophesize(Validator::class);
$this->schema = new Schema($this->body, $this->validator->reveal());
}
public function test_it_gets_errors()
{
$this->validator->getErrors()
->shouldBeCalled()
->willReturn([]);
$this->assertEquals([], $this->schema->getErrors());
}
public function test_it_gets_error_string()
{
$this->validator->getErrors()
->shouldBeCalled()
->willReturn([['property' => 'foo', 'message' => 'bar']]);
$errorMsg = sprintf("Provided values do not validate. Errors:\n[foo] bar\n");
$this->assertEquals($errorMsg, $this->schema->getErrorString());
}
public function test_it_gets_property_paths()
{
$this->assertEquals(['/foo', '/bar', '/baz'], $this->schema->getPropertyPaths());
}
public function test_it_ignores_readOnly_attrs()
{
$expected = (object)[
'foo' => true,
'bar' => false,
];
$subject = (object)[
'foo' => true,
'bar' => false,
'baz' => true,
];
$this->assertEquals((object)$expected, $this->schema->normalizeObject((object)$subject, []));
}
public function test_it_stocks_aliases()
{
$subject = (object)[
'fooAlias' => true,
'bar' => false,
'other' => true,
];
$expected = (object)[
'foo' => true,
'bar' => false,
];
$this->assertEquals($expected, $this->schema->normalizeObject($subject, ['foo' => 'fooAlias', 'bar' => 'lol']));
}
public function test_it_validates()
{
$this->validator->check([], (object) $this->body)->shouldBeCalled();
$this->schema->validate([]);
}
public function test_it_checks_validity()
{
$this->validator->isValid()->shouldBeCalled();
$this->schema->isValid();
}
}

View file

@ -0,0 +1,161 @@
<?php
namespace OpenCloud\Test\Common\Resource;
use function GuzzleHttp\Psr7\stream_for;
use GuzzleHttp\Psr7\Response;
use OpenCloud\Common\Resource\AbstractResource;
use OpenCloud\Common\Resource\Generator;
use OpenCloud\Test\Fixtures\ComputeV2Api;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class AbstractResourceTest extends TestCase
{
private $resource;
public function setUp()
{
parent::setUp();
$this->rootFixturesDir = __DIR__;
$this->resource = new TestResource($this->client->reveal(), new ComputeV2Api());
}
public function test_it_populates_from_response()
{
$response = new Response(200, ['Content-Type' => 'application/json'], stream_for(
json_encode(['foo' => ['bar' => '1']])
));
$this->resource->populateFromResponse($response);
$this->assertEquals('1', $this->resource->bar);
}
public function test_it_populates_datetimes_from_arrays()
{
$dt = new \DateTimeImmutable('2015');
$this->resource->populateFromArray(['created' => '2015']);
$this->assertEquals($this->resource->created, $dt);
}
public function test_it_populates_arrays_from_arrays()
{
$this->resource->populateFromArray(['children' => [$this->resource, $this->resource]]);
$this->assertInstanceOf(TestResource::class, $this->resource->children[0]);
}
public function test_it_gets_attrs()
{
$this->resource->bar = 'foo';
$this->assertEquals(['bar' => 'foo'], $this->resource->getAttrs(['bar']));
}
public function test_it_executes_with_state()
{
$this->resource->id = 'foo';
$this->resource->bar = 'bar';
$expectedJson = ['id' => 'foo', 'bar' => 'bar'];
$this->setupMock('GET', 'foo', $expectedJson, [], new Response(204));
$this->resource->executeWithState((new ComputeV2Api())->test());
}
public function test_it_executes_operations_until_a_204_is_received()
{
$this->client
->request('GET', 'servers', ['headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$this->client
->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []])
->shouldBeCalled()
->willReturn(new Response(204));
$count = 0;
$api = new ComputeV2Api();
foreach ($this->resource->enumerate($api->getServers()) as $item) {
$count++;
$this->assertInstanceOf(TestResource::class, $item);
}
$this->assertEquals(5, $count);
}
public function test_it_invokes_function_if_provided()
{
$this->client
->request('GET', 'servers', ['headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$this->client
->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []])
->shouldBeCalled()
->willReturn(new Response(204));
$api = new ComputeV2Api();
$count = 0;
$fn = function () use (&$count) {
$count++;
};
foreach ($this->resource->enumerate($api->getServers(), [], $fn) as $item) {
}
$this->assertEquals(5, $count);
}
public function test_it_halts_when_user_provided_limit_is_reached()
{
$this->client
->request('GET', 'servers', ['query' => ['limit' => 2], 'headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$count = 0;
$api = new ComputeV2Api();
foreach ($this->resource->enumerate($api->getServers(), ['limit' => 2]) as $item) {
$count++;
}
$this->assertEquals(2, $count);
}
}
class TestResource extends AbstractResource
{
protected $resourceKey = 'foo';
protected $resourcesKey = 'servers';
protected $markerKey = 'id';
/** @var string */
public $bar;
public $id;
/** @var \DateTimeImmutable */
public $created;
/** @var []TestResource */
public $children;
public function getAttrs(array $keys)
{
return parent::getAttrs($keys);
}
}

View file

@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": []
}

View file

@ -0,0 +1,77 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": [
{
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server1"
},
{
"id": "2",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server2"
},
{
"id": "3",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server3"
},
{
"id": "4",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server4"
},
{
"id": "5",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server5"
}
]
}

View file

@ -0,0 +1,77 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": [
{
"id": "6",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server6"
},
{
"id": "7",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server7"
},
{
"id": "8",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server8"
},
{
"id": "9",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server9"
},
{
"id": "10",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server10"
}
]
}

View file

@ -0,0 +1,164 @@
<?php
namespace OpenCloud\Test\Common\Service;
use GuzzleHttp\ClientInterface;
use OpenCloud\Common\Service\Builder;
use OpenCloud\Identity\v2\Models\Token;
use OpenCloud\Identity\v2\Service as IdentityV2;
use OpenCloud\Identity\v3\Service as IdentityV3;
use OpenCloud\Compute\v2\Service as ComputeV2;
use OpenCloud\Test\Common\Auth\FakeToken;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class BuilderTest extends TestCase
{
private $builder;
private $opts;
public function setUp()
{
$this->builder = new Builder([]);
$this->opts = [
'username' => '1',
'password' => '2',
'tenantId' => '3',
'authUrl' => '4',
'region' => '5',
'catalogName' => '6',
'catalogType' => '7',
];
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_if_username_is_missing()
{
$this->builder->createService('Compute', 2, []);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_password_is_missing()
{
$this->builder->createService('Compute', 2, ['username' => 1]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_both_tenantId_and_tenantName_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6, 'catalogType' => 7,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_authUrl_is_missing()
{
$this->builder->createService('Compute', 2, ['username' => 1, 'password' => 2, 'tenantId' => 3]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_region_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_catalogName_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_catalogType_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6,
]);
}
// public function test_it_builds_services_with_custom_identity_service()
// {
// $this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v2/';
//
// $token = $this->prophesize(FakeToken::class)->reveal();
// $service = $this->prophesize(IdentityService::class);
// $service->authenticate(Argument::type('array'))->shouldBeCalled()->willReturn([$token, '']);
//
// $this->opts += [
// 'identityService' => $service->reveal(),
// 'catalogName' => 'nova',
// 'catalogType' => 'compute',
// 'region' => 'RegionOne',
// ];
//
// $service = $this->builder->createService('Compute', 2, $this->opts);
// $this->assertInstanceOf(ComputeV2::class, $service);
// }
private function setupHttpClient()
{
$this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v3/';
$response = $this->getFixture('token-get');
$expectedJson = [
'auth' => [
'identity' => [
'methods' => ['password'],
'password' => ['user' => ['id' => '0ca8f6', 'password' => 'secretsecret']]
]
]
];
$httpClient = $this->prophesize(ClientInterface::class);
$httpClient->request('POST', 'tokens', ['json' => $expectedJson])->shouldBeCalled()->willReturn($response);
return $httpClient;
}
public function it_builds_services_with_default_identity()
{
$httpClient = $this->setupHttpClient();
$options = [
'httpClient' => $httpClient->reveal(),
'catalogName' => 'nova',
'catalogType' => 'compute',
'region' => 'RegionOne',
'user' => [
'id' => '0ca8f6',
'password' => 'secretsecret',
]
];
$service = $this->builder->createService('Compute', 2, $options);
$this->assertInstanceOf(ComputeV2::class, $service);
}
// public function test_it_does_not_authenticate_when_creating_identity_services()
// {
// $this->assertInstanceOf(IdentityV3::class, $this->builder->createService('Identity', 3, [
// 'authUrl' => 'foo.com',
// ]));
// }
}

View file

@ -0,0 +1,15 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use GuzzleHttp\Handler\MockHandler;
use OpenCloud\Common\Transport\HandlerStack;
use OpenCloud\Test\TestCase;
class HandlerStackTest extends TestCase
{
public function test_it_is_created()
{
$this->assertInstanceOf(HandlerStack::class, HandlerStack::create(new MockHandler()));
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Common\Transport\JsonSerializer;
class JsonSerializerTest extends \PHPUnit_Framework_TestCase
{
private $serializer;
public function setUp()
{
$this->serializer = new JsonSerializer();
}
public function test_it_embeds_params_according_to_path()
{
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(false);
$param->isObject()->shouldBeCalled()->willReturn(false);
$param->getName()->shouldBeCalled()->willReturn('username');
$param->getPath()->shouldBeCalled()->willReturn('auth.passwordCredentials');
$userValue = 'fooBar';
$expected = [
'auth' => [
'passwordCredentials' => [
'username' => $userValue,
],
],
];
$actual = $this->serializer->stockJson($param->reveal(), $userValue, []);
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_arrays()
{
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(true);
$param->getName()->shouldBeCalled()->willReturn('fooBar');
$param->getPath()->shouldBeCalled()->willReturn(false);
$itemSchema = $this->prophesize(Parameter::class);
$itemSchema->isArray()->shouldBeCalled()->willReturn(false);
$itemSchema->isObject()->shouldBeCalled()->willReturn(false);
$itemSchema->getName()->shouldBeCalled()->willReturn(null);
$itemSchema->getPath()->shouldBeCalled()->willReturn(null);
$param->getItemSchema()->shouldBeCalled()->willReturn($itemSchema);
$userValues = ['1', '2', '3'];
$expected = ['fooBar' => $userValues];
$actual = $this->serializer->stockJson($param->reveal(), $userValues, []);
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_objects()
{
$prop = $this->prophesize(Parameter::class);
$prop->isArray()->shouldBeCalled()->willReturn(false);
$prop->isObject()->shouldBeCalled()->willReturn(false);
$prop->getName()->shouldBeCalled()->willReturn('foo');
$prop->getPath()->shouldBeCalled()->willReturn(null);
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(false);
$param->isObject()->shouldBeCalled()->willReturn(true);
$param->getName()->shouldBeCalled()->willReturn('topLevel');
$param->getPath()->shouldBeCalled()->willReturn(false);
$param->getProperty('foo')->shouldBeCalled()->willReturn($prop);
$expected = ['topLevel' => ['foo' => true]];
$json = $this->serializer->stockJson($param->reveal(), ['foo' => true], []);
$this->assertEquals($expected, $json);
}
}

Some files were not shown because too many files have changed in this diff Show more