update to PHP-8.1
[m6w6/seekat] / lib / API.php
index 63fd004f386a73ce82b2dead3ee2669d7726c6d7..3ed788a6af07b6ef00c0d52d8be25f9c88a96f52 100644 (file)
@@ -4,84 +4,58 @@ namespace seekat;
 
 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()
                ];
        }
 
@@ -91,8 +65,8 @@ class API implements IteratorAggregate, Countable {
         * @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);
@@ -107,6 +81,7 @@ class API implements IteratorAggregate, Countable {
                                (string) $this->url,
                                (string) $that->url
                        ],
+                       "data" => $that->data
                ]);
 
                return $that;
@@ -117,26 +92,23 @@ class API implements IteratorAggregate, Countable {
         *
         * @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);
@@ -146,13 +118,15 @@ class API implements IteratorAggregate, Countable {
         * 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) {
@@ -182,12 +156,7 @@ class API implements IteratorAggregate, Countable {
         * @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);
        }
 
        /**
@@ -196,7 +165,15 @@ class API implements IteratorAggregate, Countable {
         * @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;
+               }
        }
 
        /**
@@ -205,34 +182,43 @@ class API implements IteratorAggregate, Countable {
         * @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;
        }
 
@@ -241,10 +227,17 @@ class API implements IteratorAggregate, Countable {
         *
         * @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
         *
@@ -281,7 +274,7 @@ class API implements IteratorAggregate, Countable {
         * @param mixed $data
         * @return API clone
         */
-       function withData($data) : API {
+       function withData(mixed $data) : API {
                $that = clone $this;
                $that->data = $data;
                return $that;
@@ -308,7 +301,7 @@ class API implements IteratorAggregate, Countable {
         * @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;
@@ -328,22 +321,37 @@ class API implements IteratorAggregate, Countable {
         * @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);
        }
 
        /**
@@ -351,9 +359,9 @@ class API implements IteratorAggregate, Countable {
         *
         * @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);
        }
 
@@ -363,9 +371,9 @@ class API implements IteratorAggregate, Countable {
         * @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);
        }
 
@@ -375,9 +383,9 @@ class API implements IteratorAggregate, Countable {
         * @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);
        }
 
@@ -387,9 +395,9 @@ class API implements IteratorAggregate, Countable {
         * @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);
        }
 
@@ -430,7 +438,7 @@ class API implements IteratorAggregate, Countable {
                        $seg, typeof($this->data, false), $exists ? "true" : "false"
                ), [
                        "url" => (string) $this->url,
-                       "val" => $val,
+                       "val" => typeof($val, false),
                ]);
 
                return $exists;
@@ -441,30 +449,26 @@ class API implements IteratorAggregate, Countable {
         *
         * @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,
@@ -474,6 +478,6 @@ class API implements IteratorAggregate, Countable {
                        "headers" => $headers,
                ]);
 
-               return (new Call\Deferred($this, $request, $cache ?: $this->cache))->promise();
+               return (new Call\Deferred($this, $request, $this->cache))();
        }
 }