PHP8
[m6w6/seekat] / lib / API / ContentType.php
index d13b11a6d925a4581fefd96d203b9779c5ed4f1a..e69f775ee454ef8e9dd8fcd33b74cbad03d346a5 100644 (file)
@@ -2,36 +2,19 @@
 
 namespace seekat\API;
 
-use seekat\{
-       API, Exception\InvalidArgumentException, Exception\UnexpectedValueException
-};
-use http\{
-       Header, Message\Body
-};
-
-final class ContentType
-{
+use http\Header;
+use http\Message\Body;
+use http\Params;
+use seekat\API;
+use seekat\API\ContentType\Handler;
+use seekat\Exception\InvalidArgumentException;
+use seekat\Exception\UnexpectedValueException;
+
+final class ContentType {
        /**
-        * API version
         * @var int
         */
-       static private $version = 3;
-
-       /**
-        * Content type handler map
-        * @var array
-        */
-       static private $types = [
-               "json"          => "self::fromJson",
-               "base64"        => "self::fromBase64",
-               "sha"           => "self::fromData",
-               "raw"           => "self::fromData",
-               "html"          => "self::fromData",
-               "diff"          => "self::fromData",
-               "patch"         => "self::fromData",
-               "text/plain"=> "self::fromData",
-               "application/octet-stream" => "self::fromStream",
-       ];
+       private $version;
 
        /**
         * Content type abbreviation
@@ -39,13 +22,19 @@ final class ContentType
         */
        private $type;
 
+       /**
+        * Content type handler map
+        * @var array
+        */
+       static private $types = [];
+
        /**
         * Register a content type handler
-        * @param string $type The content type (abbreviation)
-        * @param callable $handler The handler as function(Body $body):mixed;
         */
-       static function register(string $type, callable $handler) {
-               self::$types[$type] = $handler;
+       static function register(Handler $handler) {
+               foreach ($handler->types() as $type) {
+                       self::$types[$type] = $handler;
+               }
        }
 
        /**
@@ -66,123 +55,132 @@ final class ContentType
        }
 
        /**
-        * Get/set the API version to use
-        * @param int $v if not null, update the API version
-        * @return int the previously set version
+        * API Version
+        *
+        * @param int $version
+        * @param string $type
         */
-       static function version(int $v = null) : int {
-               $api = self::$version;
-               if (isset($v)) {
-                       self::$version = $v;
+       function __construct(int $version = 3, string $type = null) {
+               $this->version = $version;
+               if (isset($type)) {
+                       $this->setContentType($this->extractTypeFromParams(new Params($type)));
                }
-               return $api;
        }
 
        /**
-        * @param API $api
-        * @param string $type
-        * @return API
+        * @param Header $contentType
         */
-       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;
+       function setContentTypeHeader(Header $contentType) {
+               $this->type = $this->extractTypeFromHeader($contentType);
        }
 
        /**
-        * @param Body $json
-        * @return mixed
-        * @throws UnexpectedValueException
+        * @param string $contentType
         */
-       private static function fromJson(Body $json) {
-               $decoded = json_decode($json);
-               if (!isset($decoded) && json_last_error()) {
-                       throw new UnexpectedValueException("Could not decode JSON: ".
-                               json_last_error_msg());
-               }
-               return $decoded;
+       function setContentType(string $contentType) {
+               $this->type = $this->extractType($contentType);
        }
 
        /**
-        * @param Body $base64
-        * @return string
-        * @throws UnexpectedValueException
+        * @return int
         */
-       private static function fromBase64(Body $base64) : string {
-               if (false === ($decoded = base64_decode($base64, true))) {
-                       throw new UnexpectedValueException("Could not decode BASE64");
-               }
-               return $decoded;
+       function getVersion() : int {
+               return $this->version;
        }
 
-
        /**
-        * @param Body $stream
-        * @return resource stream
+        * Get the (abbreviated) content type name
+        * @return string
         */
-       private static function fromStream(Body $stream) {
-               return $stream->getResource();
+       function getType() : string {
+               return $this->type;
        }
 
        /**
-        * @param Body $data
+        * Get the (full) content type
         * @return string
         */
-       private static function fromData(Body $data) : string {
-               return (string) $data;
+       function getContentType() : string {
+               return $this->composeType($this->type);
        }
 
        /**
-        * @param Header $contentType
-        * @throws InvalidArgumentException
+        * @param API $api
+        * @return API clone
         */
-       function __construct(Header $contentType) {
-               if (strcasecmp($contentType->name, "Content-Type")) {
-                       throw new InvalidArgumentException(
-                               "Expected Content-Type header, got ". $contentType->name);
-               }
-               $vapi = static::version();
-               $this->type = preg_replace("/
-                       (?:application\/(?:vnd\.github(?:\.v$vapi)?)?)
-                       (?|
-                                       \.                                      ([^.+]+)
-                               |       (?:\.[^.+]+)?\+?        (json)
-                       )/x", "\\1", current(array_keys($contentType->getParams()->params)));
+       function apply(API $api) : API {
+               return $api->withHeader("Accept", $this->getContentType());
        }
 
        /**
-        * Get the (abbreviated) content type name
-        * @return string
+        * Decode a response message's body according to its content type
+        * @param Body $data
+        * @return mixed
+        * @throws UnexpectedValueException
         */
-       function getType() : string {
-               return $this->type;
+       function decode(Body $data) {
+               $type = $this->getType();
+               if (static::registered($type)) {
+                       return self::$types[$type]->decode($data);
+               }
+               throw new UnexpectedValueException("Unhandled content type '$type'");
        }
 
        /**
-        * Parse a response message's body according to its content type
-        * @param Body $data
-        * @return mixed
+        * Encode a request message's body according to its content type
+        * @param mixed $data
+        * @return Body
         * @throws UnexpectedValueException
         */
-       function parseBody(Body $data) {
+       function encode($data) : Body {
                $type = $this->getType();
                if (static::registered($type)) {
-                       return call_user_func(self::$types[$type], $data, $type);
+                       return self::$types[$type]->encode($data);
                }
                throw new UnexpectedValueException("Unhandled content type '$type'");
        }
+
+       private function composeType(string $type) : string {
+               $part = "[^()<>@,;:\\\"\/\[\]?.=[:space:][:cntrl:]]+";
+               if (preg_match("/^$part\/$part\$/", $type)) {
+                       return $type;
+               }
+
+               switch (substr($type, 0, 1)) {
+                       case "+":
+                       case ".":
+                       case "":
+                               break;
+                       default:
+                               $type = ".$type";
+                               break;
+               }
+               return "application/vnd.github.v{$this->version}$type";
+       }
+
+       private function extractType(string $type) : string {
+               return preg_replace("/
+                       (?:application\/(?:vnd\.github(?:\.v{$this->version})?)?)
+                       (?|
+                                       \.                                      ([^.+]+)
+                               |       (?:\.[^.+]+)?\+?        (json)
+                       )/x", "\\1", $type);
+       }
+
+       private function extractTypeFromParams(Params $params) : string {
+               return $this->extractType(current(array_keys($params->params)));
+       }
+
+       private function extractTypeFromHeader(Header $header) : string {
+               if (strcasecmp($header->name, "Content-Type")) {
+                       throw new InvalidArgumentException(
+                               "Expected Content-Type header, got ". $header->name);
+               }
+               return $this->extractTypeFromParams($header->getParams());
+       }
 }
+
+ContentType::register(new Handler\Text);
+ContentType::register(new Handler\Json);
+ContentType::register(new Handler\Base64);
+ContentType::register(new Handler\Stream);