Completed
Push — master ( 48d93b...434f21 )
by Veaceslav
03:06
created

UriConstraint::splitString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
/**
3
 * This file is part of Rebilly.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @see http://rebilly.com
9
 */
10
11
namespace Rebilly\OpenAPI\PhpUnit;
12
13
use JsonSchema\Constraints\ConstraintInterface as Validator;
14
use JsonSchema\Constraints\Factory;
15
use PHPUnit_Framework_Constraint as Constraint;
16
use Psr\Http\Message\UriInterface;
17
use Rebilly\OpenAPI\UnexpectedValueException;
18
19
/**
20
 * Constraint that asserts that the URI matches the expected
21
 * allowed schemes, base URI, URI paths and query-params.
22
 */
23
final class UriConstraint extends Constraint
24
{
25
    /**
26
     * @var array
27
     */
28
    private $allowedSchemes;
29
30
    /**
31
     * @var string
32
     */
33
    private $basePath;
34
35
    /**
36
     * @var array
37
     */
38
    private $templateParams;
39
40
    /**
41
     * @var array
42
     */
43
    private $queryParams;
44
45
    /**
46
     * @var string
47
     */
48
    private $host;
49
50
    /**
51
     * @var array
52
     */
53
    private $errors = [];
54
55
    /**
56
     * @var string
57
     */
58
    private $template;
59
60
    /**
61
     * @var Validator
62
     */
63
    private $validator;
64
65
    /**
66
     * @param array $expectedSchemes
67
     * @param string $host
68
     * @param string $basePath
69
     * @param string $template
70
     * @param array $pathParams
71
     * @param array $queryParams
72
     */
73 9
    public function __construct(
74
        array $expectedSchemes,
75
        $host,
76
        $basePath,
77
        $template,
78
        array $pathParams,
79
        array $queryParams
80
    ) {
81 9
        parent::__construct();
82
83 9
        $this->allowedSchemes = array_map('strtolower', $expectedSchemes);
84 9
        $this->host = $host;
85 9
        $this->basePath = $basePath;
86 9
        $this->template = $template;
87 9
        $this->templateParams = array_map([$this, 'normalizeJsonSchema'], $pathParams);
88 9
        $this->queryParams = array_map([$this, 'normalizeJsonSchema'], $queryParams);
89 9
        $this->validator = $this->createJsonValidator('undefined');
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 9
    protected function matches($uri)
96
    {
97 9
        if (!$uri instanceof UriInterface) {
98 1
            throw new UnexpectedValueException('The object should implements UriInterface');
99
        }
100
101 8
        if (!in_array(strtolower($uri->getScheme()), $this->allowedSchemes, true)) {
102 1
            $this->errors[] = [
103 1
                'property' => 'scheme',
104 1
                'message' => 'Unsupported scheme (' . implode(', ', $this->allowedSchemes) . ')',
105
            ];
106
        }
107
108 8
        if ($uri->getHost() !== $this->host) {
109 1
            $this->errors[] = [
110 1
                'property' => 'host',
111 1
                'message' => "Unexpected host ({$this->host})",
112
            ];
113
        }
114
115 8
        if (strpos($uri->getPath(), "{$this->basePath}/") !== 0) {
116 1
            $this->errors[] = [
117 1
                'property' => 'basePath',
118 1
                'message' => "Unexpected base path ({$this->basePath})",
119
            ];
120
        } else {
121 7
            $path = substr($uri->getPath(), strlen($this->basePath) + 1);
122 7
            $actualSegments = $this->splitString('#\/#', $path);
123 7
            $expectedSegments = $this->splitString('#\/#', $this->template);
124
125 7
            if (count($actualSegments) !== count($expectedSegments)) {
126 2
                $this->errors[] = [
127 2
                    'property' => 'path',
128 2
                    'message' => "Unexpected URI path, does not match the template ({$this->template})",
129
                ];
130
            } else {
131 5
                foreach ($expectedSegments as $i => $expectedSegment) {
132 5
                    $actualSegment = $actualSegments[$i];
133
134 5
                    if (strpos($expectedSegment, '{') === false) {
135
                        // Assert path segment
136 5
                        if ($actualSegment !== $expectedSegment) {
137 1
                            $this->errors[] = [
138 1
                                'property' => 'path',
139 5
                                'message' => "Missing path segment ({$expectedSegment})",
140
                            ];
141
                        }
142
                    } else {
143
                        // Assert path param
144 5
                        $pathParamSchema = $this->templateParams[substr($expectedSegment, 1, -1)];
145
146
                        // TODO: Consider to disallow non-string params in path, that make no sense
147 5
                        $actualSegment = $this->normalizeNumericString($actualSegment);
148
149 5
                        $this->validator->check($actualSegment, $pathParamSchema, 'path');
150
                    }
151
                }
152
            }
153
        }
154
155 8
        parse_str($uri->getQuery(), $actualQueryParams);
156
157
        // TODO: Assert query params
158 8
        foreach ($this->queryParams as $name => $queryParamSchema) {
159 8
            if (isset($actualQueryParams[$name])) {
160 2
                $actualQueryParam = $actualQueryParams[$name];
161
162
                // TODO: Consider to disallow non-string params in query, that make no sense
163 2
                $actualQueryParam = $this->normalizeNumericString($actualQueryParam);
164
165 2
                $this->validator->check($actualQueryParam, $queryParamSchema, 'query');
166 6
            } elseif (isset($queryParamSchema->required) && $queryParamSchema->required) {
167 6
                $this->errors[] = [
168 6
                    'property' => 'query',
169 8
                    'message' => "Missing required query param ({$name})",
170
                ];
171
            }
172
        }
173
174 8
        $this->errors = array_merge($this->errors, $this->validator->getErrors());
175
176 8
        return empty($this->errors);
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 7
    protected function failureDescription($other)
183
    {
184 7
        return json_encode((object) $this->normalizeUri($other)) . ' ' . $this->toString();
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 7
    protected function additionalFailureDescription($other)
191
    {
192 7
        return "\n" . implode(
193 7
            "\n",
194
            array_map(
195 7
                function ($error) {
196 7
                    return "[{$error['property']}] {$error['message']}";
197 7
                },
198 7
                $this->errors
199
            )
200
        );
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 7
    public function toString()
207
    {
208 7
        return 'matches an specified URI parts';
209
    }
210
211
    /**
212
     * @param UriInterface $uri
213
     *
214
     * @return array
215
     */
216 7
    private static function normalizeUri(UriInterface $uri)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
217
    {
218
        return [
219 7
            'schema' => $uri->getScheme(),
220 7
            'host' => $uri->getHost(),
221 7
            'path' => $uri->getPath(),
222
        ];
223
    }
224
225
    /**
226
     * Ensure schema is object.
227
     *
228
     * @param object|array $schema
229
     *
230
     * @return object
231
     */
232 9
    private static function normalizeJsonSchema($schema)
233
    {
234 9
        return (object) $schema;
235
    }
236
237
    /**
238
     * Cast numeric values, JSON validator does not do it.
239
     *
240
     * @param mixed $value
241
     *
242
     * @return mixed
243
     */
244 5
    private static function normalizeNumericString($value)
245
    {
246 5
        if (is_numeric($value)) {
247 4
            $value += 0;
248
        }
249
250 5
        return $value;
251
    }
252
253
    /**
254
     * Split string to segments by regexp.
255
     *
256
     * @param string $pattern
257
     * @param string $subject
258
     *
259
     * @return array
260
     */
261 7
    private static function splitString($pattern, $subject)
262
    {
263 7
        return preg_split($pattern, $subject, -1, PREG_SPLIT_NO_EMPTY);
264
    }
265
266
    /**
267
     * @param string $context
268
     *
269
     * @return Validator
270
     */
271 9
    private static function createJsonValidator($context)
272
    {
273 9
        static $validators;
274
275 9
        if (!$validators) {
276 1
            $validators = new Factory();
277
        }
278
279 9
        return $validators->createInstanceFor($context);
280
    }
281
}
282