Passed
Pull Request — master (#33)
by Joao
08:26
created

Body::matchArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 4
nc 4
nop 4
1
<?php
2
3
namespace ByJG\ApiTools\Base;
4
5
use ByJG\ApiTools\Exception\DefinitionNotFoundException;
6
use ByJG\ApiTools\Exception\GenericSwaggerException;
7
use ByJG\ApiTools\Exception\InvalidDefinitionException;
8
use ByJG\ApiTools\Exception\InvalidRequestException;
9
use ByJG\ApiTools\Exception\NotMatchedException;
10
use ByJG\ApiTools\OpenApi\OpenApiResponseBody;
11
use ByJG\ApiTools\OpenApi\OpenApiSchema;
12
use ByJG\ApiTools\Swagger\SwaggerResponseBody;
13
use ByJG\ApiTools\Swagger\SwaggerSchema;
14
use InvalidArgumentException;
15
16
abstract class Body
17
{
18
    const SWAGGER_PROPERTIES="properties";
19
    const SWAGGER_REQUIRED="required";
20
21
    /**
22
     * @var Schema
23
     */
24
    protected $schema;
25
26
    protected $structure;
27
28
    protected $name;
29
30
    /**
31
     * OpenApi 2.0 does not describe null values, so this flag defines,
32
     * if match is ok when one of property, which has type, is null
33
     *
34
     * @var bool
35
     */
36
    protected $allowNullValues;
37
38
    /**
39
     * Body constructor.
40
     *
41
     * @param Schema $schema
42
     * @param string $name
43
     * @param array $structure
44
     * @param bool $allowNullValues
45
     */
46
    public function __construct(Schema $schema, $name, $structure, $allowNullValues = false)
47
    {
48
        $this->schema = $schema;
49
        $this->name = $name;
50
        if (!is_array($structure)) {
51
            throw new InvalidArgumentException('I expected the structure to be an array');
52
        }
53
        $this->structure = $structure;
54
        $this->allowNullValues = $allowNullValues;
55
    }
56
57
    /**
58
     * @param Schema $schema
59
     * @param $name
60
     * @param $structure
61
     * @param bool $allowNullValues
62
     * @return OpenApiResponseBody|SwaggerResponseBody
63
     * @throws GenericSwaggerException
64
     */
65
    public static function getInstance(Schema $schema, $name, $structure, $allowNullValues = false)
66
    {
67
        if ($schema instanceof SwaggerSchema) {
68
            return new SwaggerResponseBody($schema, $name, $structure, $allowNullValues);
69
        }
70
71
        if ($schema instanceof OpenApiSchema) {
72
            return new OpenApiResponseBody($schema, $name, $structure, $allowNullValues);
73
        }
74
75
        throw new GenericSwaggerException("Cannot get instance SwaggerBody or SchemaBody from " . get_class($schema));
76
    }
77
78
    abstract public function match($body);
79
80
    /**
81
     * @param $name
82
     * @param $schema
83
     * @param $body
84
     * @param $type
85
     * @return bool
86
     * @throws NotMatchedException
87
     */
88
    protected function matchString($name, $schema, $body, $type)
89
    {
90
        if ($type !== 'string') {
91
            return null;
92
        }
93
94
        if (isset($schema['enum']) && !in_array($body, $schema['enum'])) {
95
            throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure);
96
        }
97
98
        return true;
99
    }
100
101
    /**
102
     * @param $name
103
     * @param $body
104
     * @param $type
105
     * @return bool
106
     * @throws NotMatchedException
107
     */
108
    protected function matchNumber($name, $body, $type)
109
    {
110
        if ($type !== 'integer' && $type !== 'float' && $type !== 'number') {
111
            return null;
112
        }
113
114
        if (!is_numeric($body)) {
115
            throw new NotMatchedException("Expected '$name' to be numeric, but found '$body'. ", $this->structure);
116
        }
117
118
        return true;
119
    }
120
121
    /**
122
     * @param $name
123
     * @param $body
124
     * @param $type
125
     * @return bool
126
     * @throws NotMatchedException
127
     */
128
    protected function matchBool($name, $body, $type)
129
    {
130
        if ($type !== 'bool' && $type !== 'boolean') {
131
            return null;
132
        }
133
134
        if (!is_bool($body)) {
135
            throw new NotMatchedException("Expected '$name' to be boolean, but found '$body'. ", $this->structure);
136
        }
137
138
        return true;
139
    }
140
141
    /**
142
     * @param $name
143
     * @param $schema
144
     * @param $body
145
     * @param $type
146
     * @return bool
147
     * @throws DefinitionNotFoundException
148
     * @throws GenericSwaggerException
149
     * @throws InvalidDefinitionException
150
     * @throws InvalidRequestException
151
     * @throws NotMatchedException
152
     */
153
    protected function matchArray($name, $schema, $body, $type)
154
    {
155
        if ($type !== 'array') {
156
            return null;
157
        }
158
159
        foreach ((array)$body as $item) {
160
            if (!isset($schema['items'])) {  // If there is no type , there is no test.
161
                continue;
162
            }
163
            $this->matchSchema($name, $schema['items'], $item);
164
        }
165
        return true;
166
    }
167
168
    protected function matchTypes($name, $schema, $body)
169
    {
170
        if (!isset($schema['type'])) {
171
            return null;
172
        }
173
174
        $type = $schema['type'];
175
        $nullable = isset($schema['nullable']) ? (bool)$schema['nullable'] : $this->schema->isAllowNullValues();
176
177
        $validators = [
178
            function () use ($name, $body, $type, $nullable)
179
            {
180
                return $this->matchNull($name, $body, $type, $nullable);
181
            },
182
183
            function () use ($name, $schema, $body, $type)
184
            {
185
                return $this->matchString($name, $schema, $body, $type);
186
            },
187
188
            function () use ($name, $body, $type)
189
            {
190
                return $this->matchNumber($name, $body, $type);
191
            },
192
193
            function () use ($name, $body, $type)
194
            {
195
                return $this->matchBool($name, $body, $type);
196
            },
197
198
            function () use ($name, $schema, $body, $type)
199
            {
200
                return $this->matchArray($name, $schema, $body, $type);
201
            }
202
        ];
203
204
        foreach ($validators as $validator) {
205
            $result = $validator();
206
            if (!is_null($result)) {
207
                return $result;
208
            }
209
        }
210
211
        return null;
212
    }
213
214
    /**
215
     * @param $name
216
     * @param $schema
217
     * @param $body
218
     * @return bool|null
219
     * @throws DefinitionNotFoundException
220
     * @throws GenericSwaggerException
221
     * @throws InvalidDefinitionException
222
     * @throws InvalidRequestException
223
     * @throws NotMatchedException
224
     */
225
    public function matchObjectProperties($name, $schema, $body)
226
    {
227
        if (!isset($schema[self::SWAGGER_PROPERTIES])) {
228
            return null;
229
        }
230
231
        if (!is_array($body)) {
232
            throw new InvalidRequestException(
233
                "I expected an array here, but I got an string. Maybe you did wrong request?",
234
                $body
235
            );
236
        }
237
238
        if (!isset($schema[self::SWAGGER_REQUIRED])) {
239
            $schema[self::SWAGGER_REQUIRED] = [];
240
        }
241
        foreach ($schema[self::SWAGGER_PROPERTIES] as $prop => $def) {
242
            $required = array_search($prop, $schema[self::SWAGGER_REQUIRED]);
243
244
            if (!array_key_exists($prop, $body)) {
245
                if ($required !== false) {
246
                    throw new NotMatchedException("Required property '$prop' in '$name' not found in object");
247
                }
248
                unset($body[$prop]);
249
                continue;
250
            }
251
252
            $this->matchSchema($prop, $def, $body[$prop]);
253
            unset($schema[self::SWAGGER_PROPERTIES][$prop]);
254
            if ($required !== false) {
255
                unset($schema[self::SWAGGER_REQUIRED][$required]);
256
            }
257
            unset($body[$prop]);
258
        }
259
260
        if (count($schema[self::SWAGGER_REQUIRED]) > 0) {
261
            throw new NotMatchedException(
262
                "The required property(ies) '"
263
                . implode(', ', $schema[self::SWAGGER_REQUIRED])
264
                . "' does not exists in the body.",
265
                $this->structure
266
            );
267
        }
268
269
        if (count($body) > 0) {
270
            throw new NotMatchedException(
271
                "The property(ies) '"
272
                . implode(', ', array_keys($body))
273
                . "' has not defined in '$name'",
274
                $body
275
            );
276
        }
277
        return true;
278
    }
279
280
    /**
281
     * @param string $name
282
     * @param $schema
283
     * @param array $body
284
     * @return bool
285
     * @throws DefinitionNotFoundException
286
     * @throws InvalidDefinitionException
287
     * @throws GenericSwaggerException
288
     * @throws InvalidRequestException
289
     * @throws NotMatchedException
290
     */
291
    protected function matchSchema($name, $schema, $body)
292
    {
293
        // Match Single Types
294
        if ($this->matchTypes($name, $schema, $body)) {
295
            return true;
296
        }
297
298
        if(!isset($schema['$ref']) && isset($schema['content'])) {
299
            $schema['$ref'] = $schema['content'][key($schema['content'])]['schema']['$ref'];
300
        }
301
302
        // Get References and try to match it again
303
        if (isset($schema['$ref'])) {
304
            $defintion = $this->schema->getDefinition($schema['$ref']);
305
            return $this->matchSchema($schema['$ref'], $defintion, $body);
306
        }
307
308
        // Match object properties
309
        if ($this->matchObjectProperties($name, $schema, $body)) {
310
            return true;
311
        }
312
313
        /**
314
         * OpenApi 2.0 does not describe ANY object value
315
         * But there is hack that makes ANY object possible, described in link below
316
         * To make that hack works, we need such condition
317
         * @link https://stackoverflow.com/questions/32841298/swagger-2-0-what-schema-to-accept-any-complex-json-value
318
         */
319
        if ($schema === []) {
320
            return true;
321
        }
322
323
        throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name");
324
    }
325
326
    /**
327
     * @param $name
328
     * @param $body
329
     * @param $type
330
     * @param $nullable
331
     * @return bool
332
     * @throws NotMatchedException
333
     */
334
    protected function matchNull($name, $body, $type, $nullable)
335
    {
336
        if (!is_null($body)) {
337
            return null;
338
        }
339
340
        if (!$nullable) {
341
            throw new NotMatchedException(
342
                "Value of property '$name' is null, but should be of type '$type'",
343
                $this->structure
344
            );
345
        }
346
347
        return true;
348
    }
349
}
350