use Countable;
use Generator;
-use http\{
- Client, Client\Request, Message\Body, QueryString, Url
-};
+use http\{Client, Client\Request, Message\Body, QueryString, Url};
+use Iterator;
use IteratorAggregate;
-use Psr\Log\{
- LoggerInterface, NullLogger
-};
-use React\Promise\{
- ExtendedPromiseInterface, function resolve
-};
-use seekat\{
- API\Call, API\Consumer, API\ContentType, API\Iterator, API\Links, Exception\InvalidArgumentException
-};
+use Psr\Log\{LoggerInterface, NullLogger};
+use seekat\API\{Call, Consumer, ContentType, Future, Links};
+use seekat\Exception\InvalidArgumentException;
class API implements IteratorAggregate, Countable {
/**
- * The current API endpoint URL
- * @var Url
+ * API version
*/
- private $url;
+ private int $version = 3;
/**
- * Logger
- * @var LoggerInterface
- */
- private $logger;
-
- /**
- * Cache
- * @var Call\Cache\Service
- */
- private $cache;
-
- /**
- * The HTTP client
- * @var Client
+ * Default headers to send out to the API endpoint
*/
- private $client;
+ private array $headers;
/**
- * Default headers to send out to the API endpoint
- * @var array
+ * Current endpoints links
*/
- private $headers;
+ private ?Links $links = null;
/**
* Current endpoint data's Content-Type
- * @var API\ContentType
*/
- private $type;
+ private API\ContentType $type;
/**
* Current endpoint's data
- * @var array|object
- */
- private $data;
-
- /**
- * Current endpoints links
- * @var Links
*/
- private $links;
+ private mixed $data = null;
/**
* Create a new API endpoint root
*
+ * @param Future $future pretending to fulfill promises
* @param array $headers Standard request headers, defaults to ["Accept" => "application/vnd.github.v3+json"]
* @param Url $url The API's endpoint, defaults to https://api.github.com
* @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, 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;
+ * @param Call\Cache\Service $cache A cache
+ */
+ function __construct(private readonly Future $future,
+ array $headers = null,
+ private Url $url = new Url("https://api.github.com"),
+ private readonly Client $client = new Client,
+ private readonly LoggerInterface $logger = new NullLogger,
+ private readonly Call\Cache\Service $cache = new Call\Cache\Service\Hollow) {
+ $this->type = new ContentType($this->version, "json");
$this->headers = (array) $headers + [
- "Accept" => "application/vnd.github.v3+json"
+ "Accept" => $this->type->getContentType()
];
}
* @param string|int $seg The "path" element to ascend into
* @return API Endpoint clone referring to {$parent}/{$seg}
*/
- function __get($seg) : API {
- if (substr($seg, -4) === "_url") {
+ function __get(string|int $seg) : API {
+ if (str_ends_with($seg, "_url")) {
$url = new Url(uri_template($this->data->$seg));
$that = $this->withUrl($url);
$seg = basename($that->url->path);
$this->exists($seg, $that->data);
}
- $this->logger->debug(__FUNCTION__."($seg)", [
+ $this->logger->debug("get($seg)", [
"url" => [
(string) $this->url,
(string) $that->url
],
+ "data" => $that->data
]);
return $that;
*
* @param string $method The API's "path" element to ascend into
* @param array $args Array of arguments forwarded to \seekat\API::get()
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function __call(string $method, array $args) : ExtendedPromiseInterface {
+ function __call(string $method, array $args) {
/* We cannot implement an explicit then() method,
* because the Promise implementation might think
* we're actually implementing Thenable,
* which might cause an infinite loop.
*/
- if ($method === "then") {
- return $this->get()->then(...$args);
- }
-
+ if ($method === "then"
/*
* very short-hand version:
* ->users->m6w6->gists->get()->then(...)
* vs:
* ->users->m6w6->gists(...)
*/
- if (is_callable(current($args))) {
- return $this->api->get()->then(current($args));
+ || is_callable(current($args))) {
+ return $this->future->handlePromise($this->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
+ * @return mixed The promise of the generator's return value
* @throws InvalidArgumentException
*/
- function __invoke($cbg) : ExtendedPromiseInterface {
- $this->logger->debug(__FUNCTION__);
+ function __invoke(callable|Generator $cbg) {
+ $this->logger->debug(__METHOD__, [$cbg]);
- $consumer = new Consumer($this->client);
+ $consumer = new Consumer($this->getFuture(), function() {
+ $this->client->send();
+ });
invoke:
if ($cbg instanceof Generator) {
goto invoke;
}
- throw InvalidArgumentException(
- "Expected callable or Generator, got ".(
- is_object($cbg)
- ? "instance of ".get_class($cbg)
- : gettype($cbg).": ".var_export($cbg, true)
- )
+ throw new InvalidArgumentException(
+ "Expected callable or Generator, got ".typeof($cbg, true)
);
}
* @return string
*/
function __toString() : string {
- if (is_scalar($this->data)) {
- return (string) $this->data;
- }
-
- /* FIXME */
- return json_encode($this->data);
+ return (string) $this->type->encode($this->data);
}
/**
* @return Iterator
*/
function getIterator() : Iterator {
- return new Iterator($this);
+ foreach ($this->data as $key => $cur) {
+ if ($this->__get($key)->exists("url", $url)) {
+ $url = new Url($url);
+ $val = $this->withUrl($url)->withData($cur);
+ } else {
+ $val = $this->__get($key)->withData($cur);
+ }
+ yield $key => $val;
+ }
}
/**
* @return int
*/
function count() : int {
- return count($this->data);
+ if (is_array($this->data)) {
+ $count = count($this->data);
+ } else if ($this->data instanceof Countable) {
+ $count = count($this->data);
+ } else if (is_object($this->data)) {
+ $count = count((array) $this->data);
+ } else {
+ $count = 0;
+ }
+ $this->logger->debug("count()", [
+ "of type" => typeof($this->data),
+ "count" => $count
+ ]);
+ return $count;
}
- /**
- * @return Url
- */
function getUrl() : Url {
return $this->url;
}
- /**
- * @return LoggerInterface
- */
function getLogger() : LoggerInterface {
return $this->logger;
}
- /**
- * @return Client
- */
+ function getFuture() : Future {
+ return $this->future;
+ }
+
public function getClient(): Client {
return $this->client;
}
- /**
- * @return array|object
- */
- function getData() {
+ public function getCache() : Call\Cache\Service {
+ return $this->cache;
+ }
+
+ function getData() : mixed {
return $this->data;
}
*
* @return null|Links
*/
- function getLinks() {
+ function getLinks() : ?Links {
return $this->links;
}
+ /**
+ * @return int
+ */
+ function getVersion() : int {
+ return $this->version;
+ }
+
/**
* Export the endpoint's underlying data
*
* @param mixed $data
* @return API clone
*/
- function withData($data) : API {
+ function withData(mixed $data) : API {
$that = clone $this;
$that->data = $data;
return $that;
* @param mixed $value
* @return API clone
*/
- function withHeader(string $name, $value) : API {
+ function withHeader(string $name, mixed $value) : API {
$that = clone $this;
if (isset($value)) {
$that->headers[$name] = $value;
* @return API clone
*/
function as(string $type, bool $keepdata = true) : API {
- $that = ContentType::apply($this, $type);
+ $ct = new ContentType($this->version, $type);
+
+ $that = $ct->apply($this);
+ $that->type = $ct;
+
if (!$keepdata) {
$that->data = null;
}
return $that;
}
+ /**
+ * Perform a HEAD request against the endpoint's underlying URL
+ *
+ * @param mixed $args The HTTP query string parameters
+ * @param array $headers The request's additional HTTP headers
+ * @return mixed promise
+ */
+ function head($args = null, array $headers = null) {
+ return $this->request("HEAD", $args, null, $headers);
+ }
+
/**
* Perform a GET request against the endpoint's underlying URL
*
* @param mixed $args The HTTP query string parameters
* @param array $headers The request's additional HTTP headers
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function get($args = null, array $headers = null, $cache = null) : ExtendedPromiseInterface {
- return $this->request("GET", $args, null, $headers, $cache);
+ function get($args = null, array $headers = null) {
+ return $this->request("GET", $args, null, $headers);
}
/**
*
* @param mixed $args The HTTP query string parameters
* @param array $headers The request's additional HTTP headers
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function delete($args = null, array $headers = null) : ExtendedPromiseInterface {
+ function delete($args = null, array $headers = null) {
return $this->request("DELETE", $args, null, $headers);
}
* @param mixed $body The HTTP message's body
* @param mixed $args The HTTP query string parameters
* @param array $headers The request's additional HTTP headers
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function post($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
+ function post($body = null, $args = null, array $headers = null) {
return $this->request("POST", $args, $body, $headers);
}
* @param mixed $body The HTTP message's body
* @param mixed $args The HTTP query string parameters
* @param array $headers The request's additional HTTP headers
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function put($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
+ function put($body = null, $args = null, array $headers = null) {
return $this->request("PUT", $args, $body, $headers);
}
* @param mixed $body The HTTP message's body
* @param mixed $args The HTTP query string parameters
* @param array $headers The request's additional HTTP headers
- * @return ExtendedPromiseInterface
+ * @return mixed promise
*/
- function patch($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface {
+ function patch($body = null, $args = null, array $headers = null) {
return $this->request("PATCH", $args, $body, $headers);
}
* @return API self
*/
function send() : API {
- $this->logger->debug(__FUNCTION__.": start loop");
+ $this->logger->debug("send: start loop");
while (count($this->client)) {
$this->client->send();
}
- $this->logger->debug(__FUNCTION__.": end loop");
+ $this->logger->debug("send: end loop");
return $this;
}
$exists = false;
}
- $this->logger->debug(__FUNCTION__."($seg) in ".(
- is_object($this->data)
- ? get_class($this->data)
- : gettype($this->data)
- )." -> ".(
- $exists
- ? "true"
- : "false"
+ $this->logger->debug(sprintf("exists(%s) in %s -> %s",
+ $seg, typeof($this->data, false), $exists ? "true" : "false"
), [
"url" => (string) $this->url,
- "val" => $val,
+ "val" => typeof($val, false),
]);
return $exists;
*
* @param string $method The HTTP request method
* @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
+ * @param mixed $body The HTTP message's body
+ * @param ?array $headers The request's additional HTTP headers
+ * @return mixed promise
*/
- private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service $cache = null) : ExtendedPromiseInterface {
+ private function request(string $method, $args = null, $body = null, array $headers = null) {
if (isset($this->data)) {
$this->logger->debug("request -> resolve", [
"method" => $method,
- "url" => (string)$this->url,
+ "url" => (string) $this->url,
"args" => $args,
"body" => $body,
"headers" => $headers,
]);
- return resolve($this);
+ return $this->future->resolve($this);
}
$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)));
+ $body = $this->type->encode(is_resource($body) ? new Body($body) : $body));
$this->logger->info("request -> deferred", [
"method" => $method,
"headers" => $headers,
]);
- return (new Call\Deferred($this, $request, $cache ?: $this->cache))->promise();
+ return (new Call\Deferred($this, $request, $this->cache))();
}
}