PHP8
[m6w6/seekat] / lib / API / ContentType.php
index b2eb5fbc0690f74af90b67214d37a6ca93b452a1..e69f775ee454ef8e9dd8fcd33b74cbad03d346a5 100644 (file)
@@ -4,86 +4,183 @@ namespace seekat\API;
 
 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;
 
-class ContentType
-{
-       static private $version = 3;
-       
-       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",
-       ];
+final class ContentType {
+       /**
+        * @var int
+        */
+       private $version;
 
+       /**
+        * Content type abbreviation
+        * @var string
+        */
        private $type;
 
-       static function register(string $type, callable $handler) {
-               self::$types[$type] = $handler;
+       /**
+        * Content type handler map
+        * @var array
+        */
+       static private $types = [];
+
+       /**
+        * Register a content type handler
+        */
+       static function register(Handler $handler) {
+               foreach ($handler->types() as $type) {
+                       self::$types[$type] = $handler;
+               }
        }
 
+       /**
+        * Check whether a handler is registered for a particular content type
+        * @param string $type The (abbreviated) content type
+        * @return bool
+        */
        static function registered(string $type) : bool {
                return isset(self::$types[$type]);
        }
 
+       /**
+        * Unregister a content type handler
+        * @param string $type
+        */
        static function unregister(string $type) {
                unset(self::$types[$type]);
        }
 
-       static function version(int $v = null) : int {
-               $api = self::$version;
-               if (isset($v)) {
-                       self::$version = $v;
+       /**
+        * API Version
+        *
+        * @param int $version
+        * @param string $type
+        */
+       function __construct(int $version = 3, string $type = null) {
+               $this->version = $version;
+               if (isset($type)) {
+                       $this->setContentType($this->extractTypeFromParams(new Params($type)));
                }
-               return $api;
-       }
-       
-       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;
        }
 
-       private static function fromBase64(Body $base64) : string {
-               if (false === ($decoded = base64_decode($base64))) {
-                       throw new \UnexpectedValueExcpeption("Could not decode BASE64");
+       /**
+        * @param Header $contentType
+        */
+       function setContentTypeHeader(Header $contentType) {
+               $this->type = $this->extractTypeFromHeader($contentType);
+       }
+
+       /**
+        * @param string $contentType
+        */
+       function setContentType(string $contentType) {
+               $this->type = $this->extractType($contentType);
+       }
+
+       /**
+        * @return int
+        */
+       function getVersion() : int {
+               return $this->version;
+       }
+
+       /**
+        * Get the (abbreviated) content type name
+        * @return string
+        */
+       function getType() : string {
+               return $this->type;
+       }
+
+       /**
+        * Get the (full) content type
+        * @return string
+        */
+       function getContentType() : string {
+               return $this->composeType($this->type);
+       }
+
+       /**
+        * @param API $api
+        * @return API clone
+        */
+       function apply(API $api) : API {
+               return $api->withHeader("Accept", $this->getContentType());
+       }
+
+       /**
+        * Decode a response message's body according to its content type
+        * @param Body $data
+        * @return mixed
+        * @throws UnexpectedValueException
+        */
+       function decode(Body $data) {
+               $type = $this->getType();
+               if (static::registered($type)) {
+                       return self::$types[$type]->decode($data);
                }
+               throw new UnexpectedValueException("Unhandled content type '$type'");
        }
 
-       private static function fromData(Body $data) : string {
-               return (string) $data;
+       /**
+        * Encode a request message's body according to its content type
+        * @param mixed $data
+        * @return Body
+        * @throws UnexpectedValueException
+        */
+       function encode($data) : Body {
+               $type = $this->getType();
+               if (static::registered($type)) {
+                       return self::$types[$type]->encode($data);
+               }
+               throw new UnexpectedValueException("Unhandled content type '$type'");
        }
 
-       function __construct(Header $contentType) {
-               if (strcasecmp($contentType->name, "Content-Type")) {
-                       throw new \InvalidArgumentException(
-                               "Expected Content-Type header, got ". $contentType->name);
+       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;
                }
-               $vapi = static::version();
-               $this->type = preg_replace("/
-                       (?:application\/(?:vnd\.github(?:\.v$vapi)?)?)
+               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", current(array_keys($contentType->getParams()->params)));
+                       )/x", "\\1", $type);
        }
 
-       function getType() : string {
-               return $this->type;
+       private function extractTypeFromParams(Params $params) : string {
+               return $this->extractType(current(array_keys($params->params)));
        }
 
-       function parseBody(Body $data) {
-               $type = $this->getType();
-               if (static::registered($type)) {
-                       return call_user_func(self::$types[$type], $data, $type);
+       private function extractTypeFromHeader(Header $header) : string {
+               if (strcasecmp($header->name, "Content-Type")) {
+                       throw new InvalidArgumentException(
+                               "Expected Content-Type header, got ". $header->name);
                }
-               throw new \UnexpectedValueException("Unhandled content type '$type'");
+               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);