Passed
Pull Request — master (#13)
by
unknown
14:24
created

ApiService::serializeRequestBody()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 5
ccs 0
cts 0
cp 0
crap 2
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 13
     *
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 13
     * @throws \Assert\AssertionFailedException
106 13
     */
107 13
    public function __construct(UriFactory $uriFactory, UriTemplate $uriTemplate, HttpClient $client, MessageFactory $messageFactory, Schema $schema, MessageValidator $messageValidator, SerializerInterface $serializer, array $config = [])
108 13
    {
109 13
        $this->uriFactory = $uriFactory;
110 13
        $this->uriTemplate = $uriTemplate;
111 13
        $this->schema = $schema;
112 13
        $this->messageValidator = $messageValidator;
113 13
        $this->client = $client;
114 10
        $this->messageFactory = $messageFactory;
115
        $this->serializer = $serializer;
116
        $this->config = $this->getConfig($config);
117
        $this->baseUri = $this->getBaseUri();
118
    }
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 5
     * @throws ConstraintViolations
125
     * @throws \Http\Client\Exception
126 5
     *
127 5
     * @return ResourceInterface|ResponseInterface|array|object
128 5
     */
129
    public function call(string $operationId, array $params = [])
130 4
    {
131 4
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
132
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
133 3
        $this->validateRequest($request, $requestDefinition);
134 3
135 3
        $response = $this->client->sendRequest($request);
136 3
        $this->validateResponse($response, $requestDefinition);
137
138 3
        return $this->getDataFromResponse(
139
            $response,
140
            $requestDefinition->getResponseDefinition(
141 3
                $response->getStatusCode()
142
            ),
143
            $request
144
        );
145
    }
146
147
    /**
148
     * @param string $operationId
149
     * @param array  $params
150
     *
151
     * @throws \Exception
152 1
     *
153
     * @return Promise
154 1
     *
155
     */
156
    public function callAsync(string $operationId, array $params = []): Promise
157
    {
158
        if (!$this->client instanceof HttpAsyncClient) {
159
            throw new \RuntimeException(
160
                sprintf(
161
                    '"%s" does not support async request',
162
                    get_class($this->client)
163 1
                )
164 1
            );
165 1
        }
166
167 1
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
168
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
169
        $promise = $this->client->sendAsyncRequest($request);
170 1
171 1
        return $promise->then(
172 1
            function (ResponseInterface $response) use ($request, $requestDefinition) {
173 1
174
                return $this->getDataFromResponse(
175 1
                    $response,
176
                    $requestDefinition->getResponseDefinition(
177 1
                        $response->getStatusCode()
178
                    ),
179
                    $request
180
                );
181
            }
182
        );
183
    }
184
185
    public function getSchema(): Schema
186
    {
187 13
        return $this->schema;
188
    }
189
190 13
    /**
191 10
     * @return UriInterface
192 10
     */
193 10
    private function getBaseUri(): UriInterface
194 1
    {
195
        // Create a base uri from the API Schema
196
        if (null === $this->config['baseUri']) {
197 9
            $schemes = $this->schema->getSchemes();
198
            if (empty($schemes)) {
199 9
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
200 8
            }
201
202 9
            $scheme = null;
203 9
            foreach ($this->schema->getSchemes() as $candidate) {
204
                // Always prefer https
205
                if ('https' === $candidate) {
206 9
                    $scheme = 'https';
207 1
                }
208
                if (null === $scheme && 'http' === $candidate) {
209
                    $scheme = 'http';
210 8
                }
211 8
            }
212 1
213
            if (null === $scheme) {
214
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
215 7
            }
216
217 3
            $host = $this->schema->getHost();
218
            if ('' === $host) {
219
                throw new \LogicException('The host in the API Schema should not be null');
220
            }
221
222
            return $this->uriFactory->createUri($scheme.'://'.$host);
223
        } else {
224 13
            return $this->uriFactory->createUri($this->config['baseUri']);
225
        }
226 13
    }
227 13
228 13
    /**
229 13
     * @param array $config
230 13
     *
231
     * @throws \Assert\AssertionFailedException
232 13
     *
233
     * @return array
234
     */
235
    private function getConfig(array $config): array
236
    {
237
        $config = array_merge(self::DEFAULT_CONFIG, $config);
238
        Assertion::boolean($config['returnResponse']);
239
        Assertion::boolean($config['validateRequest']);
240
        Assertion::boolean($config['validateResponse']);
241
        Assertion::nullOrString($config['baseUri']);
242
243
        return array_intersect_key($config, self::DEFAULT_CONFIG);
244
    }
245 6
246
    /**
247 6
     * @param RequestDefinition $definition
248 6
     * @param array             $params
249 6
     *
250 6
     * @return RequestInterface
251 6
     */
252 6
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
253
    {
254 6
        $contentType = $definition->getContentTypes()[0] ?? 'application/json';
255 2
        $requestParameters = $definition->getRequestParameters();
256 2
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($requestParameters);
257
        $headers = array_merge(
258
            $headers,
259
            ['Content-Type' => $contentType, 'Accept' => $definition->getAccepts()[0] ?? 'application/json']
0 ignored issues
show
Bug introduced by
The method getAccepts() does not exist on ElevenLabs\Api\Definition\RequestDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

259
            ['Content-Type' => $contentType, 'Accept' => $definition->/** @scrutinizer ignore-call */ getAccepts()[0] ?? 'application/json']

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
260 2
        );
261 2
262 1
        foreach ($params as $name => $value) {
263 1
            $requestParameter = $requestParameters->getByName($name);
264 2
            if (null === $requestParameter) {
265 2
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter', $name));
266 2
            }
267 1
268
            switch ($requestParameter->getLocation()) {
269
                case 'path':
270 1
                    $path[$name] = $value;
271 2
                    break;
272
                case 'query':
273
                    $query[$name] = $value;
274
                    break;
275 6
                case 'header':
276 6
                    $headers[$name] = $value;
277 6
                    break;
278 6
                case 'body':
279 6
                    $body = $this->serializeRequestBody($value, $contentType);
280
                    break;
281
                case 'formData':
282 6
                    $formData[$name] = sprintf('%s=%s', $name, $value);
283
                    break;
284
            }
285
        }
286
287
        if (!empty($formData)) {
288
            $body = implode('&', $formData);
289
        }
290
291
        $request = $this->messageFactory->createRequest(
292
            $definition->getMethod(),
293
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
294
            $headers,
295
            $body
296
        );
297
298
        return $request;
299
    }
300 6
301
    private function getDefaultValues(Parameters $requestParameters): array
302 6
    {
303 6
        $path = [];
304
        $query = [];
305 6
        $headers = [];
306
        $body = null;
307
        $formData = [];
308
309
        /** @var Parameter $parameter */
310
        foreach ($requestParameters->getIterator() as $name => $parameter) {
311
            if (isset($parameter->getSchema()->default)) {
312
                $value = $parameter->getSchema()->default;
313
                switch ($parameter->getLocation()) {
314 1
                    case 'path':
315
                        $path[$name] = $value;
316 1
                        break;
317 1
                    case 'query':
318 1
                        $query[$name] = $value;
319
                        break;
320
                    case 'header':
321
                        $headers[$name] = $value;
322
                        break;
323
                    case 'formData':
324
                        $formData[$name] = sprintf('%s=%s', $name, $value);
325
                        break;
326
                    case 'body':
327
                        $body = $this->serializeRequestBody($value, $contentType);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $contentType seems to be never defined.
Loading history...
328
                        break;
329
                }
330
            }
331
        }
332 4
333
        return [$path, $query, $headers, $body, $formData];
334
    }
335
336
337 4
    /**
338
     * @param string $pathTemplate    A template path
339
     * @param array  $pathParameters  Path parameters
340
     * @param array  $queryParameters Query parameters
341
     *
342 4
     * @return UriInterface
343 1
     */
344
    private function buildRequestUri(string $pathTemplate, array $pathParameters, array $queryParameters): UriInterface
345
    {
346 3
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
347 3
        $query = http_build_query($queryParameters);
348 3
349 3
        return $this->baseUri->withPath($path)->withQuery($query);
350
    }
351 3
352 3
    /**
353 3
     * @param array  $decodedBody
354
     * @param string $contentType
355
     *
356
     * @return string
357
     */
358
    private function serializeRequestBody(array $decodedBody, string $contentType): string
359
    {
360
        return $this->serializer->serialize(
361
            $decodedBody,
362
            DecoderUtils::extractFormatFromContentType($contentType)
363
        );
364
    }
365
366
    /**
367 5
     * @param ResponseInterface  $response
368
     * @param ResponseDefinition $definition
369 5
     * @param RequestInterface   $request
370 1
     *
371
     * @return ResourceInterface|ResponseInterface|array|object
372
     */
373 4
    private function getDataFromResponse(ResponseInterface $response, ResponseDefinition $definition, RequestInterface $request)
374 4
    {
375 1
        if (true === $this->config['returnResponse']) {
376 1
            return $response;
377
        }
378
379 3
        // @todo Find a better way to handle responses with a body definition
380
        if (!$definition->hasBodySchema()) {
381
            return new Item([], $request->getHeaders(), []);
382
        }
383
        $statusCode = $response->getStatusCode();
384
385
        return $this->serializer->deserialize(
386
            (string) $response->getBody(),
387
            $statusCode >= 400 && $statusCode <= 599 ? ErrorInterface::class : ResourceInterface::class,
388
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
389
            [
390 4
                'response' => $response,
391
                'responseDefinition' => $definition,
392 4
                'request' => $request,
393 2
            ]
394
        );
395
    }
396 2
397 2
    /**
398 1
     * @param RequestInterface  $request
399 1
     * @param RequestDefinition $definition
400
     *
401
     * @throws ConstraintViolations
402 1
     */
403
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
404
    {
405
        if (false === $this->config['validateRequest']) {
406
            return;
407
        }
408
409
        $this->messageValidator->validateRequest($request, $definition);
410
        if ($this->messageValidator->hasViolations()) {
411
            throw new RequestViolations(
412
                $this->messageValidator->getViolations()
413
            );
414
        }
415
    }
416
417
    /**
418
     * @param ResponseInterface $response
419
     * @param RequestDefinition $definition
420
     *
421
     * @throws ConstraintViolations
422
     */
423
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
424
    {
425
        if (false === $this->config['validateResponse']) {
426
            return;
427
        }
428
429
        $this->messageValidator->validateResponse($response, $definition);
430
        if ($this->messageValidator->hasViolations()) {
431
            throw new ResponseViolations(
432
                $this->messageValidator->getViolations()
433
            );
434
        }
435
    }
436
}
437