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);