UriConstraint::normalizeNumericString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This source file is proprietary and part of Rebilly.
4
 *
5
 * (c) Rebilly SRL
6
 *     Rebilly Ltd.
7
 *     Rebilly Inc.
8
 *
9
 * @see https://www.rebilly.com
10
 */
11
12
namespace Rebilly\OpenAPI\PhpUnit;
13
14
use JsonSchema\Entity\JsonPointer;
15
use PHPUnit\Framework\Constraint\Constraint;
16
use Psr\Http\Message\UriInterface;
17
use Rebilly\OpenAPI\JsonSchema\Validator;
18
use Rebilly\OpenAPI\UnexpectedValueException;
19
use stdClass;
20
21
/**
22
 * Constraint that asserts that the URI matches the expected
23
 * allowed schemes, base URI, URI paths and query-params.
24
 */
25
final class UriConstraint extends Constraint
26
{
27
    private $servers;
28
29
    private $path;
30
31
    private $pathParameters;
32
33
    private $queryParameters;
34
35
    private $validator;
36
37
    private $errors = [];
38
39 11
    public function __construct(
40
        array $servers,
41
        string $path,
42
        array $pathParameters,
43
        array $queryParameters
44
    ) {
45 11
        $this->servers = array_map('strtolower', $servers);
46 11
        $this->path = $path;
47 11
        $this->pathParameters = array_map([$this, 'normalizeJsonSchema'], $pathParameters);
48 11
        $this->queryParameters = array_map([$this, 'normalizeJsonSchema'], $queryParameters);
49 11
        $this->validator = new Validator('undefined');
50
    }
51
52 6
    public function toString(): string
53
    {
54 6
        return 'matches an specified URI parts';
55
    }
56
57 11
    protected function matches($uri): bool
58
    {
59 11
        if (!$uri instanceof UriInterface) {
60 1
            throw new UnexpectedValueException('The object should implements UriInterface');
61
        }
62
63 10
        $baseUrl = null;
64
65 10
        foreach ($this->servers as $serverUrl) {
66 10
            if (mb_strpos((string) $uri, $serverUrl) === 0) {
67 9
                $baseUrl = $serverUrl;
68
69 9
                continue;
70
            }
71
        }
72
73 10
        if ($baseUrl === null) {
74 1
            $this->errors[] = [
75 1
                'property' => 'baseUrl',
76 1
                'message' => sprintf('Unexpected URL, does not found in defined servers (%s)', implode(', ', $this->servers)),
77
            ];
78
79 1
            return false;
80
        }
81
82 9
        $pathStart = mb_strlen($baseUrl) - mb_strpos($baseUrl, '/', mb_strpos($baseUrl, '://') + 3);
83 9
        $path = mb_substr($uri->getPath(), $pathStart + 1);
84 9
        $actualSegments = $this->splitString('#\/#', $path);
85 9
        $expectedSegments = $this->splitString('#\/#', $this->path);
86
87 9
        if (count($actualSegments) !== count($expectedSegments)) {
88 1
            $this->errors[] = [
89 1
                'property' => 'path',
90 1
                'message' => "Unexpected URI path, does not match the template ({$this->path})",
91
            ];
92
93 1
            return false;
94
        }
95
96 8
        foreach ($expectedSegments as $i => $expectedSegment) {
97 8
            $actualSegment = $actualSegments[$i];
98 8
            mb_strpos($expectedSegment, '{') === false
99 8
                ? $this->assertPathSegment($expectedSegment, $actualSegment)
100 6
                : $this->assertPathParam($expectedSegment, $actualSegment);
101
        }
102
103 8
        if (!empty($this->errors)) {
104 2
            return false;
105
        }
106
107 6
        parse_str($uri->getQuery(), $actualQueryParams);
108
109
        // TODO: Assert query params
110 6 View Code Duplication
        foreach ($this->queryParameters as $name => $queryParamSchema) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111 6
            if (isset($actualQueryParams[$name])) {
112 2
                $actualQueryParam = $actualQueryParams[$name];
113
114
                // TODO: Consider to disallow non-string params in query, that make no sense
115 2
                $actualQueryParam = $this->normalizeNumericString($actualQueryParam);
116
117 2
                $this->errors = array_merge(
118 2
                    $this->errors,
119 2
                    $this->validator->validate($actualQueryParam, $queryParamSchema, new JsonPointer('#/query'))
120
                );
121 4
            } elseif (isset($queryParamSchema->required) && $queryParamSchema->required) {
122 1
                $this->errors[] = [
123 1
                    'property' => 'query',
124 1
                    'message' => "Missing required query param ({$name})",
125
                ];
126
            }
127
        }
128
129 6
        return empty($this->errors);
130
    }
131
132 6
    protected function failureDescription($other): string
133
    {
134 6
        return json_encode((object) $this->normalizeUri($other)) . ' ' . $this->toString();
135
    }
136
137 6
    protected function additionalFailureDescription($other): string
138
    {
139 6
        return $this->validator->serializeErrors($this->errors);
140
    }
141
142 8
    private function assertPathSegment(string $expectedSegment, string $actualSegment): void
143
    {
144 8
        if ($actualSegment !== $expectedSegment) {
145 1
            $this->errors[] = [
146 1
                'property' => 'path',
147 1
                'message' => "Missing path segment ({$expectedSegment})",
148
            ];
149
        }
150
    }
151
152 6
    private function assertPathParam(string $expectedSegment, string $actualSegment): void
153
    {
154 6
        $pathParamSchema = $this->pathParameters[mb_substr($expectedSegment, 1, -1)];
155
156
        // TODO: Consider to disallow non-string params in path, that make no sense
157 6
        $actualSegment = $this->normalizeNumericString($actualSegment);
158
159 6
        $this->errors = array_merge(
160 6
            $this->errors,
161 6
            $this->validator->validate($actualSegment, $pathParamSchema, new JsonPointer('#/path'))
162
        );
163
    }
164
165 6
    private static function normalizeUri(UriInterface $uri): array
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
166
    {
167
        return [
168 6
            'schema' => $uri->getScheme(),
169 6
            'host' => $uri->getHost(),
170 6
            'path' => $uri->getPath(),
171
        ];
172
    }
173
174 11
    private static function normalizeJsonSchema($schema): stdClass
175
    {
176 11
        return (object) $schema;
177
    }
178
179
    /**
180
     * Cast numeric values, JSON validator does not do it.
181
     *
182
     * @param mixed $value
183
     *
184
     * @return mixed
185
     */
186 6
    private static function normalizeNumericString($value)
187
    {
188 6
        if (is_numeric($value)) {
189 4
            $value += 0;
190
        }
191
192 6
        return $value;
193
    }
194
195 9
    private static function splitString(string $pattern, string $subject): array
196
    {
197 9
        return preg_split($pattern, $subject, -1, PREG_SPLIT_NO_EMPTY);
198
    }
199
}
200