Completed
Pull Request — master (#33)
by Joao
06:58
created

Body::match()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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