Completed
Push — master ( 11b317...37df4d )
by Lucas
09:27
created

Swagger   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 3.82%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 6
dl 0
loc 318
ccs 6
cts 157
cp 0.0382
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
C getSwaggerSpec() 0 112 12
A getBasicStructure() 0 16 1
B getBasicPathStructure() 0 41 2
A getPathTags() 0 9 2
B getSummary() 0 23 6
B getSchemaRoutes() 0 26 2
1
<?php
2
/**
3
 * Generate swagger conform specs.
4
 */
5
6
namespace Graviton\SwaggerBundle\Service;
7
8
use Graviton\CoreBundle\Service\CoreUtils;
9
use Graviton\ExceptionBundle\Exception\MalformedInputException;
10
use Graviton\RestBundle\Service\RestUtils;
11
use Graviton\SchemaBundle\Model\SchemaModel;
12
use Graviton\SchemaBundle\SchemaUtils;
13
use Symfony\Component\Config\Definition\Exception\Exception;
14
use Symfony\Component\Routing\Route;
15
16
/**
17
 * A service that generates a swagger conform service spec dynamically.
18
 *
19
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
20
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
21
 * @link     http://swisscom.ch
22
 */
23
class Swagger
24
{
25
26
    /**
27
     * @var \Graviton\RestBundle\Service\RestUtils
28
     */
29
    private $restUtils;
30
31
    /**
32
     * @var SchemaModel
33
     */
34
    private $schemaModel;
35
36
    /**
37
     * @var SchemaUtils
38
     */
39
    private $schemaUtils;
40
41
    /**
42
     * @var CoreUtils
43
     */
44
    private $coreUtils;
45
46
    /**
47
     * Constructor
48
     *
49
     * @param RestUtils   $restUtils   rest utils
50
     * @param SchemaModel $schemaModel schema model instance
51
     * @param SchemaUtils $schemaUtils schema utils
52
     * @param CoreUtils   $coreUtils   coreUtils
53
     */
54 2
    public function __construct(
55
        RestUtils $restUtils,
56
        SchemaModel $schemaModel,
57
        SchemaUtils $schemaUtils,
58
        CoreUtils $coreUtils
59
    ) {
60 2
        $this->restUtils = $restUtils;
61 2
        $this->schemaModel = $schemaModel;
62 2
        $this->schemaUtils = $schemaUtils;
63 2
        $this->coreUtils = $coreUtils;
64 2
    }
65
66
    /**
67
     * Returns the swagger spec as array
68
     *
69
     * @return array Swagger spec
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
70
     */
71
    public function getSwaggerSpec()
72
    {
73
        $ret = $this->getBasicStructure();
74
        $routingMap = $this->restUtils->getServiceRoutingMap();
75
        $paths = array();
76
77
        foreach ($routingMap as $contName => $routes) {
78
            list(, $bundle,, $document) = explode('.', $contName);
79
80
            /** @var Route  $route */
81
            foreach ($routes as $routeName => $route) {
82
                $routeMethod = strtolower($route->getMethods()[0]);
83
84
                if ($routeMethod == 'options') {
85
                    continue;
86
                }
87
88
                // skip /schema/ stuff
89
                if (strpos($route->getPath(), '/schema/') !== false) {
90
                    list($pattern, $method, $data) = $this->getSchemaRoutes($route);
91
                    $paths[$pattern][$method] = $data;
92
                    continue;
93
                }
94
95
                /** @var \Graviton\RestBundle\Model\DocumentModel $thisModel */
96
                $thisModel = $this->restUtils->getModelFromRoute($route);
97
                if ($thisModel === false) {
98
                    throw new \LogicException(
99
                        sprintf(
100
                            'Could not resolve route "%s" to model',
101
                            $routeName
102
                        )
103
                    );
104
                }
105
106
                $entityClassName = str_replace('\\', '', get_class($thisModel));
107
108
                $schema = $this->schemaUtils->getModelSchema($entityClassName, $thisModel, false);
109
110
                $ret['definitions'][$entityClassName] = json_decode(
111
                    $this->restUtils->serializeContent($schema),
112
                    true
113
                );
114
115
                $isCollectionRequest = true;
116
                if (in_array('id', array_keys($route->getRequirements())) === true) {
117
                    $isCollectionRequest = false;
118
                }
119
120
                $thisPattern = $route->getPattern();
121
                $entityName = ucfirst($document);
122
123
                $thisPath = $this->getBasicPathStructure(
124
                    $isCollectionRequest,
125
                    $entityName,
126
                    $entityClassName,
127
                    $schema->getProperty('id')->getType()
128
                );
129
130
                $thisPath['tags'] = $this->getPathTags($route);
131
                $thisPath['operationId'] = $routeName;
132
                $thisPath['summary'] = $this->getSummary($routeMethod, $isCollectionRequest, $entityName);
133
134
                // post body stuff
135
                if ($routeMethod == 'put' || $routeMethod == 'post') {
136
                    // special handling for POST/PUT.. we need to have 2 schemas, one for response, one for request..
137
                    // we don't want to have ID in the request body within those requests do we..
138
                    // an exception is when id is required..
139
                    $incomingEntitySchema = $entityClassName;
140
                    if (is_null($schema->getRequired()) || !in_array('id', $schema->getRequired())) {
141
                        $incomingEntitySchema = $incomingEntitySchema . 'Incoming';
142
                        $incomingSchema = clone $schema;
143
                        $incomingSchema->removeProperty('id');
144
                        $ret['definitions'][$incomingEntitySchema] = json_decode(
145
                            $this->restUtils->serializeContent($incomingSchema),
146
                            true
147
                        );
148
                    }
149
150
                    $thisPath['parameters'][] = array(
151
                        'name' => $bundle,
152
                        'in' => 'body',
153
                        'description' => 'Post',
154
                        'required' => true,
155
                        'schema' => array('$ref' => '#/definitions/' . $incomingEntitySchema)
156
                    );
157
158
                    if ($routeMethod == 'post') {
159
                        $thisPath['responses'][201] = $thisPath['responses'][200];
160
                        unset($thisPath['responses'][200]);
161
                    }
162
163
                    // add error responses..
164
                    $thisPath['responses'][400] = array(
165
                        'description' => 'Bad request',
166
                        'schema' => array(
167
                            'type' => 'object'
168
                        )
169
                    );
170
                }
171
172
                $paths[$thisPattern][$routeMethod] = $thisPath;
173
            }
174
        }
175
176
        $ret['definitions']['SchemaModel'] = $this->schemaModel->getSchema();
177
178
        ksort($paths);
179
        $ret['paths'] = $paths;
180
181
        return $ret;
182
    }
183
184
    /**
185
     * Basic structure of the spec
186
     *
187
     * @return array Basic structure
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|array|string[]>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
188
     */
189
    private function getBasicStructure()
190
    {
191
        $ret = array();
192
        $ret['swagger'] = '2.0';
193
194
        $ret['info'] = array(
195
            'version' => $this->coreUtils->getWrapperVersion()['version'],
196
            'title' => 'Graviton REST Services',
197
            'description' => 'Testable API Documentation of this Graviton instance.',
198
        );
199
200
        $ret['basePath'] = '/';
201
        $ret['schemes'] = array('http', 'https');
202
203
        return $ret;
204
    }
205
206
    /**
207
     * Return the basic structure of a path element
208
     *
209
     * @param bool   $isCollectionRequest if collection request
210
     * @param string $entityName          entity name
211
     * @param string $entityClassName     class name
212
     * @param string $idType              type of id field
213
     *
214
     * @return array Path spec
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<strin...array<string,string>>>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
215
     */
216
    protected function getBasicPathStructure($isCollectionRequest, $entityName, $entityClassName, $idType)
217
    {
218
        $thisPath = array(
219
            'consumes' => array('application/json'),
220
            'produces' => array('application/json')
221
        );
222
223
        // collection return or not?
224
        if (!$isCollectionRequest) {
225
            // add object response
226
            $thisPath['responses'] = array(
227
                200 => array(
228
                    'description' => $entityName . ' response',
229
                    'schema' => array('$ref' => '#/definitions/' . $entityClassName)
230
                ),
231
                404 => array(
232
                    'description' => 'Resource not found'
233
                )
234
            );
235
236
            // add id param
237
            $thisPath['parameters'][] = array(
238
                'name' => 'id',
239
                'in' => 'path',
240
                'description' => 'ID of ' . $entityName . ' item to fetch/update',
241
                'required' => true,
242
                'type' => $idType
243
            );
244
        } else {
245
            // add array response
246
            $thisPath['responses'][200] = array(
247
                'description' => $entityName . ' response',
248
                'schema' => array(
249
                    'type' => 'array',
250
                    'items' => array('$ref' => '#/definitions/' . $entityClassName)
251
                )
252
            );
253
        }
254
255
        return $thisPath;
256
    }
257
258
    /**
259
     * Returns the tags (which influences the grouping visually) for a given route
260
     *
261
     * @param Route $route route
262
     * @param int   $part  part of route to use for generating a tag
263
     *
264
     * @return array Array of tags..
265
     */
266
    protected function getPathTags(Route $route, $part = 1)
267
    {
268
        $ret = array();
269
        $routeParts = explode('/', $route->getPath());
270
        if (isset($routeParts[$part])) {
271
            $ret[] = ucfirst($routeParts[$part]);
272
        }
273
        return $ret;
274
    }
275
276
    /**
277
     * Returns a meaningful summary depending on certain conditions
278
     *
279
     * @param string $method              Method
280
     * @param bool   $isCollectionRequest If collection request
281
     * @param string $entityName          Name of entity
282
     *
283
     * @return string summary
284
     */
285
    protected function getSummary($method, $isCollectionRequest, $entityName)
286
    {
287
        $ret = '';
288
        // meaningful descriptions..
289
        switch ($method) {
290
            case 'get':
291
                if ($isCollectionRequest) {
292
                    $ret = 'Get collection of ' . $entityName . ' resources';
293
                } else {
294
                    $ret = 'Get single ' . $entityName . ' resources';
295
                }
296
                break;
297
            case 'post':
298
                $ret = 'Create new ' . $entityName . ' resource';
299
                break;
300
            case 'put':
301
                $ret = 'Update existing ' . $entityName . ' resource';
302
                break;
303
            case 'delete':
304
                $ret = 'Delete existing ' . $entityName . ' resource';
305
        }
306
        return $ret;
307
    }
308
309
    /**
310
     * @param Route $route route
311
     *
312
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string|array<strin...ring>>[]|array|string>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
313
     */
314
    protected function getSchemaRoutes(Route $route)
315
    {
316
        $path = $route->getPath();
317
318
        $describedService = substr(substr($path, 7), 0, substr($path, -5) == '/item' ? -7 : -10);
319
320
        $tags = array_merge(['Schema'], $this->getPathTags($route, 2));
321
322
        return [
323
            $path,
324
            'get',
325
            [
326
                'produces' => [
327
                    'application/json',
328
                ],
329
                'responses' => [
330
                    200 => [
331
                        'description' => 'JSON-Schema for ' . $describedService . '.',
332
                        'schema' => ['$ref' => '#/definitions/SchemaModel'],
333
                    ]
334
                ],
335
                'tags' => $tags,
336
                'summary' => 'Get schema information for ' . $describedService . ' endpoints.',
337
            ]
338
        ];
339
    }
340
}
341