Completed
Push — master ( 675c2f...342161 )
by Guillem
01:39
created

SwaggerSchemaFactory   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 253
Duplicated Lines 0 %

Test Coverage

Coverage 97.83%

Importance

Changes 0
Metric Value
wmc 50
eloc 134
dl 0
loc 253
ccs 135
cts 138
cp 0.9783
rs 8.4
c 0
b 0
f 0

8 Methods

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