Completed
Push — master ( 09627c...b27d19 )
by Joao Gilberto
04:20 queued 02:07
created

SwaggerBody::matchSchema()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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