Completed
Pull Request — master (#13)
by
unknown
02:14
created

ApiService::getDefaultValues()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 8
nop 2
dl 0
loc 36
ccs 26
cts 26
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ElevenLabs\Api\Service;
6
7
use Assert\Assertion;
8
use ElevenLabs\Api\Decoder\DecoderUtils;
9
use ElevenLabs\Api\Definition\Parameter;
10
use ElevenLabs\Api\Definition\Parameters;
11
use ElevenLabs\Api\Definition\RequestDefinition;
12
use ElevenLabs\Api\Definition\ResponseDefinition;
13
use ElevenLabs\Api\Schema;
14
use ElevenLabs\Api\Service\Exception\ConstraintViolations;
15
use ElevenLabs\Api\Service\Exception\RequestViolations;
16
use ElevenLabs\Api\Service\Exception\ResponseViolations;
17
use ElevenLabs\Api\Service\Resource\Item;
18
use ElevenLabs\Api\Service\Resource\ResourceInterface;
19
use ElevenLabs\Api\Validator\MessageValidator;
20
use Http\Client\HttpAsyncClient;
21
use Http\Client\HttpClient;
22
use Http\Message\MessageFactory;
23
use Http\Message\UriFactory;
24
use Http\Promise\Promise;
25
use Psr\Http\Message\RequestInterface;
26
use Psr\Http\Message\ResponseInterface;
27
use Psr\Http\Message\UriInterface;
28
use Rize\UriTemplate;
29
use Symfony\Component\Serializer\SerializerInterface;
30
31
/**
32
 * Class ApiService.
33
 */
34
class ApiService
35
{
36
    const DEFAULT_CONFIG = [
37
        // override the scheme and host provided in the API schema by a baseUri (ex:https://domain.com)
38
        'baseUri' => null,
39
        // validate request
40
        'validateRequest' => true,
41
        // validate response
42
        'validateResponse' => false,
43
        // return response instead of a denormalized object
44
        'returnResponse' => false,
45
    ];
46
47
    /** @var UriInterface */
48
    private $baseUri;
49
50
    /**
51
     * @var Schema
52
     */
53
    private $schema;
54
55
    /**
56
     * @var MessageValidator
57
     */
58
    private $messageValidator;
59
60
    /**
61
     * @var HttpAsyncClient|HttpClient
62
     */
63
    private $client;
64
65
    /**
66
     * @var MessageFactory
67
     */
68
    private $messageFactory;
69
70
    /**
71
     * @var UriTemplate
72
     */
73
    private $uriTemplate;
74
75
    /**
76
     * @var UriFactory
77
     */
78
    private $uriFactory;
79
80
    /**
81
     * @var SerializerInterface
82
     */
83
    private $serializer;
84
85
    /**
86
     * @var array
87
     */
88
    private $config;
89
90
    /**
91
     * ApiService constructor.
92
     *
93
     * @param UriFactory          $uriFactory
94
     * @param UriTemplate         $uriTemplate
95
     * @param HttpClient          $client
96
     * @param MessageFactory      $messageFactory
97
     * @param Schema              $schema
98
     * @param MessageValidator    $messageValidator
99
     * @param SerializerInterface $serializer
100
     * @param array               $config
101
     *
102
     * @throws \Assert\AssertionFailedException
103
     */
104 34
    public function __construct(
105
        UriFactory $uriFactory,
106
        UriTemplate $uriTemplate,
107
        HttpClient $client,
108
        MessageFactory $messageFactory,
109
        Schema $schema,
110
        MessageValidator $messageValidator,
111
        SerializerInterface $serializer,
112
        array $config = []
113
    ) {
114 34
        $this->uriFactory = $uriFactory;
115 34
        $this->uriTemplate = $uriTemplate;
116 34
        $this->schema = $schema;
117 34
        $this->messageValidator = $messageValidator;
118 34
        $this->client = $client;
119 34
        $this->messageFactory = $messageFactory;
120 34
        $this->serializer = $serializer;
121 34
        $this->config = $this->getConfig($config);
122 30
        $this->baseUri = $this->getBaseUri();
123 27
    }
124
125
    /**
126
     * @param string $operationId The name of your operation as described in the API Schema
127
     * @param array  $params      An array of request parameters
128
     *
129
     * @throws ConstraintViolations
130
     * @throws \Http\Client\Exception
131
     *
132
     * @return ResourceInterface|ResponseInterface|array|object
133
     */
134 21
    public function call(string $operationId, array $params = [])
135
    {
136 21
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
137 21
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
138 20
        $this->validateRequest($request, $requestDefinition);
139
140 18
        $response = $this->client->sendRequest($request);
141 18
        $this->validateResponse($response, $requestDefinition);
142
143 16
        return $this->getDataFromResponse(
144 16
            $response,
145 16
            $requestDefinition->getResponseDefinition(
146 16
                $response->getStatusCode()
147
            ),
148 16
            $request
149
        );
150
    }
151
152
    /**
153
     * @param string $operationId
154
     * @param array  $params
155
     *
156
     * @throws \Exception
157
     *
158
     * @return Promise
159
     *
160
     */
161 1
    public function callAsync(string $operationId, array $params = []): Promise
162
    {
163 1
        if (!$this->client instanceof HttpAsyncClient) {
164
            throw new \RuntimeException(
165
                sprintf(
166
                    '"%s" does not support async request',
167
                    get_class($this->client)
168
                )
169
            );
170
        }
171
172 1
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
173 1
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
174 1
        $promise = $this->client->sendAsyncRequest($request);
175
176 1
        return $promise->then(
177
            function (ResponseInterface $response) use ($request, $requestDefinition) {
178
179 1
                return $this->getDataFromResponse(
180 1
                    $response,
181 1
                    $requestDefinition->getResponseDefinition(
182 1
                        $response->getStatusCode()
183
                    ),
184 1
                    $request
185
                );
186 1
            }
187
        );
188
    }
189
190
    /**
191
     * Configure the base uri from using the API Schema if no baseUri is provided
192
     * Or from the baseUri config if provided
193
     *
194
     * @return UriInterface
195
     */
196 30
    private function getBaseUri(): UriInterface
197
    {
198
        // Create a base uri from the API Schema
199 30
        if (null === $this->config['baseUri']) {
200 11
            $schemes = $this->schema->getSchemes();
201 11
            if (empty($schemes)) {
202 1
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
203
            }
204
205 10
            $scheme = null;
206 10
            foreach ($this->schema->getSchemes() as $candidate) {
207
                // Always prefer https
208 10
                if ('https' === $candidate) {
209 8
                    $scheme = 'https';
210
                }
211 10
                if (null === $scheme && 'http' === $candidate) {
212 2
                    $scheme = 'http';
213
                }
214
            }
215
216 10
            if (null === $scheme) {
217 1
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
218
            }
219
220 9
            $host = $this->schema->getHost();
221 9
            if ('' === $host) {
222 1
                throw new \LogicException('The host in the API Schema should not be null');
223
            }
224
225 8
            return $this->uriFactory->createUri($scheme.'://'.$host);
226
        } else {
227 19
            return $this->uriFactory->createUri($this->config['baseUri']);
228
        }
229
    }
230
231
    /**
232
     * @param array $config
233
     *
234
     * @throws \Assert\AssertionFailedException
235
     *
236
     * @return array
237
     */
238 34
    private function getConfig(array $config): array
239
    {
240 34
        $config = array_merge(self::DEFAULT_CONFIG, $config);
241 34
        Assertion::boolean($config['returnResponse']);
242 33
        Assertion::boolean($config['validateRequest']);
243 32
        Assertion::boolean($config['validateResponse']);
244 31
        Assertion::nullOrString($config['baseUri']);
245
246 30
        return array_intersect_key($config, self::DEFAULT_CONFIG);
247
    }
248
249
    /**
250
     * Create an PSR-7 Request from the API Schema
251
     *
252
     * @param RequestDefinition $definition
253
     * @param array             $params
254
     *
255
     * @return RequestInterface
256
     */
257 22
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
258
    {
259 22
        $contentType = $definition->getContentTypes()[0];
260 22
        $requestParameters = $definition->getRequestParameters();
261 22
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($contentType, $requestParameters);
262
263 22
        foreach ($params as $name => $value) {
264 11
            $requestParameter = $requestParameters->getByName($name);
265 11
            if (null === $requestParameter) {
266 1
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter', $name));
267
            }
268
269 10
            switch ($requestParameter->getLocation()) {
270 10
                case 'path':
271 2
                    $path[$name] = $value;
272 2
                    break;
273 9
                case 'query':
274 6
                    $query[$name] = $value;
275 6
                    break;
276 4
                case 'header':
277 1
                    $headers[$name] = $value;
278 1
                    break;
279 3
                case 'body':
280 2
                    $body = $this->serializeRequestBody($value, $contentType);
281 2
                    break;
282 1
                case 'formData':
283 1
                    $formData[$name] = sprintf("%s=%s", $name, $value);
284 1
                    break;
285
            }
286
        }
287
288 21
        if (!empty($formData)) {
289 2
            $body = implode("&", $formData);
290
        }
291
292 21
        $request = $this->messageFactory->createRequest(
293 21
            $definition->getMethod(),
294 21
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
295 21
            $headers,
296 21
            $body
297
        );
298
299 21
        return $request;
300
    }
301
302
    /**
303
     * @param string     $contentType
304
     * @param Parameters $requestParameters
305
     *
306
     * @return array
307
     */
308 22
    private function getDefaultValues(string $contentType, Parameters $requestParameters): array
309
    {
310 22
        $path = [];
311 22
        $query = [];
312 22
        $headers = ['Content-Type' => $contentType];
313 22
        $body = null;
314 22
        $formData = [];
315
316
        /**
317
         * @var string    $name
318
         * @var Parameter $parameter
319
         */
320 22
        foreach ($requestParameters->getIterator() as $name => $parameter) {
321 10
            if (isset($parameter->getSchema()->default)) {
322 10
                $value = $parameter->getSchema()->default;
323 10
                switch ($parameter->getLocation()) {
324 10
                    case 'path':
325 6
                        $path[$name] = $value;
326 6
                        break;
327 9
                    case 'query':
328 6
                        $query[$name] = $value;
329 6
                        break;
330 8
                    case 'header':
331 6
                        $headers[$name] = $value;
332 6
                        break;
333 2
                    case 'formData':
334 1
                        $formData[$name] = sprintf("%s=%s", $name, $value);
335 1
                        break;
336 1
                    case 'body':
337 1
                        $body = $this->serializeRequestBody($value, $contentType);
338 1
                        break;
339
                }
340
            }
341
        }
342
343 22
        return [$path, $query, $headers, $body, $formData];
344
    }
345
346
347
    /**
348
     * Create a complete API Uri from the Base Uri, path and query parameters.
349
     *
350
     * Example:
351
     *  Given a base uri that equal http://domain.tld
352
     *  Given the following parameters /pets/{id}, ['id' => 1], ['foo' => 'bar']
353
     *  Then the Uri will equal to http://domain.tld/pets/1?foo=bar
354
     *
355
     * @param string $pathTemplate    A template path
356
     * @param array  $pathParameters  Path parameters
357
     * @param array  $queryParameters Query parameters
358
     *
359
     * @return UriInterface
360
     */
361 21
    private function buildRequestUri(string $pathTemplate, array $pathParameters, array $queryParameters): UriInterface
362
    {
363 21
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
364 21
        $query = http_build_query($queryParameters);
365
366 21
        return $this->baseUri->withPath($path)->withQuery($query);
367
    }
368
369
    /**
370
     * @param array  $decodedBody
371
     * @param string $contentType
372
     *
373
     * @return string
374
     */
375 3
    private function serializeRequestBody(array $decodedBody, string $contentType): string
376
    {
377 3
        return $this->serializer->serialize(
378 3
            $decodedBody,
379 3
            DecoderUtils::extractFormatFromContentType($contentType)
380
        );
381
    }
382
383
    /**
384
     * Transform a given response into a denormalized PHP object
385
     * If the config option "returnResponse" is set to TRUE, it return a Response instead
386
     *
387
     * @param ResponseInterface  $response
388
     * @param ResponseDefinition $definition
389
     * @param RequestInterface   $request
390
     *
391
     * @return ResourceInterface|ResponseInterface|array|object
392
     */
393 17
    private function getDataFromResponse(ResponseInterface $response, ResponseDefinition $definition, RequestInterface $request)
394
    {
395 17
        if ($this->config['returnResponse'] === true) {
396 1
            return $response;
397
        }
398
399
        // @todo Find a better way to handle responses with a body definition
400 16
        if (!$definition->hasBodySchema()) {
401 11
            return new Item([], $request->getHeaders(), []);
402
        }
403
404 5
        return $this->serializer->deserialize(
405 5
            (string) $response->getBody(),
406 5
            ResourceInterface::class,
407 5
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
408
            [
409 5
                'response' => $response,
410 5
                'responseDefinition' => $definition,
411 5
                'request' => $request,
412
            ]
413
        );
414
    }
415
416
    /**
417
     * @param RequestInterface  $request
418
     * @param RequestDefinition $definition
419
     *
420
     * @throws ConstraintViolations
421
     */
422 20
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
423
    {
424 20
        if ($this->config['validateRequest'] === false) {
425 2
            return;
426
        }
427
428 18
        $this->messageValidator->validateRequest($request, $definition);
429 18
        if ($this->messageValidator->hasViolations()) {
430 2
            throw new RequestViolations(
431 2
                $this->messageValidator->getViolations()
432
            );
433
        }
434 16
    }
435
436
    /**
437
     * @param ResponseInterface $response
438
     * @param RequestDefinition $definition
439
     *
440
     * @throws ConstraintViolations
441
     */
442 18
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
443
    {
444 18
        if ($this->config['validateResponse'] === false) {
445 10
            return;
446
        }
447
448 8
        $this->messageValidator->validateResponse($response, $definition);
449 8
        if ($this->messageValidator->hasViolations()) {
450 2
            throw new ResponseViolations(
451 2
                $this->messageValidator->getViolations()
452
            );
453
        }
454 6
    }
455
}
456