Add Balanced Payments API

Summary: Adds the Balanced PHP API to externals/. Ref T2787.

Test Plan: Used in next diff.

Reviewers: btrahan, chad

Reviewed By: chad

CC: aran, aurelijus

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5764
This commit is contained in:
epriestley
2013-04-25 09:47:30 -07:00
parent a8bc87578e
commit 23786784ef
95 changed files with 7994 additions and 0 deletions

12
externals/restful/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
# composer
.buildpath
composer.lock
composer.phar
vendor
# phar
*.phar
# eclipse-pdt
.settings
.project

8
externals/restful/.travis.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
language: php
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install --prefer-source
script: phpunit --bootstrap vendor/autoload.php tests/
php:
- 5.3
- 5.4

22
externals/restful/LICENSE vendored Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2012 Noone
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

111
externals/restful/README.md vendored Normal file
View File

@@ -0,0 +1,111 @@
# RESTful
Library for writing RESTful PHP clients.
[![Build Status](https://secure.travis-ci.org/bninja/restful.png)](http://travis-ci.org/bninja/restful)
The design of this library was heavily influenced by [Httpful](https://github.com/nategood/httpful).
## Requirements
- [PHP](http://www.php.net) >= 5.3 **with** [cURL](http://www.php.net/manual/en/curl.installation.php)
- [Httpful](https://github.com/nategood/httpful) >= 0.1
## Issues
Please use appropriately tagged github [issues](https://github.com/bninja/restful/issues) to request features or report bugs.
## Installation
You can install using [composer](#composer), a [phar](#phar) package or from [source](#source). Note that RESTful is [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) compliant:
### Composer
If you don't have Composer [install](http://getcomposer.org/doc/00-intro.md#installation) it:
$ curl -s https://getcomposer.org/installer | php
Add this to your `composer.json`:
{
"require": {
"bninja/restful": "*"
}
}
Refresh your dependencies:
$ php composer.phar update
Then make sure to `require` the autoloader and initialize both:
<?php
require(__DIR__ . '/vendor/autoload.php');
Httpful\Bootstrap::init();
RESTful\Bootstrap::init();
...
### Phar
Download an Httpful [phar](http://php.net/manual/en/book.phar.php) file, which are all [here](https://github.com/nategood/httpful/downloads):
$ curl -s -L -o httpful.phar https://github.com/downloads/nategood/httpful/httpful.phar
Download a RESTful [phar](http://php.net/manual/en/book.phar.php) file, which are all [here](https://github.com/bninja/restful/downloads):
$ curl -s -L -o restful.phar https://github.com/bninja/restful/downloads/restful-{VERSION}.phar
And then `include` both:
<?php
include(__DIR__ . '/httpful.phar');
include(__DIR__ . '/restful.phar');
...
### Source
Download [Httpful](https://github.com/nategood/httpful) source:
$ curl -s -L -o httpful.zip https://github.com/nategood/httpful/zipball/master;
$ unzip httpful.zip; mv nategood-httpful* httpful; rm httpful.zip
Download the RESTful source:
$ curl -s -L -o restful.zip https://github.com/bninja/restful/zipball/master
$ unzip restful.zip; mv bninja-restful-* restful; rm restful.zip
And then `require` both bootstrap files:
<?php
require(__DIR__ . "/httpful/bootstrap.php")
require(__DIR__ . "/restful/bootstrap.php")
...
## Usage
TODO
## Testing
$ phpunit --bootstrap vendor/autoload.php tests/
## Publishing
1. Ensure that **all** [tests](#testing) pass
2. Increment minor `VERSION` in `src/RESTful/Settings` and `composer.json` (`git commit -am 'v{VERSION} release'`)
3. Tag it (`git tag -a v{VERSION} -m 'v{VERSION} release'`)
4. Push the tag (`git push --tag`)
5. [Packagist](http://packagist.org/packages/bninja/restful) will see the new tag and take it from there
6. Build (`build-phar`) and upload a [phar](http://php.net/manual/en/book.phar.php) file
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Write your code **and [tests](#testing)**
4. Ensure all tests still pass (`phpunit --bootstrap vendor/autoload.php tests/`)
5. Commit your changes (`git commit -am 'Add some feature'`)
6. Push to the branch (`git push origin my-new-feature`)
7. Create new pull request

4
externals/restful/bootstrap.php vendored Normal file
View File

@@ -0,0 +1,4 @@
<?php
require(__DIR__ . '/src/RESTful/Bootstrap.php');
\RESTful\Bootstrap::init();

36
externals/restful/build-phar vendored Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/php
<?php
include('src/RESTful/Settings.php');
function exit_unless($condition, $msg = null) {
if ($condition)
return;
echo "[FAIL] $msg";
exit(1);
}
echo "Building Phar... ";
$base_dir = dirname(__FILE__);
$source_dir = $base_dir . '/src/RESTful/';
$phar_name = 'restful.phar';
$phar_path = $base_dir . '/' . $phar_name;
$phar = new Phar($phar_path, 0, $phar_name);
$stub = <<<HEREDOC
<?php
// Phar Stub File
Phar::mapPhar('restful.phar');
include('phar://restful.phar/RESTful/Bootstrap.php');
\RESTful\Bootstrap::pharInit();
__HALT_COMPILER();
HEREDOC;
$phar->setStub($stub);
exit_unless($phar, "Unable to create a phar. Make sure you have phar.readonly=0 set in your ini file.");
$phar->buildFromDirectory(dirname($source_dir));
echo "[ OK ]\n";
echo "Renaming Phar... ";
$phar_versioned_name = 'restful-' . \RESTful\Settings::VERSION . '.phar';
$phar_versioned_path = $base_dir . '/' . $phar_versioned_name;
rename($phar_path, $phar_versioned_path);
echo "[ OK ]\n";

18
externals/restful/composer.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "bninja/restful",
"description": "Library for writing RESTful PHP clients.",
"homepage": "http://github.com/bninja/restful",
"license": "MIT",
"keywords": ["http", "api", "client", "rest"],
"version": "0.1.7",
"authors": [
],
"require": {
"nategood/httpful": "*"
},
"autoload": {
"psr-0": {
"RESTful": "src/"
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace RESTful;
/**
* Bootstrapper for RESTful does autoloading.
*/
class Bootstrap
{
const DIR_SEPARATOR = DIRECTORY_SEPARATOR;
const NAMESPACE_SEPARATOR = '\\';
public static $initialized = false;
public static function init()
{
spl_autoload_register(array('\RESTful\Bootstrap', 'autoload'));
}
public static function autoload($classname)
{
self::_autoload(dirname(dirname(__FILE__)), $classname);
}
public static function pharInit()
{
spl_autoload_register(array('\RESTful\Bootstrap', 'pharAutoload'));
}
public static function pharAutoload($classname)
{
self::_autoload('phar://restful.phar', $classname);
}
private static function _autoload($base, $classname)
{
$parts = explode(self::NAMESPACE_SEPARATOR, $classname);
$path = $base . self::DIR_SEPARATOR . implode(self::DIR_SEPARATOR, $parts) . '.php';
if (file_exists($path)) {
require_once($path);
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace RESTful;
use RESTful\Exceptions\HTTPError;
use RESTful\Settings;
class Client
{
public function __construct($settings_class, $request_class = null, $convert_error = null)
{
$this->request_class = $request_class == null ? '\Httpful\Request' : $request_class;
$this->settings_class = $settings_class;
$this->convert_error = $convert_error;
}
public function get($uri)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::get($url);
return $this->_op($request);
}
public function post($uri, $payload)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::post($url, $payload, 'json');
return $this->_op($request);
}
public function put($uri, $payload)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::put($url, $payload, 'json');
return $this->_op($request);
}
public function delete($uri)
{
$settings_class = $this->settings_class;
$url = $settings_class::$url_root . $uri;
$request_class = $this->request_class;
$request = $request_class::delete($url);
return $this->_op($request);
}
private function _op($request)
{
$settings_class = $this->settings_class;
$user_agent = $settings_class::$agent . '/' . $settings_class::$version;
$request->headers['User-Agent'] = $user_agent;
if ($settings_class::$api_key != null) {
$request = $request->authenticateWith($settings_class::$api_key, '');
}
$request->expects('json');
$response = $request->sendIt();
if ($response->hasErrors() || $response->code == 300) {
if ($this->convert_error != null) {
$error = call_user_func($this->convert_error, $response);
} else {
$error = new HTTPError($response);
}
throw $error;
}
return $response;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace RESTful;
class Collection extends Itemization
{
public function __construct($resource, $uri, $data = null)
{
parent::__construct($resource, $uri, $data);
$this->_parseUri();
}
private function _parseUri()
{
$parsed = parse_url($this->uri);
$this->_uri = $parsed['path'];
if (array_key_exists('query', $parsed)) {
foreach (explode('&', $parsed['query']) as $param) {
$param = explode('=', $param);
$key = urldecode($param[0]);
$val = (count($param) == 1) ? null : urldecode($param[1]);
// size
if ($key == 'limit') {
$this->_size = $val;
}
}
}
}
public function create($payload)
{
$class = $this->resource;
$client = $class::getClient();
$response = $client->post($this->uri, $payload);
return new $this->resource($response->body);
}
public function query()
{
return new Query($this->resource, $this->uri);
}
public function paginate()
{
return new Pagination($this->resource, $this->uri);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace RESTful\Exceptions;
/**
* Base class for all RESTful\Exceptions.
*/
class Base extends \Exception
{
}

View File

@@ -0,0 +1,28 @@
<?php
namespace RESTful\Exceptions;
/**
* Indicates an HTTP level error has occurred. The underlying HTTP response is
* stored as response member. The response payload fields if any are stored as
* members of the same name.
*
* @see \Httpful\Response
*/
class HTTPError extends Base
{
public $response;
public function __construct($response)
{
$this->response = $response;
$this->_objectify($this->response->body);
}
protected function _objectify($fields)
{
foreach ($fields as $key => $val) {
$this->$key = $val;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace RESTful\Exceptions;
/**
* Indicates that a query unexpectedly returned multiple results when at most
* one was expected.
*/
class MultipleResultsFound extends Base
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace RESTful\Exceptions;
/**
* Indicates that a query unexpectedly returned no results.
*/
class NoResultFound extends Base
{
}

85
externals/restful/src/RESTful/Field.php vendored Normal file
View File

@@ -0,0 +1,85 @@
<?php
namespace RESTful;
class Field
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
public function __get($name)
{
return new Field($this->name . '.' . $name);
}
public function in($vals)
{
return new FilterExpression($this->name, 'in', $vals, '!in');
}
public function startswith($prefix)
{
if (!is_string($prefix)) {
throw new \InvalidArgumentException('"startswith" prefix must be a string');
}
return new FilterExpression($this->name, 'contains', $prefix);
}
public function endswith($suffix)
{
if (!is_string($suffix)) {
throw new \InvalidArgumentException('"endswith" suffix must be a string');
}
return new FilterExpression($this->name, 'contains', $suffix);
}
public function contains($fragment)
{
if (!is_string($fragment)) {
throw new \InvalidArgumentException('"contains" fragment must be a string');
}
return new FilterExpression($this->name, 'contains', $fragment, '!contains');
}
public function eq($val)
{
return new FilterExpression($this->name, '=', $val, '!eq');
}
public function lt($val)
{
return new FilterExpression($this->name, '<', $val, '>=');
}
public function lte($val)
{
return new FilterExpression($this->name, '<=', $val, '>');
}
public function gt($val)
{
return new FilterExpression($this->name, '>', $val, '<=');
}
public function gte($val)
{
return new FilterExpression($this->name, '>=', $val, '<');
}
public function asc()
{
return new SortExpression($this->name, true);
}
public function desc()
{
return new SortExpression($this->name, false);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace RESTful;
class Fields
{
public function __get($name)
{
return new Field($name);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace RESTful;
class FilterExpression
{
public $field,
$op,
$val,
$not_op;
public function __construct($field, $op, $val, $not_op = null)
{
$this->field = $field;
$this->op = $op;
$this->val = $val;
$this->not_op = $not_op;
}
public function not()
{
if (null === $this->not_op) {
throw new \LogicException(sprintf('Filter cannot be inverted'));
}
$temp = $this->op;
$this->op = $this->not_op;
$this->not_op = $temp;
return $this;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace RESTful;
class Itemization implements \IteratorAggregate, \ArrayAccess
{
public $resource,
$uri;
protected $_page,
$_offset = 0,
$_size = 25;
public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
$this->uri = $uri;
if ($data != null) {
$this->_page = new Page($resource, $uri, $data);
} else {
$this->_page = null;
}
}
protected function _getPage($offset = null)
{
if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
} elseif ($offset != null) {
$offset = $offset * $this->_size;
if ($offset != $this->_offset) {
$this->_offset = $offset;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
}
return $this->_page;
}
protected function _getItem($offset)
{
$page_offset = floor($offset / $this->_size);
$page = $this->_getPage($page_offset);
return $page->items[$offset - $page->offset];
}
public function total()
{
return $this->_getPage()->total;
}
protected function _buildUri($offset = null)
{
# TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?';
} else {
$uri = $this->uri . '&';
}
$uri = $uri . 'offset=' . strval($offset);
return $uri;
}
// IteratorAggregate
public function getIterator()
{
$uri = $this->_buildUri($offset = 0);
$uri = $this->_buildUri($offset = 0);
return new ItemizationIterator($this->resource, $uri);
}
// ArrayAccess
public function offsetSet($offset, $value)
{
throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
}
public function offsetExists($offset)
{
return (0 <= $offset && $offset < $this->total());
}
public function offsetUnset($offset)
{
throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
}
public function offsetGet($offset)
{
return $this->_getItem($offset);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace RESTful;
class ItemizationIterator implements \Iterator
{
protected $_page,
$_offset = 0;
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page->items[$this->_offset];
}
public function key()
{
return $this->_page->offset + $this->_offset;
}
public function next()
{
$this->_offset += 1;
if ($this->_offset >= count($this->_page->items)) {
$this->_offset = 0;
$this->_page = $this->_page->next();
}
}
public function rewind()
{
$this->_page = $this->_page->first();
$this->_offset = 0;
}
public function valid()
{
return ($this->_page != null && $this->_offset < count($this->_page->items));
}
}

72
externals/restful/src/RESTful/Page.php vendored Normal file
View File

@@ -0,0 +1,72 @@
<?php
namespace RESTful;
class Page
{
public $resource,
$total,
$items,
$offset,
$limit;
private $_first_uri,
$_previous_uri,
$_next_uri,
$_last_uri;
public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
if ($data == null) {
$client = $resource::getClient();
$data = $client->get($uri)->body;
}
$this->total = $data->total;
$this->items = array_map(
function ($x) use ($resource) {
return new $resource($x);
},
$data->items);
$this->offset = $data->offset;
$this->limit = $data->limit;
$this->_first_uri = property_exists($data, 'first_uri') ? $data->first_uri : null;
$this->_previous_uri = property_exists($data, 'previous_uri') ? $data->previous_uri : null;
$this->_next_uri = property_exists($data, 'next_uri') ? $data->next_uri : null;
$this->_last_uri = property_exists($data, 'last_uri') ? $data->last_uri : null;
}
public function first()
{
return new Page($this->resource, $this->_first_uri);
}
public function next()
{
if (!$this->hasNext()) {
return null;
}
return new Page($this->resource, $this->_next_uri);
}
public function hasNext()
{
return $this->_next_uri != null;
}
public function previous()
{
return new Page($this->resource, $this->_previous_uri);
}
public function hasPrevious()
{
return $this->_previous_uri != null;
}
public function last()
{
return new Page($this->resource, $this->_last_uri);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace RESTful;
class Pagination implements \IteratorAggregate, \ArrayAccess
{
public $resource,
$uri;
protected $_page,
$_offset = 0,
$_size = 25;
public function __construct($resource, $uri, $data = null)
{
$this->resource = $resource;
$this->uri = $uri;
if ($data != null) {
$this->_page = new Page($resource, $uri, $data);
} else {
$this->_page = null;
}
}
protected function _getPage($offset = null)
{
if ($this->_page == null) {
$this->_offset = ($offset == null) ? 0 : $offset * $this->_size;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
} elseif ($offset != null) {
$offset = $offset * $this->_size;
if ($offset != $this->_offset) {
$this->_offset = $offset;
$uri = $this->_buildUri();
$this->_page = new Page($this->resource, $uri);
}
}
return $this->_page;
}
public function total()
{
return floor($this->_getPage()->total / $this->_size);
}
protected function _buildUri($offset = null)
{
# TODO: hacky but works for now
$offset = ($offset == null) ? $this->_offset : $offset;
if (strpos($this->uri, '?') === false) {
$uri = $this->uri . '?';
} else {
$uri = $this->uri . '&';
}
$uri = $uri . 'offset=' . strval($offset);
return $uri;
}
// IteratorAggregate
public function getIterator()
{
$uri = $this->_buildUri($offset = 0);
return new PaginationIterator($this->resource, $uri);
}
// ArrayAccess
public function offsetSet($offset, $value)
{
throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
}
public function offsetExists($offset)
{
return (0 <= $offset && $offset < $this->total());
}
public function offsetUnset($offset)
{
throw new \BadMethodCallException(get_class($this) . ' array access is read-only');
}
public function offsetGet($offset)
{
return $this->_getPage($offset);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace RESTful;
class PaginationIterator implements \Iterator
{
public function __construct($resource, $uri, $data = null)
{
$this->_page = new Page($resource, $uri, $data);
}
// Iterator
public function current()
{
return $this->_page;
}
public function key()
{
return $this->_page->index;
}
public function next()
{
$this->_page = $this->_page->next();
}
public function rewind()
{
$this->_page = $this->_page->first();
}
public function valid()
{
return $this->_page != null;
}
}

161
externals/restful/src/RESTful/Query.php vendored Normal file
View File

@@ -0,0 +1,161 @@
<?php
namespace RESTful;
use RESTful\Exceptions\NoResultFound;
use RESTful\Exceptions\MultipleResultsFound;
class Query extends Itemization
{
public $filters = array(),
$sorts = array(),
$size;
public function __construct($resource, $uri)
{
parent::__construct($resource, $uri);
$this->size = $this->_size;
$this->_parseUri($uri);
}
private function _parseUri($uri)
{
$parsed = parse_url($uri);
$this->uri = $parsed['path'];
if (array_key_exists('query', $parsed)) {
foreach (explode('&', $parsed['query']) as $param) {
$param = explode('=', $param);
$key = urldecode($param[0]);
$val = (count($param) == 1) ? null : urldecode($param[1]);
// limit
if ($key == 'limit') {
$this->size = $this->_size = $val;
} // sorts
else if ($key == 'sort') {
array_push($this->sorts, $val);
} // everything else
else {
if (!array_key_exists($key, $this->filters)) {
$this->filters[$key] = array();
}
if (!is_array($val)) {
$val = array($val);
}
$this->filters[$key] = array_merge($this->filters[$key], $val);
}
}
}
}
protected function _buildUri($offset = null)
{
// params
$params = array_merge(
$this->filters,
array(
'sort' => $this->sorts,
'limit' => $this->_size,
'offset' => ($offset == null) ? $this->_offset : $offset
)
);
$getSingle = function ($v) {
if (is_array($v) && count($v) == 1)
return $v[0];
return $v;
};
$params = array_map($getSingle, $params);
// url encode params
// NOTE: http://stackoverflow.com/a/8171667/1339571
$qs = http_build_query($params);
$qs = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $qs);
return $this->uri . '?' . $qs;
}
private function _reset()
{
$this->_page = null;
}
public function filter($expression)
{
if ($expression->op == '=') {
$field = $expression->field;
} else {
$field = $expression->field . '[' . $expression->op . ']';
}
if (is_array($expression->val)) {
$val = implode(',', $expression->val);
} else {
$val = $expression->val;
}
if (!array_key_exists($field, $this->filters)) {
$this->filters[$field] = array();
}
array_push($this->filters[$field], $val);
$this->_reset();
return $this;
}
public function sort($expression)
{
$dir = $expression->ascending ? 'asc' : 'desc';
array_push($this->sorts, $expression->field . ',' . $dir);
$this->_reset();
return $this;
}
public function limit($limit)
{
$this->size = $this->_size = $limit;
$this->_reset();
return $this;
}
public function all()
{
$items = array();
foreach ($this as $item) {
array_push($items, $item);
}
return $items;
}
public function first()
{
$prev_size = $this->_size;
$this->_size = 1;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
$item = count($page->items) != 0 ? $page->items[0] : null;
return $item;
}
public function one()
{
$prev_size = $this->_size;
$this->_size = 2;
$page = new Page($this->resource, $this->_buildUri());
$this->_size = $prev_size;
if (count($page->items) == 1) {
return $page->items[0];
}
if (count($page->items) == 0) {
throw new NoResultFound();
}
throw new MultipleResultsFound();
}
public function paginate()
{
return new Pagination($this->resource, $this->_buildUri());
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace RESTful;
class Registry
{
protected $_resources = array();
public function add($resource)
{
array_push($this->_resources, $resource);
}
public function match($uri)
{
foreach ($this->_resources as $resource) {
$spec = $resource::getURISpec();
$result = $spec->match($uri);
if ($result == null) {
continue;
}
$result['class'] = $resource;
return $result;
}
return null;
}
}

View File

@@ -0,0 +1,205 @@
<?php
namespace RESTful;
abstract class Resource
{
protected $_collection_uris,
$_member_uris;
public static function getClient()
{
$class = get_called_class();
return $class::$_client;
}
public static function getRegistry()
{
$class = get_called_class();
return $class::$_registry;
}
public static function getURISpec()
{
$class = get_called_class();
return $class::$_uri_spec;
}
public function __construct($fields = null)
{
if ($fields == null) {
$fields = array();
}
$this->_objectify($fields);
}
public function __get($name)
{
// collection uri
if (array_key_exists($name, $this->_collection_uris)) {
$result = $this->_collection_uris[$name];
$this->$name = new Collection($result['class'], $result['uri']);
return $this->$name;
} // member uri
else if (array_key_exists($name, $this->_member_uris)) {
$result = $this->$_collection_uris[$name];
$response = self::getClient() . get($result['uri']);
$class = $result['class'];
$this->$name = new $class($response->body);
return $this->$name;
}
// unknown
$trace = debug_backtrace();
trigger_error(
sprintf('Undefined property via __get(): %s in %s on line %s', $name, $trace[0]['file'], $trace[0]['line']),
E_USER_NOTICE
);
return null;
}
public function __isset($name)
{
if (array_key_exists($name, $this->_collection_uris) || array_key_exists($name, $this->_member_uris)) {
return true;
}
return false;
}
protected function _objectify($fields)
{
// initialize uris
$this->_collection_uris = array();
$this->_member_uris = array();
foreach ($fields as $key => $val) {
// nested uri
if ((strlen($key) - 3) == strrpos($key, 'uri', 0) && $key != 'uri') {
$result = self::getRegistry()->match($val);
if ($result != null) {
$name = substr($key, 0, -4);
$class = $result['class'];
if ($result['collection']) {
$this->_collection_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
} else {
$this->_member_uris[$name] = array(
'class' => $class,
'uri' => $val,
);
}
continue;
}
} elseif (is_object($val) && property_exists($val, 'uri')) {
// nested
$result = self::getRegistry()->match($val->uri);
if ($result != null) {
$class = $result['class'];
if ($result['collection']) {
$this->$key = new Collection($class, $val['uri'], $val);
} else {
$this->$key = new $class($val);
}
continue;
}
} elseif (is_array($val) && array_key_exists('uri', $val)) {
$result = self::getRegistry()->match($val['uri']);
if ($result != null) {
$class = $result['class'];
if ($result['collection']) {
$this->$key = new Collection($class, $val['uri'], $val);
} else {
$this->$key = new $class($val);
}
continue;
}
}
// default
$this->$key = $val;
}
}
public static function query()
{
$uri_spec = self::getURISpec();
if ($uri_spec == null || $uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly query %s resources', get_called_class());
throw new \LogicException($msg);
}
return new Query(get_called_class(), $uri_spec->collection_uri);
}
public static function get($uri)
{
# id
if (strncmp($uri, '/', 1)) {
$uri_spec = self::getURISpec();
if ($uri_spec == null || $uri_spec->collection_uri == null) {
$msg = sprintf('Cannot get %s resources by id %s', $class, $uri);
throw new \LogicException($msg);
}
$uri = $uri_spec->collection_uri . '/' . $uri;
}
$response = self::getClient()->get($uri);
$class = get_called_class();
return new $class($response->body);
}
public function save()
{
// payload
$payload = array();
foreach ($this as $key => $val) {
if ($key[0] == '_' || is_object($val)) {
continue;
}
$payload[$key] = $val;
}
// update
if (array_key_exists('uri', $payload)) {
$uri = $payload['uri'];
unset($payload['uri']);
$response = self::getClient()->put($uri, $payload);
} else {
// create
$class = get_class($this);
if ($class::$_uri_spec == null || $class::$_uri_spec->collection_uri == null) {
$msg = sprintf('Cannot directly create %s resources', $class);
throw new \LogicException($msg);
}
$response = self::getClient()->post($class::$_uri_spec->collection_uri, $payload);
}
// re-objectify
foreach ($this as $key => $val) {
unset($this->$key);
}
$this->_objectify($response->body);
return $this;
}
public function delete()
{
self::getClient()->delete($this->uri);
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace RESTful;
/**
* Settings.
*
*/
class Settings
{
const VERSION = '0.1.7';
}

View File

@@ -0,0 +1,15 @@
<?php
namespace RESTful;
class SortExpression
{
public $name,
$ascending;
public function __construct($field, $ascending = true)
{
$this->field = $field;
$this->ascending = $ascending;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace RESTful;
class URISpec
{
public $collection_uri = null,
$name,
$idNames;
public function __construct($name, $idNames, $root = null)
{
$this->name = $name;
if (!is_array($idNames)) {
$idNames = array($idNames);
}
$this->idNames = $idNames;
if ($root != null) {
if ($root == '' || substr($root, -1) == '/') {
$this->collection_uri = $root . $name;
} else {
$this->collection_uri = $root . '/' . $name;
}
}
}
public function match($uri)
{
$parts = explode('/', rtrim($uri, "/"));
// collection
if ($parts[count($parts) - 1] == $this->name) {
return array(
'collection' => true,
);
}
// non-member
if (count($parts) < count($this->idNames) + 1 ||
$parts[count($parts) - 1 - count($this->idNames)] != $this->name
) {
return null;
}
// member
$ids = array_combine(
$this->idNames,
array_slice($parts, -count($this->idNames))
);
$result = array(
'collection' => false,
'ids' => $ids,
);
return $result;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace RESTful\Test;
\RESTful\Bootstrap::init();
\Httpful\Bootstrap::init();
use RESTful\URISpec;
use RESTful\Client;
use RESTful\Registry;
use RESTful\Fields;
use RESTful\Query;
use RESTful\Page;
class Settings
{
public static $url_root = 'http://api.example.com';
public static $agent = 'example-php';
public static $version = '0.1.0';
public static $api_key = null;
}
class Resource extends \RESTful\Resource
{
public static $fields, $f;
protected static $_client, $_registry, $_uri_spec;
public static function init()
{
self::$_client = new Client('Settings');
self::$_registry = new Registry();
self::$f = self::$fields = new Fields();
}
public static function getClient()
{
$class = get_called_class();
return $class::$_client;
}
public static function getRegistry()
{
$class = get_called_class();
return $class::$_registry;
}
public static function getURISpec()
{
$class = get_called_class();
return $class::$_uri_spec;
}
}
Resource::init();
class A extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('as', 'id', '/');
self::$_registry->add(get_called_class());
}
}
A::init();
class B extends Resource
{
protected static $_uri_spec = null;
public static function init()
{
self::$_uri_spec = new URISpec('bs', 'id', '/');
self::$_registry->add(get_called_class());
}
}
B::init();
class URISpecTest extends \PHPUnit_Framework_TestCase
{
public function testNoRoot()
{
$uri_spec = new URISpec('grapes', 'seed');
$this->assertEquals($uri_spec->collection_uri, null);
$result = $uri_spec->match('/some/raisins');
$this->assertEquals($result, null);
$result = $uri_spec->match('/some/grapes');
$this->assertEquals($result, array('collection' => true));
$result = $uri_spec->match('/some/grapes/1234');
$expected = array(
'collection' => false,
'ids' => array('seed' => '1234')
);
$this->assertEquals($expected, $result);
}
public function testSingleId()
{
$uri_spec = new URISpec('tomatoes', 'stem', '/v1');
$this->assertNotEquals($uri_spec->collection_uri, null);
$result = $uri_spec->match('/some/tomatoes/that/are/green');
$this->assertEquals($result, null);
$result = $uri_spec->match('/some/tomatoes');
$this->assertEquals($result, array('collection' => true));
$result = $uri_spec->match('/some/tomatoes/4321');
$expected = array(
'collection' => false,
'ids' => array('stem' => '4321')
);
$this->assertEquals($expected, $result);
}
public function testMultipleIds()
{
$uri_spec = new URISpec('tomatoes', array('stem', 'root'), '/v1');
$this->assertNotEquals($uri_spec->collection_uri, null);
$result = $uri_spec->match('/some/tomatoes/that/are/green');
$this->assertEquals($result, null);
$result = $uri_spec->match('/some/tomatoes');
$this->assertEquals($result, array('collection' => true));
$result = $uri_spec->match('/some/tomatoes/4321/1234');
$expected = array(
'collection' => false,
'ids' => array('stem' => '4321', 'root' => '1234')
);
$this->assertEquals($expected, $result);
}
}
class QueryTest extends \PHPUnit_Framework_TestCase
{
public function testParse()
{
$uri = '/some/uri?field2=123&sort=field5%2Cdesc&limit=101&field3.field4%5Bcontains%5D=hi';
$query = new Query('Resource', $uri);
$expected = array(
'field2' => array('123'),
'field3.field4[contains]' => array('hi')
);
$this->assertEquals($query->filters, $expected);
$expected = array('field5,desc');
$this->assertEquals($query->sorts, $expected);
$this->assertEquals($query->size, 101);
}
public function testBuild()
{
$query = new Query('Resource', '/some/uri');
$query->filter(Resource::$f->name->eq('Wonka Chocs'))
->filter(Resource::$f->support_email->endswith('gmail.com'))
->filter(Resource::$f->variable_fee_percentage->gte(3.5))
->sort(Resource::$f->name->asc())
->sort(Resource::$f->variable_fee_percentage->desc())
->limit(101);
$this->assertEquals(
$query->filters,
array(
'name' => array('Wonka Chocs'),
'support_email[contains]' => array('gmail.com'),
'variable_fee_percentage[>=]'=> array(3.5)
)
);
$this->assertEquals(
$query->sorts,
array('name,asc', 'variable_fee_percentage,desc')
);
$this->assertEquals(
$query->size,
101
);
}
}
class PageTest extends \PHPUnit_Framework_TestCase
{
public function testConstruct()
{
$data = new \stdClass();
$data->first_uri = 'some/first/uri';
$data->previous_uri = 'some/previous/uri';
$data->next_uri = null;
$data->last_uri = 'some/last/uri';
$data->limit= 25;
$data->offset = 0;
$data->total = 101;
$data->items = array();
$page = new Page(
'Resource',
'/some/uri',
$data
);
$this->assertEquals($page->resource, 'Resource');
$this->assertEquals($page->total, 101);
$this->assertEquals($page->items, array());
$this->assertTrue($page->hasPrevious());
$this->assertFalse($page->hasNext());
}
}
class ResourceTest extends \PHPUnit_Framework_TestCase
{
public function testQuery()
{
$query = A::query();
$this->assertEquals(get_class($query), 'RESTful\Query');
}
public function testObjectify()
{
$a = new A(array(
'uri' => '/as/123',
'field1' => 123,
'b' => array(
'uri' => '/bs/321',
'field2' => 321
))
);
$this->assertEquals(get_class($a), 'RESTful\Test\A');
$this->assertEquals($a->field1, 123);
$this->assertEquals(get_class($a->b), 'RESTful\Test\B');
$this->assertEquals($a->b->field2, 321);
}
}

8
externals/restful/tests/phpunit.xml vendored Normal file
View File

@@ -0,0 +1,8 @@
<phpunit>
<testsuite name="RESTful">
<directory>.</directory>
</testsuite>
<logging>
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
</logging>
</phpunit>