Passed
Pull Request — master (#13)
by
unknown
03:28
created

ApiService::validateRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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