Completed
Pull Request — master (#20)
by Woody
01:29
created

SwaggerSchemaFactory::createSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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