refactor
[m6w6/seekat] / lib / API / ContentType.php
1 <?php
2
3 namespace seekat\API;
4
5 use seekat\{
6 API, Exception\InvalidArgumentException, Exception\UnexpectedValueException
7 };
8 use http\{
9 Header, Message\Body
10 };
11
12 class ContentType
13 {
14 /**
15 * API version
16 * @var int
17 */
18 static private $version = 3;
19
20 /**
21 * Content type handler map
22 * @var array
23 */
24 static private $types = [
25 "json" => "self::fromJson",
26 "base64" => "self::fromBase64",
27 "sha" => "self::fromData",
28 "raw" => "self::fromData",
29 "html" => "self::fromData",
30 "diff" => "self::fromData",
31 "patch" => "self::fromData",
32 "text/plain"=> "self::fromData",
33 "application/octet-stream" => "self::fromStream",
34 ];
35
36 /**
37 * Content type abbreviation
38 * @var string
39 */
40 private $type;
41
42 /**
43 * Register a content type handler
44 * @param string $type The content type (abbreviation)
45 * @param callable $handler The handler as function(Body $body):mixed;
46 */
47 static function register(string $type, callable $handler) {
48 self::$types[$type] = $handler;
49 }
50
51 /**
52 * Check whether a handler is registered for a particular content type
53 * @param string $type The (abbreviated) content type
54 * @return bool
55 */
56 static function registered(string $type) : bool {
57 return isset(self::$types[$type]);
58 }
59
60 /**
61 * Unregister a content type handler
62 * @param string $type
63 */
64 static function unregister(string $type) {
65 unset(self::$types[$type]);
66 }
67
68 /**
69 * Get/set the API version to use
70 * @param int $v if not null, update the API version
71 * @return int the previously set version
72 */
73 static function version(int $v = null) : int {
74 $api = self::$version;
75 if (isset($v)) {
76 self::$version = $v;
77 }
78 return $api;
79 }
80
81 /**
82 * @param API $api
83 * @param string $type
84 * @return API
85 */
86 static function apply(API $api, string $type) : API {
87 $part = "[^()<>@,;:\\\"\/\[\]?.=[:space:][:cntrl:]]+";
88 if (preg_match("/^$part\/$part\$/", $type)) {
89 $that = $api->withHeader("Accept", $type);
90 } else {
91 switch (substr($type, 0, 1)) {
92 case "+":
93 case ".":
94 case "":
95 break;
96 default:
97 $type = ".$type";
98 break;
99 }
100 $vapi = static::version();
101 $that = $api->withHeader("Accept", "application/vnd.github.v$vapi$type");
102 }
103 return $that;
104 }
105
106 /**
107 * @param Body $json
108 * @return mixed
109 * @throws UnexpectedValueException
110 */
111 private static function fromJson(Body $json) {
112 $decoded = json_decode($json);
113 if (!isset($decoded) && json_last_error()) {
114 throw new UnexpectedValueException("Could not decode JSON: ".
115 json_last_error_msg());
116 }
117 return $decoded;
118 }
119
120 /**
121 * @param Body $base64
122 * @return string
123 * @throws UnexpectedValueException
124 */
125 private static function fromBase64(Body $base64) : string {
126 if (false === ($decoded = base64_decode($base64))) {
127 throw new UnexpectedValueException("Could not decode BASE64");
128 }
129 return $decoded;
130 }
131
132
133 /**
134 * @param Body $stream
135 * @return resource stream
136 */
137 private static function fromStream(Body $stream) {
138 return $stream->getResource();
139 }
140
141 /**
142 * @param Body $data
143 * @return string
144 */
145 private static function fromData(Body $data) : string {
146 return (string) $data;
147 }
148
149 /**
150 * @param Header $contentType
151 * @throws InvalidArgumentException
152 */
153 function __construct(Header $contentType) {
154 if (strcasecmp($contentType->name, "Content-Type")) {
155 throw new InvalidArgumentException(
156 "Expected Content-Type header, got ". $contentType->name);
157 }
158 $vapi = static::version();
159 $this->type = preg_replace("/
160 (?:application\/(?:vnd\.github(?:\.v$vapi)?)?)
161 (?|
162 \. ([^.+]+)
163 | (?:\.[^.+]+)?\+? (json)
164 )/x", "\\1", current(array_keys($contentType->getParams()->params)));
165 }
166
167 /**
168 * Get the (abbreviated) content type name
169 * @return string
170 */
171 function getType() : string {
172 return $this->type;
173 }
174
175 /**
176 * Parse a response message's body according to its content type
177 * @param Body $data
178 * @return mixed
179 * @throws UnexpectedValueException
180 */
181 function parseBody(Body $data) {
182 $type = $this->getType();
183 if (static::registered($type)) {
184 return call_user_func(self::$types[$type], $data, $type);
185 }
186 throw new UnexpectedValueException("Unhandled content type '$type'");
187 }
188 }