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