Completed
Pull Request — master (#20)
by Woody
02:17 queued 12s
created

SwaggerSchemaFactory   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Test Coverage

Coverage 53.19%

Importance

Changes 0
Metric Value
wmc 53
eloc 137
dl 0
loc 257
ccs 75
cts 141
cp 0.5319
rs 6.96
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A createSchema() 0 13 4
A resolveSchemaFile() 0 41 5
A createResponseDefinition() 0 22 4
A containsBodyParametersLocations() 0 13 5
A createParameter() 0 24 6
B expandSchemaReferences() 0 8 7
F createRequestDefinitions() 0 82 15
B guessSupportedContentTypes() 0 28 7

How to fix   Complexity   

Complex Class

Complex classes like SwaggerSchemaFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SwaggerSchemaFactory, and based on these observations, apply Extract Interface, too.

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 stdClass;
15
use Symfony\Component\Yaml\Yaml;
16
17
/**
18
 * Create a schema definition from a Swagger file
19
 */
20
class SwaggerSchemaFactory implements SchemaFactory
21
{
22 4
    public function createSchema(string $schemaFile): Schema
23
    {
24 4
        $schema = $this->resolveSchemaFile($schemaFile);
25
26 3
        $host = isset($schema->host) ? $schema->host : null;
27 3
        $basePath = isset($schema->basePath) ? $schema->basePath : '';
28 3
        $schemes = isset($schema->schemes) ? $schema->schemes : ['http'];
29
30 3
        return new Schema(
31 3
            $this->createRequestDefinitions($schema),
0 ignored issues
show
Bug introduced by
$this->createRequestDefinitions($schema) of type ElevenLabs\Api\Definition\RequestDefinition is incompatible with the type ElevenLabs\Api\Definition\RequestDefinitions expected by parameter $requestDefinitions of ElevenLabs\Api\Schema::__construct(). ( Ignorable by Annotation )

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

31
            /** @scrutinizer ignore-type */ $this->createRequestDefinitions($schema),
Loading history...
32
            $basePath,
33
            $host,
0 ignored issues
show
Bug introduced by
It seems like $host can also be of type null; however, parameter $host of ElevenLabs\Api\Schema::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

33
            /** @scrutinizer ignore-type */ $host,
Loading history...
34
            $schemes
35
        );
36
    }
37
38 4
    protected function resolveSchemaFile($schemaFile): stdClass
39
    {
40 4
        $extension = pathinfo($schemaFile, PATHINFO_EXTENSION);
41 4
        switch ($extension) {
42 4
            case 'yml':
43 4
            case 'yaml':
44
                if (!class_exists(Yaml::class)) {
45
                    // @codeCoverageIgnoreStart
46
                    throw new \InvalidArgumentException(
47
                        'You need to require the "symfony/yaml" component in order to parse yml files'
48
                    );
49
                    // @codeCoverageIgnoreEnd
50
                }
51
52
                $uriRetriever = new YamlUriRetriever();
53
                break;
54 4
            case 'json':
55 3
                $uriRetriever = new UriRetriever();
56 3
                break;
57
            default:
58 1
                throw new \InvalidArgumentException(
59 1
                    sprintf(
60 1
                        'file "%s" does not provide a supported extension choose either json, yml or yaml',
61 1
                        $schemaFile
62
                    )
63
                );
64
        }
65
66 3
        $schemaStorage = new SchemaStorage(
67 3
            $uriRetriever,
68 3
            new UriResolver()
69
        );
70
71 3
        $schema = $schemaStorage->getSchema($schemaFile);
72
73
        // JsonSchema normally defers resolution of $ref values until validation.
74
        // That does not work for us, because we need to have the complete schema
75
        // to build definitions.
76 3
        $this->expandSchemaReferences($schema, $schemaStorage);
77
78 3
        return $schema;
79
    }
80
81 3
    private function expandSchemaReferences(&$schema, SchemaStorage $schemaStorage): void
82
    {
83 3
        foreach ($schema as &$member) {
84 3
            if (is_object($member) && property_exists($member, '$ref') && is_string($member->{'$ref'})) {
85
                $member = $schemaStorage->resolveRef($member->{'$ref'});
86
            }
87 3
            if (is_object($member) || is_array($member)) {
88 3
                $this->expandSchemaReferences($member, $schemaStorage);
89
            }
90
        }
91 3
    }
92
93 3
    protected function createRequestDefinitions(stdClass $schema): RequestDefinition
94
    {
95 3
        $definitions = [];
96 3
        $defaultConsumedContentTypes = [];
97 3
        $defaultProducedContentTypes = [];
98
99 3
        if (isset($schema->consumes)) {
100
            $defaultConsumedContentTypes = $schema->consumes;
101
        }
102 3
        if (isset($schema->produces)) {
103
            $defaultProducedContentTypes = $schema->produces;
104
        }
105
106 3
        $basePath = isset($schema->basePath) ? $schema->basePath : '';
107
108 3
        foreach ($schema->paths as $pathTemplate => $methods) {
109 3
            foreach ($methods as $method => $definition) {
110 3
                $method = strtoupper($method);
111 3
                $contentTypes = $defaultConsumedContentTypes;
112 3
                if (isset($definition->consumes)) {
113
                    $contentTypes = $definition->consumes;
114
                }
115
116 3
                if (!isset($definition->operationId)) {
117 1
                    throw new \LogicException(
118 1
                        sprintf(
119 1
                            'You need to provide an operationId for %s %s',
120 1
                            $method,
121 1
                            $pathTemplate
122
                        )
123
                    );
124
                }
125
126 2
                if (empty($contentTypes) && $this->containsBodyParametersLocations($definition)) {
127 1
                    $contentTypes = $this->guessSupportedContentTypes($definition, $pathTemplate);
128
                }
129
130 1
                if (!isset($definition->responses)) {
131 1
                    throw new \LogicException(
132 1
                        sprintf(
133 1
                            'You need to specify at least one response for %s %s',
134 1
                            $method,
135 1
                            $pathTemplate
136
                        )
137
                    );
138
                }
139
140
                if (!isset($definition->parameters)) {
141
                    $definition->parameters = [];
142
                }
143
144
                $requestParameters = [];
145
                foreach ($definition->parameters as $parameter) {
146
                    $requestParameters[] = $this->createParameter($parameter);
147
                }
148
149
                $responseContentTypes = $defaultProducedContentTypes;
150
                if (isset($definition->produces)) {
151
                    $responseContentTypes = $definition->produces;
152
                }
153
154
                $responseDefinitions = [];
155
                foreach ($definition->responses as $statusCode => $response) {
156
                    $responseDefinitions[] = $this->createResponseDefinition(
157
                        $statusCode,
158
                        $responseContentTypes,
159
                        $response
160
                    );
161
                }
162
163
                $definitions[] = new RequestDefinition(
164
                    $method,
165
                    $definition->operationId,
166
                    $basePath.$pathTemplate,
167
                    new Parameters($requestParameters),
168
                    $contentTypes,
169
                    $responseDefinitions
170
                );
171
            }
172
        }
173
174
        return new RequestDefinitions($definitions);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new ElevenLabs\Ap...finitions($definitions) returns the type ElevenLabs\Api\Definition\RequestDefinitions which is incompatible with the type-hinted return ElevenLabs\Api\Definition\RequestDefinition.
Loading history...
175
    }
176
177 2
    private function containsBodyParametersLocations(stdClass $definition): bool
178
    {
179 2
        if (!isset($definition->parameters)) {
180 1
            return false;
181
        }
182
183 1
        foreach ($definition->parameters as $parameter) {
184 1
            if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) {
185 1
                return true;
186
            }
187
        }
188
189
        return false;
190
    }
191
192
    /**
193
     * @return string[]
194
     */
195 1
    private function guessSupportedContentTypes(stdClass $definition, string $pathTemplate): array
196
    {
197 1
        if (!isset($definition->parameters)) {
198
            return [];
199
        }
200
201 1
        $bodyLocations = [];
202 1
        foreach ($definition->parameters as $parameter) {
203 1
            if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) {
204 1
                $bodyLocations[] = $parameter->in;
205
            }
206
        }
207
208 1
        if (count($bodyLocations) > 1) {
209 1
            throw new \LogicException(
210 1
                sprintf(
211 1
                    'Parameters cannot have %s locations at the same time in %s',
212 1
                    implode(' and ', $bodyLocations),
213 1
                    $pathTemplate
214
                )
215
            );
216
        }
217
218
        if (count($bodyLocations) === 1) {
219
            return [Parameter::BODY_LOCATIONS_TYPES[current($bodyLocations)]];
220
        }
221
222
        return [];
223
    }
224
225
    /**
226
     * @param string|int $statusCode
227
     * @param string[] $allowedContentTypes
228
     */
229
    protected function createResponseDefinition($statusCode, array $allowedContentTypes, stdClass $response): ResponseDefinition
230
    {
231
        $parameters = [];
232
        if (isset($response->schema)) {
233
            $parameters[] = $this->createParameter((object) [
234
                'in' => 'body',
235
                'name' => 'body',
236
                'required' => true,
237
                'schema' => $response->schema
238
            ]);
239
        }
240
241
        if (isset($response->headers)) {
242
            foreach ($response->headers as $headerName => $schema) {
243
                $schema->in = 'header';
244
                $schema->name = $headerName;
245
                $schema->required = true;
246
                $parameters[] = $this->createParameter($schema);
247
            }
248
        }
249
250
        return new ResponseDefinition($statusCode, $allowedContentTypes, new Parameters($parameters));
251
    }
252
253
    protected function createParameter(stdClass $parameter): Parameter
254
    {
255
        $parameter = get_object_vars($parameter);
256
        $location = $parameter['in'];
257
        $name = $parameter['name'];
258
        $schema = isset($parameter['schema']) ? $parameter['schema'] : new stdClass();
259
        $required = isset($parameter['required']) ? $parameter['required'] : false;
260
261
        unset($parameter['in']);
262
        unset($parameter['name']);
263
        unset($parameter['required']);
264
        unset($parameter['schema']);
265
266
        // Every remaining parameter may be json schema properties
267
        foreach ($parameter as $key => $value) {
268
            $schema->{$key} = $value;
269
        }
270
271
        // It's not relevant to validate file type
272
        if (isset($schema->format) && $schema->format === 'file') {
273
            $schema = null;
274
        }
275
276
        return new Parameter($location, $name, $required, $schema);
277
    }
278
}
279