-
+*~
+/.buildpath
+/.externalToolBuilders/
+/.idea/
+/.project
+/.settings/
+/clover.xml
+/code-coverage-report/
+/composer.lock
+/nbproject/
+/perf*
+/tmp/
+/vendor/
"autoload": {
"psr-4": {
"seekat\\": "lib/"
- }
+ },
+ "files": [
+ "lib/functions.php"
+ ]
},
"require": {
"php": "^7.0",
"ext-http": "^3.0",
"react/promise": "^2.4",
"seebz/uri-template": "dev-master",
- "psr/log": "^1.0"
+ "psr/log": "^1.0",
+ "psr/cache": "^1.0",
+ "psr/simple-cache": "^1.0"
},
"require-dev": {
"peridot-php/peridot": "^1.15",
--- /dev/null
+#!/usr/bin/env php
+<?php
+
+require __DIR__."/../vendor/autoload.php";
+
+use Monolog\{
+ Handler\StreamHandler, Logger
+};
+
+$log = new Logger("seekat");
+$log->pushHandler(new StreamHandler(STDERR, Logger::INFO));
+
+$redis = new Redis;
+$redis->connect("localhost");
+$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
+$cache = new class($redis) implements \seekat\API\Call\Cache\Service {
+ private $redis;
+ function __construct(Redis $redis) {
+ $this->redis = $redis;
+ }
+ function clear() {
+ return $this->redis->flushDB();
+ }
+ function fetch(string $key, \http\Client\Response &$response = null): bool {
+ list($exists, $response) = $this->redis
+ ->multi()
+ ->exists($key)
+ ->get($key)
+ ->exec();
+ return $exists;
+ }
+ function store(string $key, \http\Client\Response $response): bool {
+ return $this->redis->set($key, $response);
+ }
+};
+
+$api = new seekat\API([
+ "Authorization" => "token ".getenv("GITHUB_TOKEN")
+], null, null, $log, $cache);
+
+$api(function($api) use($cache) {
+ yield $api->users->m6w6();
+ yield $api->users->m6w6();
+ $cache->clear();
+});
--- /dev/null
+<?php
+
+require_once __DIR__."/../vendor/autoload.php";
+
+$api = new seekat\API([
+ "Authorization" => "token ".getenv("GITHUB_TOKEN")
+]);
+array_shift($argv);
+
+($self = function($api) use(&$self) {
+ global $argv;
+
+ while (null !== ($arg = array_shift($argv))) {
+ if ("." === $arg) {
+ $api->then($self);
+ return;
+ }
+ $api = $api->$arg;
+ }
+
+ echo $api, "\n";
+})($api);
+
+$api->send();
--- /dev/null
+<?php
+
+
+class EvHandler implements http\Client\Curl\User
+{
+ private $client;
+ private $run;
+ private $ios = [];
+ private $timeout;
+
+ function __construct(http\Client $client) {
+ $this->client = $client;
+ }
+
+ function init($run) {
+ $this->run = $run;
+ }
+
+ function timer(int $timeout_ms) {
+ if (isset($this->timeout)) {
+ $this->timeout->set($timeout_ms/1000, 0);
+ $this->timeout->start();
+ } else {
+ $this->timeout = new EvTimer($timeout_ms/1000, 0, function() {
+ if (!call_user_func($this->run, $this->client)) {
+ if ($this->timeout) {
+ $this->timeout->stop();
+ $this->timeout = null;
+ }
+ }
+ });
+ }
+ }
+
+ function socket($socket, int $action) {
+ switch ($action) {
+ case self::POLL_NONE:
+ break;
+ case self::POLL_REMOVE:
+ echo "U";
+ if (isset($this->ios[(int) $socket])) {
+ $this->ios[(int) $socket]->stop();
+ unset($this->ios[(int) $socket]);
+ }
+ break;
+ default:
+ $ev = 0;
+ if ($action & self::POLL_IN) {
+ $ev |= Ev::READ;
+ }
+ if ($action & self::POLL_OUT) {
+ $ev |= Ev::WRITE;
+ }
+ if (isset($this->ios[(int) $socket])) {
+ $this->ios[(int) $socket]->set($socket, $ev);
+ } else {
+ $this->ios[(int) $socket] = new EvIo($socket, $ev, function($watcher, $events) use($socket) {
+ $action = 0;
+ if ($events & Ev::READ) {
+ $action |= self::POLL_IN;
+ }
+ if ($events & Ev::WRITE) {
+ $action |= self::POLL_OUT;
+ }
+ if (!call_user_func($this->run, $this->client, $socket, $action)) {
+ if ($this->timeout) {
+ $this->timeout->stop();
+ $this->timeout = null;
+ }
+ }
+ });
+ }
+ break;
+ }
+ }
+
+ function once() {
+ echo "O";
+ Ev::run(EV::RUN_NOWAIT);
+ }
+ function wait(int $timeout_ms = null) {
+ echo "W";
+ Ev::run(EV::RUN_ONCE);
+ }
+ function send() {
+ echo "!";
+ Ev::verify();
+ Ev::run();
+ }
+}
require_once __DIR__."/../vendor/autoload.php";
use seekat\API;
+use seekat\API\Links;
$log = new Monolog\Logger("seekat");
$log->pushHandler((new Monolog\Handler\StreamHandler(STDERR))->setLevel(Monolog\Logger::INFO));
$events = yield $api->repos->m6w6->{"ext-http"}->issues->events();
while ($events) {
/* pro-actively queue the next request */
- $next = $events->next();
+ $next = Links\next($events);
foreach ($events as $event) {
- if ($event->event == "labeled") {
+ if ($event->event == "labeled" || $event->event == "unlabeled") {
continue;
}
++$count;
require __DIR__."/../vendor/autoload.php";
$api = new seekat\API([
- "Authentication" => "token ".getenv("GUTHUB_TOKEN")
+ "Authorization" => "token ".getenv("GITHUB_TOKEN")
]);
$api(function($api) {
$gists = yield $api->users->m6w6->gists();
while ($gists) {
+ $next = \seekat\API\Links\next($gists);
+
foreach ($gists as $gist) {
foreach ($gist->files as $name => $file) {
if ($name == "blog.md") {
}
}
- $gists = yield $gists->next();
+ $gists = yield $next;
}
});
$cli->configure([
"max_host_connections" => 10,
"max_total_connections" => 50,
+ "use_eventloop" => false,
]);
$log = new Monolog\Logger("seekat");
"affiliation" => "owner"
]);
while ($repos) {
- $next = $repos->next();
+ $next = next($repos);
$batch = [];
foreach ($repos as $repo) {
namespace seekat;
use Countable;
-use Exception;
use Generator;
use http\{
- Client,
- Client\Request,
- Client\Response,
- Header,
- Message\Body,
- QueryString,
- Url
+ Client, Client\Request, Message\Body, QueryString, Url
};
-use InvalidArgumentException;
use IteratorAggregate;
use Psr\Log\{
- LoggerInterface,
- NullLogger
-};
-use seekat\{
- API\Call,
- API\ContentType,
- API\Invoker,
- API\Iterator,
- API\Links,
- Exception\RequestException
+ LoggerInterface, NullLogger
};
use React\Promise\{
- ExtendedPromiseInterface,
- function reject,
- function resolve
+ ExtendedPromiseInterface, function resolve
+};
+use seekat\{
+ API\Call, API\Consumer, API\ContentType, API\Iterator, API\Links, Exception\InvalidArgumentException
};
-use Throwable;
-use UnexpectedValueException;
class API implements IteratorAggregate, Countable {
/**
* The current API endpoint URL
* @var Url
*/
- private $__url;
+ private $url;
/**
* Logger
* @var LoggerInterface
*/
- private $__log;
+ private $logger;
+
+ /**
+ * Cache
+ * @var Call\Cache\Service
+ */
+ private $cache;
/**
* The HTTP client
* @var Client
*/
- private $__client;
+ private $client;
/**
* Default headers to send out to the API endpoint
* @var array
*/
- private $__headers;
+ private $headers;
/**
* Current endpoint data's Content-Type
- * @var Header
+ * @var API\ContentType
*/
- private $__type;
+ private $type;
/**
* Current endpoint's data
* @var array|object
*/
- private $__data;
+ private $data;
/**
* Current endpoints links
* @var Links
*/
- private $__links;
+ private $links;
/**
* Create a new API endpoint root
* @param Client $client The HTTP client to use for executing requests
* @param LoggerInterface $log A logger
*/
- function __construct(array $headers = null, Url $url = null, Client $client = null, LoggerInterface $log = null) {
- $this->__log = $log ?? new NullLogger;
- $this->__url = $url ?? new Url("https://api.github.com");
- $this->__client = $client ?? new Client;
- $this->__headers = (array) $headers + [
+ function __construct(array $headers = null, Url $url = null, Client $client = null, LoggerInterface $log = null, Call\Cache\Service $cache = null) {
+ $this->cache = $cache;
+ $this->logger = $log ?? new NullLogger;
+ $this->url = $url ?? new Url("https://api.github.com");
+ $this->client = $client ?? new Client;
+ $this->headers = (array) $headers + [
"Accept" => "application/vnd.github.v3+json"
];
}
*/
function __get($seg) : API {
if (substr($seg, -4) === "_url") {
- $url = new Url(uri_template($this->__data->$seg));
+ $url = new Url(uri_template($this->data->$seg));
$that = $this->withUrl($url);
- $seg = basename($that->__url->path);
+ $seg = basename($that->url->path);
} else {
$that = clone $this;
- $that->__url->path .= "/".urlencode($seg);
- $this->exists($seg, $that->__data);
+ $that->url->path .= "/".urlencode($seg);
+ $this->exists($seg, $that->data);
}
- $this->__log->debug(__FUNCTION__."($seg)", [
+ $this->logger->debug(__FUNCTION__."($seg)", [
"url" => [
- (string) $this->__url,
- (string) $that->__url
+ (string) $this->url,
+ (string) $that->url
],
]);
* ->users->m6w6->gists(...)
*/
if (is_callable(current($args))) {
- return $this->$method->get()->then(current($args));
+ return $this->api->get()->then(current($args));
}
- /* standard access */
- if ($this->exists($method)) {
- return $this->$method->get(...$args);
+ return (new Call($this, $method))($args);
+ }
+
+ /**
+ * Run the send loop through a generator
+ *
+ * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
+ * @return ExtendedPromiseInterface The promise of the generator's return value
+ * @throws InvalidArgumentException
+ */
+ function __invoke($cbg) : ExtendedPromiseInterface {
+ $this->logger->debug(__FUNCTION__);
+
+ $consumer = new Consumer($this->client);
+
+ invoke:
+ if ($cbg instanceof Generator) {
+ return $consumer($cbg);
}
- /* fetch resource, unless already localized, and try for {$method}_url */
- return $this->$method->get(...$args)->otherwise(function($error) use($method, $args) {
- if ($error instanceof Throwable) {
- $message = $error->getMessage();
- } else {
- $message = $error;
- $error = new Exception($error);
- }
- if ($this->exists($method."_url", $url)) {
-
- $this->__log->info(__FUNCTION__."($method): ". $message, [
- "url" => (string) $this->__url
- ]);
-
- $url = new Url(uri_template($url, (array) current($args)));
- return $this->withUrl($url)->get(...$args);
- }
-
- $this->__log->error(__FUNCTION__."($method): ". $message, [
- "url" => (string) $this->__url
- ]);
+ if (is_callable($cbg)) {
+ $cbg = $cbg($this);
+ goto invoke;
+ }
- throw $error;
- });
+ throw InvalidArgumentException(
+ "Expected callable or Generator, got ".(
+ is_object($cbg)
+ ? "instance of ".get_class($cbg)
+ : gettype($cbg).": ".var_export($cbg, true)
+ )
+ );
}
/**
* Clone handler ensuring the underlying url will be cloned, too
*/
function __clone() {
- $this->__url = clone $this->__url;
+ $this->url = clone $this->url;
}
/**
* @return string
*/
function __toString() : string {
- if (is_scalar($this->__data)) {
- return (string) $this->__data;
+ if (is_scalar($this->data)) {
+ return (string) $this->data;
}
/* FIXME */
- return json_encode($this->__data);
+ return json_encode($this->data);
}
/**
- * Import handler for the endpoint's underlying data
- *
- * \seekat\Call will call this when the request will have finished.
+ * Create an iterator over the endpoint's underlying data
*
- * @param Response $response
- * @return API self
- * @throws UnexpectedValueException
- * @throws RequestException
- * @throws \Exception
+ * @return Iterator
*/
- function import(Response $response) : API {
- $this->__log->info(__FUNCTION__.": ". $response->getInfo(), [
- "url" => (string) $this->__url
- ]);
-
- if ($response->getResponseCode() >= 400) {
- $e = new RequestException($response);
-
- $this->__log->critical(__FUNCTION__.": ".$e->getMessage(), [
- "url" => (string) $this->__url,
- ]);
+ function getIterator() : Iterator {
+ return new Iterator($this);
+ }
- throw $e;
- }
+ /**
+ * Count the underlying data's entries
+ *
+ * @return int
+ */
+ function count() : int {
+ return count($this->data);
+ }
- if (!($type = $response->getHeader("Content-Type", Header::class))) {
- $e = new RequestException($response);
- $this->__log->error(
- __FUNCTION__.": Empty Content-Type -> ".$e->getMessage(), [
- "url" => (string) $this->__url,
- ]);
- throw $e;
- }
+ /**
+ * @return Url
+ */
+ function getUrl() : Url {
+ return $this->url;
+ }
- try {
- $this->__type = new ContentType($type);
- $this->__data = $this->__type->parseBody($response->getBody());
+ /**
+ * @return LoggerInterface
+ */
+ function getLogger() : LoggerInterface {
+ return $this->logger;
+ }
- if (($link = $response->getHeader("Link", Header::class))) {
- $this->__links = new Links($link);
- }
- } catch (\Exception $e) {
- $this->__log->error(__FUNCTION__.": ".$e->getMessage(), [
- "url" => (string) $this->__url
- ]);
+ /**
+ * @return Client
+ */
+ public function getClient(): Client {
+ return $this->client;
+ }
- throw $e;
- }
+ /**
+ * @return array|object
+ */
+ function getData() {
+ return $this->data;
+ }
- return $this;
+ /**
+ * Accessor to any hypermedia links
+ *
+ * @return null|Links
+ */
+ function getLinks() {
+ return $this->links;
}
/**
* Export the endpoint's underlying data
*
- * @param
- * @return mixed
+ * @return array ["url", "data", "type", "links", "headers"]
+ */
+ function export() : array {
+ $data = $this->data;
+ $url = clone $this->url;
+ $type = clone $this->type;
+ $links = $this->links ? clone $this->links : null;
+ $headers = $this->headers;
+ return compact("url", "data", "type", "links", "headers");
+ }
+
+ /**
+ * @param $export
+ * @return API
*/
- function export(&$type = null) {
- $type = clone $this->__type;
- return $this->__data;
+ function with($export) : API {
+ $that = clone $this;
+ if (is_array($export) || ($export instanceof \ArrayAccess)) {
+ isset($export["url"]) && $that->url = $export["url"];
+ isset($export["data"]) && $that->data = $export["data"];
+ isset($export["type"]) && $that->type = $export["type"];
+ isset($export["links"]) && $that->links = $export["links"];
+ isset($export["headers"]) && $that->headers = $export["headers"];
+ }
+ return $that;
}
/**
*/
function withData($data) : API {
$that = clone $this;
- $that->__data = $data;
+ $that->data = $data;
return $that;
}
* @return API clone
*/
function withUrl(Url $url) : API {
- $that = $this->withData(null);
- $that->__url = $url;
+ $that = clone $this;
+ $that->url = $url;
+ $that->data = null;
+ #$that->links = null;
return $that;
}
function withHeader(string $name, $value) : API {
$that = clone $this;
if (isset($value)) {
- $that->__headers[$name] = $value;
+ $that->headers[$name] = $value;
} else {
- unset($that->__headers[$name]);
+ unset($that->headers[$name]);
}
return $that;
}
*
* Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
*
- * @param string $type The expected return data type, e.g. "raw", "html", etc.
+ * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
* @param bool $keepdata Whether to keep already fetched data
* @return API clone
*/
function as(string $type, bool $keepdata = true) : API {
- switch(substr($type, 0, 1)) {
- case "+":
- case ".":
- case "":
- break;
- default:
- $type = ".$type";
- break;
- }
- $vapi = ContentType::version();
- $that = $this->withHeader("Accept", "application/vnd.github.v$vapi$type");
+ $that = ContentType::apply($this, $type);
if (!$keepdata) {
- $that->__data = null;
+ $that->data = null;
}
return $that;
}
- /**
- * Create an iterator over the endpoint's underlying data
- *
- * @return Iterator
- */
- function getIterator() : Iterator {
- return new Iterator($this);
- }
-
- /**
- * Count the underlying data's entries
- *
- * @return int
- */
- function count() : int {
- return count($this->__data);
- }
-
/**
* Perform a GET request against the endpoint's underlying URL
*
* @param array $headers The request's additional HTTP headers
* @return ExtendedPromiseInterface
*/
- function get($args = null, array $headers = null) : ExtendedPromiseInterface {
- return $this->__xfer("GET", $args, null, $headers);
+ function get($args = null, array $headers = null, $cache = null) : ExtendedPromiseInterface {
+ return $this->request("GET", $args, null, $headers, $cache);
}
/**
* @return ExtendedPromiseInterface
*/
function delete($args = null, array $headers = null) : ExtendedPromiseInterface {
- return $this->__xfer("DELETE", $args, null, $headers);
+ return $this->request("DELETE", $args, null, $headers);
}
/**
* @return ExtendedPromiseInterface
*/
function post($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
- return $this->__xfer("POST", $args, $body, $headers);
+ return $this->request("POST", $args, $body, $headers);
}
/**
* @return ExtendedPromiseInterface
*/
function put($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
- return $this->__xfer("PUT", $args, $body, $headers);
+ return $this->request("PUT", $args, $body, $headers);
}
/**
* @return ExtendedPromiseInterface
*/
function patch($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
- return $this->__xfer("PATCH", $args, $body, $headers);
- }
-
- /**
- * Accessor to any hypermedia links
- *
- * @return null|Links
- */
- function links() {
- return $this->__links;
- }
-
- /**
- * Perform a GET request against the link's "first" relation
- *
- * @return ExtendedPromiseInterface
- */
- function first() : ExtendedPromiseInterface {
- if ($this->links() && ($first = $this->links()->getFirst())) {
- return $this->withUrl($first)->get();
- }
- return reject($this->links());
- }
-
- /**
- * Perform a GET request against the link's "prev" relation
- *
- * @return ExtendedPromiseInterface
- */
- function prev() : ExtendedPromiseInterface {
- if ($this->links() && ($prev = $this->links()->getPrev())) {
- return $this->withUrl($prev)->get();
- }
- return reject($this->links());
- }
-
- /**
- * Perform a GET request against the link's "next" relation
- *
- * @return ExtendedPromiseInterface
- */
- function next() : ExtendedPromiseInterface {
- if ($this->links() && ($next = $this->links()->getNext())) {
- return $this->withUrl($next)->get();
- }
- return reject($this->links());
- }
-
- /**
- * Perform a GET request against the link's "last" relation
- *
- * @return ExtendedPromiseInterface
- */
- function last() : ExtendedPromiseInterface {
- if ($this->links() && ($last = $this->links()->getLast())) {
- return $this->withUrl($last)->get();
- }
- return reject($this->links());
+ return $this->request("PATCH", $args, $body, $headers);
}
/**
* @return API self
*/
function send() : API {
- $this->__log->debug(__FUNCTION__.": start loop");
- while (count($this->__client)) {
- $this->__client->send();
+ $this->logger->debug(__FUNCTION__.": start loop");
+ while (count($this->client)) {
+ $this->client->send();
}
- $this->__log->debug(__FUNCTION__.": end loop");
+ $this->logger->debug(__FUNCTION__.": end loop");
return $this;
}
- /**
- * Run the send loop through a generator
- *
- * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
- * @return ExtendedPromiseInterface The promise of the generator's return value
- * @throws InvalidArgumentException
- */
- function __invoke($cbg) : ExtendedPromiseInterface {
- $this->__log->debug(__FUNCTION__);
-
- $invoker = new Invoker($this->__client);
-
- if ($cbg instanceof Generator) {
- return $invoker->iterate($cbg)->promise();
- }
-
- if (is_callable($cbg)) {
- return $invoker->invoke(function() use($cbg) {
- return $cbg($this);
- })->promise();
- }
-
- throw InvalidArgumentException(
- "Expected callable or Generator, got ".(
- is_object($cbg)
- ? "instance of ".get_class($cbg)
- : gettype($cbg).": ".var_export($cbg, true)
- )
- );
- }
-
/**
* Check for a specific key in the endpoint's underlying data
*
* @return bool
*/
function exists($seg, &$val = null) : bool {
- if (is_array($this->__data) && array_key_exists($seg, $this->__data)) {
- $val = $this->__data[$seg];
+ if (is_array($this->data) && array_key_exists($seg, $this->data)) {
+ $val = $this->data[$seg];
$exists = true;
- } elseif (is_object($this->__data) && property_exists($this->__data, $seg)) {
- $val = $this->__data->$seg;
+ } elseif (is_object($this->data) && property_exists($this->data, $seg)) {
+ $val = $this->data->$seg;
$exists = true;
} else {
$val = null;
$exists = false;
}
- $this->__log->debug(__FUNCTION__."($seg) in ".(
- is_object($this->__data)
- ? get_class($this->__data)
- : gettype($this->__data)
+ $this->logger->debug(__FUNCTION__."($seg) in ".(
+ is_object($this->data)
+ ? get_class($this->data)
+ : gettype($this->data)
)." -> ".(
$exists
? "true"
: "false"
), [
- "url" => (string) $this->__url,
+ "url" => (string) $this->url,
"val" => $val,
]);
* @param mixed $args The HTTP query string parameters
* @param mixed $body Thee HTTP message's body
* @param array $headers The request's additional HTTP headers
+ * @param Call\Cache\Service $cache
* @return ExtendedPromiseInterface
*/
- private function __xfer(string $method, $args = null, $body = null, array $headers = null) : ExtendedPromiseInterface {
- if (isset($this->__data)) {
- $this->__log->debug(__FUNCTION__."($method) -> resolve", [
- "url" => (string) $this->__url,
- "args" => $args,
- "body" => $body,
+ private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service $cache = null) : ExtendedPromiseInterface {
+ if (isset($this->data)) {
+ $this->logger->debug("request -> resolve", [
+ "method" => $method,
+ "url" => (string)$this->url,
+ "args" => $args,
+ "body" => $body,
"headers" => $headers,
]);
return resolve($this);
}
- $url = $this->__url->mod(["query" => new QueryString($args)]);
- $request = new Request($method, $url, ((array) $headers) + $this->__headers,
+ $url = $this->url->mod(["query" => new QueryString($args)]);
+ $request = new Request($method, $url, ((array) $headers) + $this->headers,
$body = is_array($body) ? json_encode($body) : (
is_resource($body) ? new Body($body) : (
is_scalar($body) ? (new Body)->append($body) :
$body)));
- $this->__log->info(__FUNCTION__."($method) -> request", [
- "url" => (string) $this->__url,
- "args" => $this->__url->query,
+ $this->logger->info("request -> deferred", [
+ "method" => $method,
+ "url" => (string) $this->url,
+ "args" => $this->url->query,
"body" => $body,
"headers" => $headers,
]);
- return (new Call($this, $this->__client, $request))->promise();
+ return (new Call\Deferred($this, $request, $cache ?: $this->cache))->promise();
}
}
namespace seekat\API;
-use Exception;
-use http\ {
- Client,
- Client\Request,
- Client\Response
-};
-use React\Promise\Deferred;
+use http\Url;
+use React\Promise\ExtendedPromiseInterface;
use seekat\API;
-use SplObserver;
-use SplSubject;
+use seekat\Exception;
-class Call extends Deferred implements SplObserver
+class Call
{
/**
- * The endpoint
* @var API
*/
private $api;
/**
- * The HTTP client
- * @var Client
+ * @var string
*/
- private $client;
+ private $call;
- /**
- * The executed request
- * @var Request
- */
- private $request;
-
- /**
- * The promised response
- * @var Response
- */
- private $response;
-
- /**
- * Create a deferred promise for the response of $request
- *
- * @param API $api The endpoint of the request
- * @param Client $client The HTTP client to send the request
- * @param Request $request The request to execute
- */
- function __construct(API $api, Client $client, Request $request) {
+ function __construct(API $api, string $call) {
$this->api = $api;
- $this->client = $client;
- $this->request = $request;
-
- parent::__construct(function($resolve, $reject) {
- return $this->cancel($resolve, $reject);
- });
-
- $client->attach($this);
- $client->enqueue($request, function(Response $response) {
- $this->response = $response;
- $this->complete(
- [$this, "resolve"],
- [$this, "reject"]
- );
- return true;
- });
- /* start off */
- $client->once();
+ $this->call = $call;
}
- /**
- * Progress observer
- *
- * Import the response's data on success and resolve the promise.
- *
- * @param SplSubject $client The observed HTTP client
- * @param Request $request The request which generated the update
- * @param object $progress The progress information
- */
- function update(SplSubject $client, Request $request = null, $progress = null) {
- if ($request !== $this->request) {
- return;
- }
+ function __invoke(array $args) : ExtendedPromiseInterface {
+ $promise = $this->api->{$this->call}->get(...$args);
- $this->notify((object) compact("client", "request", "progress"));
- }
+ /* fetch resource, unless already localized, and try for {$method}_url */
+ if (!$this->api->exists($this->call)) {
+ $promise = $promise->otherwise(function ($error) use($args) {
+ if ($this->api->exists($this->call."_url", $url)) {
+ $url = new Url(uri_template($url, (array)current($args)));
+ return $this->api->withUrl($url)->get(...$args);
+ }
- /**
- * Completion callback
- * @param callable $resolve
- * @param callable $reject
- */
- private function complete(callable $resolve, callable $reject) {
- $this->client->detach($this);
+ $message = Exception\message($error);
+ $this->api->getLogger()->error("call($this->call): " . $message, [
+ "url" => (string) $this->api->getUrl()
+ ]);
- if ($this->response) {
- try {
- $resolve($this->api->import($this->response));
- } catch (Exception $e) {
- $reject($e);
- }
- } else {
- $reject($this->client->getTransferInfo($this->request)->error);
+ throw $error;
+ });
}
- }
- /**
- * Cancellation callback
- * @param callable $resolve
- * @param callable $reject
- */
- private function cancel(callable $resolve, callable $reject) {
- /* did we finish in the meantime? */
- if ($this->response) {
- $this->complete($resolve, $reject);
- } else {
- $this->client->detach($this);
- $this->client->dequeue($this->request);
- $reject("Cancelled");
- }
+ return $promise;
}
}
--- /dev/null
+<?php
+
+namespace seekat\API\Call;
+
+use http\Client\Request;
+use http\Client\Response;
+use seekat\API\Call\Cache\Control;
+use seekat\API\Call\Cache\Service;
+use seekat\API\Call\Cache\Service\Hollow;
+
+
+class Cache
+{
+ /**
+ * @var Service
+ */
+ private $cache;
+
+ /**
+ * @param Service $cache
+ */
+ public function __construct(Service $cache = null) {
+ $this->cache = $cache ?? new Hollow;
+ }
+
+ /**
+ * Save call data
+ * @param Request $request
+ * @param Response $response
+ * @return bool
+ */
+ public function save(Request $request, Response $response) : bool {
+ $ctl = new Control($request);
+ if (!$ctl->isValid()) {
+ return false;
+ }
+
+ $time = time();
+ if ($time - 1 <= $response->getHeader("X-Cache-Time")) {
+ return true;
+ }
+ $response->setHeader("X-Cache-Time", $time);
+
+ return $this->cache->store($ctl->getKey(), $response);
+ }
+
+ /**
+ * Attempt to load call data
+ * @param Request $request
+ * @param Response $response out param
+ * @return bool
+ */
+ public function load(Request $request, Response &$response = null) : bool {
+ $ctl = new Control($request);
+ if (!$ctl->isValid()) {
+ return false;
+ }
+
+ if (!$this->cache->fetch($ctl->getKey(), $response)) {
+ return false;
+ }
+ if ($ctl->isStale($response)) {
+ if (($lmod = $response->getHeader("Last-Modified"))) {
+ $request->setOptions(["lastmodified" => strtotime($lmod)]);
+ }
+ if (($etag = $response->getHeader("ETag"))) {
+ $request->setOptions(["etag" => $etag]);
+ }
+ return false;
+ }
+ return true;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call\Cache;
+
+use http\Client\Request;
+use http\Client\Response;
+use http\Header;
+use http\Params;
+use http\QueryString;
+use http\Url;
+
+final class Control
+{
+ /**
+ * @var string
+ */
+ private $key;
+
+ /**
+ * @param Request $request
+ */
+ function __construct(Request $request) {
+ $method = $request->getRequestMethod();
+ switch ($method) {
+ case "HEAD":
+ case "GET":
+ $uid = $this->extractAuth($request);
+ $url = $request->getRequestUrl();
+ $this->key = "seekat call $uid $method $url";
+ break;
+ default:
+ $this->key = "";
+ break;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() : bool {
+ return strlen($this->key) > 0;
+ }
+
+ /**
+ * @return string
+ */
+ public function getKey() : string {
+ return $this->key;
+ }
+
+ /**
+ * @param Response $response
+ * @return bool
+ */
+ public function isStale(Response $response) : bool {
+ if (false === $response->getHeader("Cache-Control") &&
+ false === $response->getHeader("Expires")) {
+ return false;
+ }
+
+ $max_age = $this->extractMaxAge($response);
+ $cur_age = time() - $response->getHeader("X-Cache-Time");
+
+ return $max_age >= 0 && $max_age < $cur_age;
+ }
+
+ /**
+ * @param Response $response
+ * @return int
+ */
+ private function extractMaxAge(Response $response) : int {
+ /* @var Header $date
+ * @var Header $control
+ * @var Header $expires
+ */
+ $control = $response->getHeader("Cache-Control", Header::class);
+ if ($control) {
+ /* @var Params $params */
+ $params = $control->getParams();
+ if (isset($params["max-age"])) {
+ return (int) $params->params["max-age"]["value"];
+ }
+ }
+
+ $expires = $response->getHeader("Expires", Header::class);
+ if ($expires) {
+ if ($expires->match(0, Header::MATCH_FULL)) {
+ return 0;
+ }
+
+ $date = $response->getHeader("Date", Header::class);
+ if ($date) {
+ return strtotime($expires->value) - strtotime($date->value);
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * @param Request $request
+ * @return string
+ */
+ private function extractAuth(Request $request) : string {
+ $auth = $request->getHeader("Authorization");
+ if ($auth) {
+ return substr($auth, strpos($auth, " ") + 1);
+ }
+
+ $opts = $request->getOptions();
+ if (isset($opts["httpauth"])) {
+ return base64_encode($opts["httpauth"]);
+ }
+
+ $query = new QueryString((new Url($request->getRequestUrl()))->query);
+ return $query->getString("access_token", $query->getString("client_id", ""));
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call\Cache;
+
+use http\Client\Response;
+
+interface Service
+{
+ function fetch(string $key, Response &$response = null) : bool;
+ function store(string $key, Response $response) : bool;
+ function clear();
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call\Cache\Service;
+
+use http\Client\Response;
+use seekat\API\Call\Cache\Service;
+
+final class Hollow implements Service
+{
+ private $storage = [];
+
+ public function fetch(string $key, Response &$response = null) : bool {
+ if (isset($this->storage[$key])) {
+ $response = $this->storage[$key];
+ return true;
+ }
+ return false;
+ }
+
+ public function store(string $key, Response $response) : bool {
+ $this->storage[$key] = $response;
+ return true;
+ }
+
+ public function clear() {
+ $this->storage = [];
+ }
+
+ public function getStorage() : array {
+ return $this->storage;
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call\Cache\Service;
+
+use http\Client\Response;
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\CacheItemPoolInterface;
+use seekat\API\Call\Cache\Service;
+
+final class ItemPool implements Service
+{
+ /**
+ * @var CacheItemPoolInterface
+ */
+ private $cache;
+
+ /**
+ * @var CacheItemInterface
+ */
+ private $item;
+
+ public function __construct(CacheItemPoolInterface $cache) {
+ $this->cache = $cache;
+ }
+
+ public function fetch(string $key, Response &$response = null) : bool {
+ $this->item = $this->cache->getItem($key);
+ if ($this->item->isHit()) {
+ $response = $this->item->get();
+ return true;
+ }
+ return false;
+ }
+
+ public function store(string $key, Response $response) : bool {
+ $this->item->set($response);
+ return $this->cache->save($this->item);
+ }
+
+ public function clear() {
+ $this->cache->clear();
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call\Cache\Service;
+
+use http\Client\Response;
+use Psr\SimpleCache\CacheInterface;
+use seekat\API\Call\Cache\Service;
+
+final class Simple implements Service
+{
+ /**
+ * @var CacheInterface
+ */
+ private $cache;
+
+ public function __construct(CacheInterface $cache) {
+ $this->cache = $cache;
+ }
+
+ public function fetch(string $key, Response &$response = null) : bool {
+ $response = $this->cache->get($key);
+ return !!$response;
+ }
+
+ public function store(string $key, Response $response) : bool {
+ return $this->cache->set($key, $response);
+ }
+
+ public function clear() {
+ $this->cache->clear();
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call;
+
+use Exception;
+use http\{
+ Client, Client\Request, Client\Response
+};
+use React\Promise\ExtendedPromiseInterface;
+use seekat\API;
+use SplObserver;
+use SplSubject;
+
+class Deferred extends \React\Promise\Deferred implements SplObserver
+{
+ /**
+ * The response importer
+ *
+ * @var Result
+ */
+ private $result;
+
+ /**
+ * The HTTP client
+ *
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * Request cache
+ *
+ * @var callable
+ */
+ private $cache;
+
+ /**
+ * The executed request
+ *
+ * @var Request
+ */
+ private $request;
+
+ /**
+ * The promised response
+ *
+ * @var Response
+ */
+ private $response;
+
+ /**
+ * Create a deferred promise for the response of $request
+ *
+ * @param API $api The endpoint of the request
+ * @param Request $request The request to execute
+ * @param Cache\Service $cache
+ */
+ function __construct(API $api, Request $request, Cache\Service $cache = null) {
+ parent::__construct(function ($resolve, $reject) {
+ return $this->cancel($resolve, $reject);
+ });
+
+ $this->request = $request;
+ $this->client = $api->getClient();
+ $this->result = new Result($api);
+ $this->cache = new Cache($cache);
+
+ if ($this->cache->load($this->request, $cached)) {
+ $api->getLogger()->info("deferred -> cached", [
+ "method" => $request->getRequestMethod(),
+ "url" => $request->getRequestUrl(),
+ ]);
+
+ $this->response = $cached;
+ $this->complete(
+ [$this, "resolve"],
+ [$this, "reject"]
+ );
+ } else {
+ $this->client->attach($this);
+ $this->client->enqueue($this->request, function(Response $response) use($cached) {
+ if ($response->getResponseCode() == 304) {
+ $this->response = $cached;
+ } else {
+ $this->response = $response;
+ }
+ $this->complete(
+ [$this, "resolve"],
+ [$this, "reject"]
+ );
+ return true;
+ });
+ $api->getLogger()->info("deferred -> enqueued", [
+ "method" => $request->getRequestMethod(),
+ "url" => $request->getRequestUrl(),
+ ]);
+ /* start off */
+ $this->client->once();
+ }
+ }
+
+ /**
+ * Progress observer
+ *
+ * Import the response's data on success and resolve the promise.
+ *
+ * @param SplSubject $client The observed HTTP client
+ * @param Request $request The request which generated the update
+ * @param object $progress The progress information
+ */
+ function update(SplSubject $client, Request $request = null, $progress = null) {
+ if ($request !== $this->request) {
+ return;
+ }
+
+ $this->notify((object) compact("client", "request", "progress"));
+ }
+
+ /**
+ * Completion callback
+ * @param callable $resolve
+ * @param callable $reject
+ */
+ private function complete(callable $resolve, callable $reject) {
+ $this->client->detach($this);
+
+ if ($this->response) {
+ try {
+ $api = ($this->result)($this->response);
+
+ $this->cache->save($this->request, $this->response);
+
+ $resolve($api);
+ } catch (Exception $e) {
+ $reject($e);
+ }
+ } else {
+ $reject($this->client->getTransferInfo($this->request)->error);
+ }
+ }
+
+ /**
+ * Cancellation callback
+ * @param callable $resolve
+ * @param callable $reject
+ */
+ private function cancel(callable $resolve, callable $reject) {
+ /* did we finish in the meantime? */
+ if ($this->response) {
+ $this->complete($resolve, $reject);
+ } else {
+ $this->client->detach($this);
+ $this->client->dequeue($this->request);
+ $reject("Cancelled");
+ }
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API\Call;
+
+use http\Client\Response;
+use http\Header;
+use seekat\API;
+use seekat\Exception\RequestException;
+
+class Result
+{
+ private $api;
+
+ function __construct(API $api) {
+ $this->api = $api;
+ }
+
+ function __invoke(Response $response) : API {
+ $url = $this->api->getUrl();
+ $log = $this->api->getLogger();
+ $log->info(($response->getHeader("X-Cache-Time") ? "cached" : "enqueued")." -> response", [
+ "url" => (string) $url,
+ "info" => $response->getInfo(),
+ ]);
+
+ if ($response->getResponseCode() >= 400) {
+ $e = new RequestException($response);
+
+ $log->critical(__FUNCTION__.": ".$e->getMessage(), [
+ "url" => (string) $url,
+ ]);
+
+ throw $e;
+ }
+
+ if (!($type = $response->getHeader("Content-Type", Header::class))) {
+ $e = new RequestException($response);
+ $log->error(
+ __FUNCTION__.": Empty Content-Type -> ".$e->getMessage(), [
+ "url" => (string) $url,
+ ]);
+ throw $e;
+ }
+
+ try {
+ $type = new API\ContentType($type);
+ $data = $type->parseBody($response->getBody());
+
+ if (($link = $response->getHeader("Link", Header::class))) {
+ $links = new API\Links($link);
+ } else {
+ $links = null;
+ }
+
+ $this->api = $this->api->with(compact("type", "data", "links"));
+ } catch (\Exception $e) {
+ $log->error(__FUNCTION__.": ".$e->getMessage(), [
+ "url" => (string) $url
+ ]);
+
+ throw $e;
+ }
+
+ return $this->api;
+ }
+}
--- /dev/null
+<?php
+
+namespace seekat\API;
+
+use Generator;
+use http\Client;
+use React\Promise\{
+ Deferred,
+ ExtendedPromiseInterface,
+ PromiseInterface,
+ function all
+};
+
+class Consumer extends Deferred
+{
+ /**
+ * The HTTP client
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * The return value of the generator
+ * @var mixed
+ */
+ private $result;
+
+ /**
+ * Cancellation flag
+ * @var bool
+ */
+ private $cancelled = false;
+
+ /**
+ * Create a new generator invoker
+ * @param Client $client
+ */
+ function __construct(Client $client) {
+ $this->client = $client;
+
+ parent::__construct(function($resolve, $reject) {
+ return $this->cancel($resolve, $reject);
+ });
+ }
+
+ /**
+ * Iterate over $gen, a \Generator yielding promises
+ *
+ * @param Generator $gen
+ * @return ExtendedPromiseInterface
+ */
+ function __invoke(Generator $gen) : ExtendedPromiseInterface {
+ $this->cancelled = false;
+
+ foreach ($gen as $promise) {
+ if ($this->cancelled) {
+ break;
+ }
+ $this->give($promise, $gen);
+ }
+
+ if (!$this->cancelled) {
+ $this->resolve($this->result = $gen->getReturn());
+ }
+
+ return $this->promise();
+ }
+
+ /**
+ * Promise handler
+ *
+ * @param array|PromiseInterface $promise
+ * @param Generator $gen
+ */
+ private function give($promise, Generator $gen) {
+ if ($promise instanceof PromiseInterface) {
+ $promise->then(function($result) use($gen) {
+ if (($promise = $gen->send($result))) {
+ $this->give($promise, $gen);
+ }
+ });
+ } else {
+ all($promise)->then(function($results) use($gen) {
+ if (($promise = $gen->send($results))) {
+ $this->give($promise, $gen);
+ }
+ });
+ }
+ $this->client->send();
+ }
+
+ /**
+ * Cancellation callback
+ *
+ * @param callable $resolve
+ * @param callable $reject
+ */
+ private function cancel(callable $resolve, callable $reject) {
+ $this->cancelled = true;
+
+ /* did we finish in the meantime? */
+ if ($this->result) {
+ $resolve($this->result);
+ } else {
+ $reject("Cancelled");
+ }
+ }
+}
namespace seekat\API;
+use seekat\{
+ API, Exception\InvalidArgumentException, Exception\UnexpectedValueException
+};
use http\{
- Header,
- Message\Body
+ Header, Message\Body
};
-use InvalidArgumentException;
-use UnexpectedValueException;
-
class ContentType
{
/**
"diff" => "self::fromData",
"patch" => "self::fromData",
"text/plain"=> "self::fromData",
+ "application/octet-stream" => "self::fromStream",
];
/**
return $api;
}
+ /**
+ * @param API $api
+ * @param string $type
+ * @return API
+ */
+ static function apply(API $api, string $type) : API {
+ $part = "[^()<>@,;:\\\"\/\[\]?.=[:space:][:cntrl:]]+";
+ if (preg_match("/^$part\/$part\$/", $type)) {
+ $that = $api->withHeader("Accept", $type);
+ } else {
+ switch (substr($type, 0, 1)) {
+ case "+":
+ case ".":
+ case "":
+ break;
+ default:
+ $type = ".$type";
+ break;
+ }
+ $vapi = static::version();
+ $that = $api->withHeader("Accept", "application/vnd.github.v$vapi$type");
+ }
+ return $that;
+ }
+
/**
* @param Body $json
* @return mixed
return $decoded;
}
+
+ /**
+ * @param Body $stream
+ * @return resource stream
+ */
+ private static function fromStream(Body $stream) {
+ return $stream->getResource();
+ }
+
/**
* @param Body $data
* @return string
+++ /dev/null
-<?php
-
-namespace seekat\API;
-
-use Generator;
-use http\Client;
-use React\Promise\{
- Deferred,
- ExtendedPromiseInterface,
- PromiseInterface,
- function all
-};
-
-class Invoker extends Deferred
-{
- /**
- * The HTTP client
- * @var Client
- */
- private $client;
-
- /**
- * The return value of the generator
- * @var mixed
- */
- private $result;
-
- /**
- * Cancellation flag
- * @var bool
- */
- private $cancelled = false;
-
- /**
- * Create a new generator invoker
- * @param Client $client
- */
- function __construct(Client $client) {
- $this->client = $client;
-
- parent::__construct(function($resolve, $reject) {
- return $this->cancel($resolve, $reject);
- });
- }
-
- /**
- * Invoke $generator to create a \Generator which yields promises
- *
- * @param callable $generator as function():\Generator, creating a generator yielding promises
- * @return Invoker
- */
- function invoke(callable $generator) : Invoker {
- $this->iterate($generator());
- return $this;
- }
-
- /**
- * Iterate over $gen, a \Generator yielding promises
- *
- * @param Generator $gen
- * @return Invoker
- */
- function iterate(Generator $gen) : Invoker {
- $this->cancelled = false;
-
- foreach ($gen as $promise) {
- if ($this->cancelled) {
- break;
- }
- $this->give($promise, $gen);
- }
-
- if (!$this->cancelled) {
- $this->resolve($this->result = $gen->getReturn());
- }
- return $this;
- }
-
- /**
- * Get the generator's result
- *
- * @return ExtendedPromiseInterface
- */
- function result() : ExtendedPromiseInterface {
- return $this->promise();
- }
-
- /**
- * Promise handler
- *
- * @param array|PromiseInterface $promise
- * @param Generator $gen
- */
- private function give($promise, Generator $gen) {
- if ($promise instanceof PromiseInterface) {
- $promise->then(function($result) use($gen) {
- if (($promise = $gen->send($result))) {
- $this->give($promise, $gen);
- }
- });
- } else {
- all($promise)->then(function($results) use($gen) {
- if (($promise = $gen->send($results))) {
- $this->give($promise, $gen);
- }
- });
- }
- $this->client->send();
- }
-
- /**
- * Cancellation callback
- *
- * @param callable $resolve
- * @param callable $reject
- */
- private function cancel(callable $resolve, callable $reject) {
- $this->cancelled = true;
-
- /* did we finish in the meantime? */
- if ($this->result) {
- $resolve($this->result);
- } else {
- $reject("Cancelled");
- }
- }
-}
*/
function __construct(API $api) {
$this->api = $api;
- $this->data = (array) $api->export();
+ $this->data = (array) $api->export()["data"];
}
/**
namespace seekat\API;
+use seekat\Exception\UnexpectedValueException;
use http\ {
Header,
Params,
Url
};
use Serializable;
-use UnexpectedValueException;
class Links implements Serializable
{
--- /dev/null
+<?php
+
+namespace seekat\Exception;
+
+use seekat\Exception;
+
+class InvalidArgumentException extends \InvalidArgumentException implements Exception
+{
+}
};
use seekat\Exception;
+/**
+ * @code-coverage-ignore
+ */
class RequestException extends BaseException implements Exception
{
/**
--- /dev/null
+<?php
+
+namespace seekat\Exception;
+
+use seekat\Exception;
+
+class UnexpectedValueException extends \UnexpectedValueException implements Exception
+{
+}
--- /dev/null
+<?php
+
+namespace seekat\Exception;
+
+function message(&$error) : string {
+ if ($error instanceof \Throwable) {
+ $message = $error->getMessage();
+ } else {
+ $message = $error;
+ $error = new \Exception($error);
+ }
+ return $message;
+}
+
+namespace seekat\API\Links;
+
+use React\Promise\{
+ ExtendedPromiseInterface,
+ function reject
+};
+use seekat\API;
+use seekat\API\Call\Cache;
+
+/**
+ * Perform a GET request against the link's "first" relation
+ *
+ * @return ExtendedPromiseInterface
+ */
+function first(API $api, Cache\Service $cache = null) : ExtendedPromiseInterface {
+ $links = $api->getLinks();
+ if ($links && ($first = $links->getFirst())) {
+ return $api->withUrl($first)->get(null, null, $cache);
+ }
+ return reject($links);
+}
+
+/**
+ * Perform a GET request against the link's "prev" relation
+ *
+ * @return ExtendedPromiseInterface
+ */
+function prev(API $api, Cache\Service $cache = null) : ExtendedPromiseInterface {
+ $links = $api->getLinks();
+ if ($links && ($prev = $links->getPrev())) {
+ return $api->withUrl($prev)->get(null, null, $cache);
+ }
+ return reject($links);
+}
+
+/**
+ * Perform a GET request against the link's "next" relation
+ *
+ * @return ExtendedPromiseInterface
+ */
+function next(API $api, Cache\Service $cache = null) : ExtendedPromiseInterface {
+ $links = $api->getLinks();
+ if ($links && ($next = $links->getNext())) {
+ return $api->withUrl($next)->get(null, null, $cache);
+ }
+ return reject($links);
+}
+
+/**
+ * Perform a GET request against the link's "last" relation
+ *
+ * @return ExtendedPromiseInterface
+ */
+function last(API $api, Cache\Service $cache = null) : ExtendedPromiseInterface {
+ $links = $api->getLinks();
+ if ($links && ($last = $links->getLast())) {
+ return $api->withUrl($last)->get(null, null, $cache);
+ }
+ return reject($links);
+}
+
use Peridot\Console\Environment;
use Peridot\Core\Suite;
use Peridot\Core\Test;
-use seekat\API;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
+use Peridot\Reporter\AbstractBaseReporter;
+use Peridot\Reporter\AnonymousReporter;
use Peridot\Reporter\CodeCoverage\AbstractCodeCoverageReporter;
use Peridot\Reporter\CodeCoverageReporters;
use Peridot\Reporter\ReporterFactory;
-use Peridot\Reporter\AnonymousReporter;
-use Peridot\Reporter\AbstractBaseReporter;
+use seekat\API;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
return function(EventEmitter $emitter) {
(new CodeCoverageReporters($emitter))->register();
return new class($reporterFactory, $ar->getConfiguration(), $ar->getOutput(), $ar->getEventEmitter()) extends AbstractBaseReporter {
private $reporters = [];
+ private $factory;
function __construct(ReporterFactory $factory, Configuration $configuration, OutputInterface $output, EventEmitter $eventEmitter) {
- fprintf(STDERR, "Creating reporters\n");
- $this->reporters[] = $factory->create("spec");
- $this->reporters[] = $factory->create("text-code-coverage");
+ $this->factory = $factory;
parent::__construct($configuration, $output, $eventEmitter);
}
function init() {
+ fprintf(STDERR, "Creating reporters\n");
+ $this->reporters[] = $this->factory->create("spec");
+ if (extension_loaded("xdebug")) {
+ $this->reporters[] = $this->factory->create("text-code-coverage");
+ }
}
- function __2call($method, array $args) {
+
+ function X__call($method, array $args) {
fprintf(STDERR, "Calling %s\n", $method);
foreach ($this->reporters as $reporter) {
$output = $reporter->$method(...$args);
$emitter->on("suite.start", function(Suite $suite) use($log) {
$headers = [];
if (($token = getenv("GITHUB_TOKEN"))) {
- $headers["Authentication"] = "token $token";
+ $headers["Authorization"] = "token $token";
} elseif (function_exists("posix_isatty") && defined("STDIN") && posix_isatty(STDIN)) {
fprintf(STDOUT, "GITHUB_TOKEN is not set in the environment, enter Y to continue without: ");
fflush(STDOUT);
use seekat\API;
use React\Promise\PromiseInterface;
+use seekat\API\Links\ {
+ function first, function last, function next, function prev
+};
describe("API", function() {
- it("should return API on property access", function() {
- expect($this->api->users)->to->be->instanceof(API::class);
- });
+ describe("Interface", function() {
+ it("should return API on property access", function() {
+ expect($this->api->users)->to->be->instanceof(API::class);
+ });
- it("should return a clone on property access", function() {
- expect($this->api->users)->to->not->equal($this->api);
- });
+ it("should return a clone on property access", function() {
+ expect($this->api->users)->to->not->equal($this->api);
+ });
- it("should return PromiseInterface on function call", function() {
- expect($this->api->users->m6w6())->to->be->instanceof(PromiseInterface::class);
+ it("should return PromiseInterface on function call", function() {
+ expect($this->api->users->m6w6())->to->be->instanceof(PromiseInterface::class);
+ });
});
- it("should successfully request /users/m6w6", function() {
- $this->api->users->m6w6()->then(function($json) use(&$m6w6) {
- $m6w6 = $json;
- })->otherwise(function($error) use(&$errors) {
- $errors[] = (string) $error;
+ describe("Requests", function() {
+
+ it("should successfully request /users/m6w6", function() {
+ $this->api->users->m6w6()->then(function($json) use(&$m6w6) {
+ $m6w6 = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+
+ $this->api->send();
+
+ expect($errors)->to->be->empty;
+ expect($m6w6->login)->to->loosely->equal("m6w6");
});
- $this->api->send();
+ it("should export an array of data, url, type, links and headers", function() {
+ $this->api->users->m6w6()->then(function($json) use(&$m6w6) {
+ $m6w6 = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
- expect($errors)->to->be->empty;
- expect($m6w6->login)->to->loosely->equal("m6w6");
- });
+ $this->api->send();
- it("should return the count of followers when accessing /users/m6w6->followers", function() {
- $this->api->users->m6w6()->then(function($m6w6) use(&$followers) {
- $followers = $m6w6->followers;
- })->otherwise(function($error) use(&$errors) {
- $errors[] = (string) $error;
+ expect($errors)->to->be->empty();
+ expect($m6w6->export())->to->be->an("array")->and->contain->keys([
+ "data", "url", "type", "links", "headers"
+ ]);
});
- $this->api->send();
+ it("should return the count of followers when accessing /users/m6w6->followers", function() {
+ $this->api->users->m6w6()->then(function($m6w6) use(&$followers) {
+ $followers = $m6w6->followers;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
- expect($errors)->to->be->empty();
- expect($followers->export())->to->be->an("integer");
- });
+ $this->api->send();
- it("should fetch followers_url when accessing /users/m6w6->followers_url", function() {
- $this->api->users->m6w6()->then(function($m6w6) use(&$followers, &$errors) {
- $m6w6->followers_url()->then(function($json) use(&$followers) {
- $followers = $json;
- })->otherwise(function($error) use(&$errors) {
+ expect($errors)->to->be->empty();
+ expect($followers->export()["data"])->to->be->an("integer");
+ });
+
+ it("should fetch followers_url when accessing /users/m6w6->followers_url", function() {
+ $this->api->users->m6w6()->then(function($m6w6) use(&$followers, &$errors) {
+ $m6w6->followers_url()->then(function($json) use(&$followers) {
+ $followers = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+ }, function($error) use(&$errors) {
$errors[] = (string) $error;
});
- })->otherwise(function($error) use(&$errors) {
- $errors[] = (string) $error;
+
+ $this->api->send();
+
+ expect($errors)->to->be->empty;
+ expect($followers->export()["data"])->to->be->an("array");
+ expect(count($followers))->to->be->above(0);
});
- $this->api->send();
+ it("should provide access to array indices", function() {
+ $this->api->users->m6w6()->then(function($m6w6) use(&$followers, &$errors) {
+ $m6w6->followers_url()->then(function($json) use(&$followers) {
+ $followers = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+
+ $this->api->send();
+
+ expect($errors)->to->be->empty;
+ expect($followers->{0})->to->be->an("object");
+ expect($followers->export()["data"][0])->to->be->an("object");
+ });
+
+ it("should handle a few requests in parallel", function() {
+ $this->api->users->m6w6()->then(function($m6w6) use(&$count, &$errors) {
+ foreach ($m6w6 as $key => $val) {
+ switch ($key) {
+ case "html_url":
+ case "avatar_url":
+ break;
+ default:
+ if (substr($key, -4) === "_url") {
+ $val->get()->then(function() use(&$count) {
+ ++$count;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+ }
+ }
+ }
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+
+ $this->api->send();
+
+ expect($errors)->to->be->empty;
+ expect($count)->to->be->above(2);
+ });
- expect($errors)->to->be->empty;
- expect($followers->export())->to->be->an("array");
- expect(count($followers))->to->be->above(0);
});
- it("should handle a few requests in parallel", function() {
- $this->api->users->m6w6()->then(function($m6w6) use(&$count, &$errors) {
- foreach ($m6w6 as $key => $val) {
- switch ($key) {
- case "html_url":
- case "avatar_url":
- break;
- default:
- if (substr($key, -4) === "_url") {
- $val->get()->then(function() use(&$count) {
- ++$count;
- })->otherwise(function($error) use(&$errors) {
- $errors[] = (string) $error;
- });
- }
+ describe("Cache", function() {
+ it("should cache successive calls", function() {
+ $cache = new API\Call\Cache\Service\Hollow();
+ $this->api->users->m6w6(null, null, $cache)->then(function($json) use(&$m6w6) {
+ $m6w6 = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+
+ $this->api->send();
+
+ $data = $cache->getStorage();
+ $this->api->users->m6w6(null, null, $cache)->then(function($json) use(&$m6w6_) {
+ $m6w6_ = $json;
+ }, function($error) use(&$errors) {
+ $errors[] = (string) $error;
+ });
+
+ $this->api->send();
+
+ expect($errors)->to->be->empty;
+ expect($m6w6->login)->to->loosely->equal("m6w6");
+ expect($m6w6_->login)->to->loosely->equal("m6w6");
+ expect($data)->to->equal($cache->getStorage());
+ expect(count($cache->getStorage()))->to->equal(1);
+ });
+ xit("should refresh stale cache entries");
+ });
+
+ describe("Generators", function() {
+ it("should iterate over a generator of promises", function() {
+ ($this->api)(function($api) use(&$gists_count, &$files_count) {
+ $gists = yield $api->users->m6w6->gists();
+ $gists_count = count($gists);
+ foreach ($gists as $gist) {
+ $files_count += count($gist->files);
}
- }
- })->otherwise(function($error) use(&$errors) {
- $errors[] = (string) $error;
+ });
+ expect($gists_count)->to->be->above(0);
+ expect($files_count)->to->be->at->least($gists_count);
+ });
+ it("should iterate over a generator of promises with links", function() {
+ ($this->api)(function($api) use(&$repos, &$first, &$next, &$last, &$prev) {
+ $repos = yield $api->users->m6w6->repos(["per_page" => 1]);
+ $last = yield last($repos);
+ $prev = yield prev($last);
+ $next = yield next($prev);
+ $first = yield first($prev);
+ return -123;
+ })->done(function($value) use(&$result) {
+ $result = $value;
+ });
+
+ expect($result)->to->equal(-123);
+ expect($repos->export()["data"])->to->loosely->equal($first->export()["data"]);
+ expect($last->export()["data"])->to->loosely->equal($next->export()["data"]);
});
+ });
- $this->api->send();
+ describe("Errors", function() {
+ it("should handle cancellation gracefully", function() {
+ $this->api->users->m6w6()->then(function($value) use(&$result) {
+ $result = $value;
+ }, function($error) use(&$message) {
+ $message = \seekat\Exception\message($error);
+ })->cancel();
+ expect($result)->to->be->empty();
+ expect($message)->to->equal("Cancelled");
+ });
- expect($errors)->to->be->empty;
- expect($count)->to->be->above(2);
+ it("should handle request errors gracefully", function() {
+ $this->api->generate->a404()->then(function($value) use(&$result) {
+ $result = $value;
+ }, function($error) use(&$message) {
+ $message = \seekat\Exception\message($error);
+ });
+ $this->api->send();
+ expect($result)->to->be->empty();
+ expect($message)->to->equal("Not Found");
+ });
});
});