Passed
Pull Request — master (#16)
by Woody
02:09
created

containsBodyParametersLocations()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
ccs 0
cts 7
cp 0
rs 9.6111
c 0
b 0
f 0
cc 5
nc 4
nop 1
crap 30
1
<?php
2
namespace ElevenLabs\Api\Factory;
3
4
use ElevenLabs\Api\Definition\RequestDefinition;
5
use ElevenLabs\Api\Definition\Parameter;
6
use ElevenLabs\Api\Definition\Parameters;
7
use ElevenLabs\Api\Definition\RequestDefinitions;
8
use ElevenLabs\Api\Definition\ResponseDefinition;
9
use ElevenLabs\Api\Schema;
10
use ElevenLabs\Api\JsonSchema\Uri\YamlUriRetriever;
11
use JsonSchema\SchemaStorage;
12
use JsonSchema\Uri\UriResolver;
13
use JsonSchema\Uri\UriRetriever;
14
use Symfony\Component\Yaml\Yaml;
15
16
/**
17
 * Create a schema definition from a Swagger file
18
 */
19
class SwaggerSchemaFactory implements SchemaFactory
20
{
21
    /**
22
     * @param string $schemaFile (must start with a scheme: file://, http:// or https://)
23
     *
24
     * @return Schema
25
     */
26 4
    public function createSchema($schemaFile)
27
    {
28 4
        $schema = $this->resolveSchemaFile($schemaFile);
29
30
        $host = isset($schema->host) ? $schema->host : null;
31
        $basePath = isset($schema->basePath) ? $schema->basePath : '';
32
        $schemes = isset($schema->schemes) ? $schema->schemes : ['http'];
33
34
        return new Schema(
35
            $this->createRequestDefinitions($schema),
36
            $basePath,
37
            $host,
38
            $schemes
39
        );
40
    }
41
42
    /**
43
     *
44
     * @param string $schemaFile
45
     *
46
     * @return object
47
     */
48 4
    protected function resolveSchemaFile($schemaFile)
49
    {
50 4
        $extension = pathinfo($schemaFile, PATHINFO_EXTENSION);
51 4
        switch ($extension) {
52 4
            case 'yml':
53 4
            case 'yaml':
54
                if (!class_exists(Yaml::class)) {
55
                    // @codeCoverageIgnoreStart
56
                    throw new \InvalidArgumentException(
57
                        'You need to require the "symfony/yaml" component in order to parse yml files'
58
                    );
59
                    // @codeCoverageIgnoreEnd
60
                }
61
62
                $uriRetriever = new YamlUriRetriever();
63
                break;
64 4
            case 'json':
65 3
                $uriRetriever = new UriRetriever();
66 3
                break;
67
            default:
68 1
                throw new \InvalidArgumentException(
69 1
                    sprintf(
70 1
                        'file "%s" does not provide a supported extension choose either json, yml or yaml',
71 1
                        $schemaFile
72
                    )
73
                );
74
        }
75
76 3
        $schemaStorage = new SchemaStorage(
77 3
            $uriRetriever,
78 3
            new UriResolver()
79
        );
80
81 3
        $schema = $schemaStorage->getSchema($schemaFile);
82
83
        // JsonSchema normally defers resolution of $ref values until validation.
84
        // That does not work for us, because we need to have the complete schema
85
        // to build definitions.
86 3
        $this->expandSchemaReferences($schema, $schemaStorage);
87
88
        return $schema;
89
    }
90
91 3
    private function expandSchemaReferences(\stdClass &$schema, SchemaStorage $schemaStorage)
92
    {
93 3
        foreach ($schema as &$member) {
94 3
            if (is_object($member) && property_exists($member, '$ref') && is_string($member->{'$ref'})) {
95
                $member = $schemaStorage->resolveRef($member->{'$ref'});
96
            }
97 3
            if (is_object($member) || is_array($member)) {
98 3
                $this->expandReferences($member, $schemaStorage);
0 ignored issues
show
Bug introduced by
The method expandReferences() does not exist on ElevenLabs\Api\Factory\SwaggerSchemaFactory. ( Ignorable by Annotation )

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

98
                $this->/** @scrutinizer ignore-call */ 
99
                       expandReferences($member, $schemaStorage);

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...
99
            }
100
        }
101
    }
102
103
    /**
104
     * @param \stdClass $schema
105
     * @return RequestDefinitions
106
     */
107
    protected function createRequestDefinitions(\stdClass $schema)
108
    {
109
        $definitions = [];
110
        $defaultConsumedContentTypes = [];
111
        $defaultProducedContentTypes = [];
112
113
        if (isset($schema->consumes)) {
114
            $defaultConsumedContentTypes = $schema->consumes;
115
        }
116
        if (isset($schema->produces)) {
117
            $defaultProducedContentTypes = $schema->produces;
118
        }
119
120
        $basePath = isset($schema->basePath) ? $schema->basePath : '';
121
122
        foreach ($schema->paths as $pathTemplate => $methods) {
123
            foreach ($methods as $method => $definition) {
124
                $method = strtoupper($method);
125
                $contentTypes = $defaultConsumedContentTypes;
126
                if (isset($definition->consumes)) {
127
                    $contentTypes = $definition->consumes;
128
                }
129
130
                if (!isset($definition->operationId)) {
131
                    throw new \LogicException(
132
                        sprintf(
133
                            'You need to provide an operationId for %s %s',
134
                            $method,
135
                            $pathTemplate
136
                        )
137
                    );
138
                }
139
140
                if (empty($contentTypes) && $this->containsBodyParametersLocations($definition)) {
141
                    $contentTypes = $this->guessSupportedContentTypes($definition, $pathTemplate);
142
                }
143
144
                if (!isset($definition->responses)) {
145
                    throw new \LogicException(
146
                        sprintf(
147
                            'You need to specify at least one response for %s %s',
148
                            $method,
149
                            $pathTemplate
150
                        )
151
                    );
152
                }
153
154
                if (!isset($definition->parameters)) {
155
                    $definition->parameters = [];
156
                }
157
158
                $requestParameters = [];
159
                foreach ($definition->parameters as $parameter) {
160
                    $requestParameters[] = $this->createParameter($parameter);
161
                }
162
163
                $responseContentTypes = $defaultProducedContentTypes;
164
                if (isset($definition->produces)) {
165
                    $responseContentTypes = $definition->produces;
166
                }
167
168
                $responseDefinitions = [];
169
                foreach ($definition->responses as $statusCode => $response) {
170
                    $responseDefinitions[] = $this->createResponseDefinition(
171
                        $statusCode,
172
                        $responseContentTypes,
173
                        $response
174
                    );
175
                }
176
177
                $definitions[] = new RequestDefinition(
178
                    $method,
179
                    $definition->operationId,
180
                    $basePath.$pathTemplate,
181
                    new Parameters($requestParameters),
182
                    $contentTypes,
183
                    $responseDefinitions
184
                );
185
            }
186
        }
187
188
        return new RequestDefinitions($definitions);
189
    }
190
191
    /**
192
     * @return bool
193
     */
194
    private function containsBodyParametersLocations(\stdClass $definition)
195
    {
196
        if (!isset($definition->parameters)) {
197
            return false;
198
        }
199
200
        foreach ($definition->parameters as $parameter) {
201
            if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) {
202
                return true;
203
            }
204
        }
205
206
        return false;
207
    }
208
209
    /**
210
     * @param \stdClass $definition
211
     * @param string $pathTemplate
212
     *
213
     * @return array
214
     */
215
    private function guessSupportedContentTypes(\stdClass $definition, $pathTemplate)
216
    {
217
        if (!isset($definition->parameters)) {
218
            return [];
219
        }
220
221
        $bodyLocations = [];
222
        foreach ($definition->parameters as $parameter) {
223
            if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) {
224
                $bodyLocations[] = $parameter->in;
225
            }
226
        }
227
228
        if (count($bodyLocations) > 1) {
229
            throw new \LogicException(
230
                sprintf(
231
                    'Parameters cannot have %s locations at the same time in %s',
232
                    implode(' and ', $bodyLocations),
233
                    $pathTemplate
234
                )
235
            );
236
        }
237
238
        if (count($bodyLocations) === 1) {
239
            return [Parameter::BODY_LOCATIONS_TYPES[current($bodyLocations)]];
240
        }
241
242
        return [];
243
    }
244
245
    protected function createResponseDefinition($statusCode, array $defaultProducedContentTypes, \stdClass $response)
246
    {
247
        $allowedContentTypes = $defaultProducedContentTypes;
248
        $parameters = [];
249
        if (isset($response->schema)) {
250
            $parameters[] = $this->createParameter((object) [
251
                'in' => 'body',
252
                'name' => 'body',
253
                'required' => true,
254
                'schema' => $response->schema
255
            ]);
256
        }
257
258
        if (isset($response->headers)) {
259
            foreach ($response->headers as $headerName => $schema) {
260
                $schema->in = 'header';
261
                $schema->name = $headerName;
262
                $schema->required = true;
263
                $parameters[] = $this->createParameter($schema);
264
            }
265
        }
266
267
        return new ResponseDefinition($statusCode, $allowedContentTypes, new Parameters($parameters));
268
    }
269
270
    /**
271
     * Create a Parameter from a swagger parameter
272
     *
273
     * @param \stdClass $parameter
274
     *
275
     * @return Parameter
276
     */
277
    protected function createParameter(\stdClass $parameter)
278
    {
279
        $parameter = get_object_vars($parameter);
280
        $location = $parameter['in'];
281
        $name = $parameter['name'];
282
        $schema = isset($parameter['schema']) ? $parameter['schema'] : new \stdClass();
283
        $required = isset($parameter['required']) ? $parameter['required'] : false;
284
285
        unset($parameter['in']);
286
        unset($parameter['name']);
287
        unset($parameter['required']);
288
        unset($parameter['schema']);
289
290
        // Every remaining parameter may be json schema properties
291
        foreach ($parameter as $key => $value) {
292
            $schema->{$key} = $value;
293
        }
294
295
        // It's not relevant to validate file type
296
        if (isset($schema->format) && $schema->format === 'file') {
297
            $schema = null;
298
        }
299
300
        return new Parameter($location, $name, $required, $schema);
301
    }
302
}
303