Passed
Push — master ( 8a3a90...ff3ac2 )
by Eric
02:15
created

Utils::validateCachePath()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 10
ccs 6
cts 6
cp 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Esi\LibrariesIO.
7
 *
8
 * (c) 2023-2024 Eric Sizemore <https://github.com/ericsizemore>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13
14
namespace Esi\LibrariesIO;
15
16
use Esi\LibrariesIO\Exception\InvalidEndpointException;
17
use Esi\LibrariesIO\Exception\InvalidEndpointOptionsException;
18
use JsonException;
19
use Psr\Http\Message\ResponseInterface;
20
use stdClass;
21
22
use function implode;
23
use function is_dir;
24
use function is_writable;
25
use function json_decode;
26
use function ltrim;
27
use function max;
28
use function min;
29
use function str_contains;
30
use function str_ends_with;
31
use function str_replace;
32
use function strtoupper;
33
34
use const JSON_THROW_ON_ERROR;
35
36
/**
37
 * Utility class.
38
 */
39
final class Utils
40
{
41
    private const MAX_PERPAGE_ALLOWED = 100;
42
    private const MIN_PAGE_ALLOWED    = 1;
43
    private const MIN_PERPAGE_ALLOWED = 30;
44
45
    /**
46
     * @var array<string, array{format: string, options: array{}|array<string>, method: string}>
47
     */
48
    public static array $projectParameters = [
49
        'contributors'           => ['format' => ':platform/:name/contributors', 'options' => ['platform', 'name'], 'method' => 'get'],
50
        'dependencies'           => ['format' => ':platform/:name/:version/dependencies', 'options' => ['platform', 'name', 'version'], 'method' => 'get'],
51
        'dependent_repositories' => ['format' => ':platform/:name/dependent_repositories', 'options' => ['platform', 'name'], 'method' => 'get'],
52
        'dependents'             => ['format' => ':platform/:name/dependents', 'options' => ['platform', 'name'], 'method' => 'get'],
53
        'search'                 => ['format' => 'search', 'options' => ['query', 'sort'], 'method' => 'get'],
54
        'sourcerank'             => ['format' => ':platform/:name/sourcerank', 'options' => ['platform', 'name'], 'method' => 'get'],
55
        'project'                => ['format' => ':platform/:name', 'options' => ['platform', 'name'], 'method' => 'get'],
56
    ];
57
58
    /**
59
     * @var array<string, array{format: string, options: array{}|array<string>, method: string}>
60
     */
61
    public static array $repositoryParameters = [
62
        'dependencies' => ['format' => 'github/:owner/:name/dependencies', 'options' => ['owner', 'name'], 'method' => 'get'],
63
        'projects'     => ['format' => 'github/:owner/:name/projects', 'options' => ['owner', 'name'], 'method' => 'get'],
64
        'repository'   => ['format' => 'github/:owner/:name', 'options' => ['owner', 'name'], 'method' => 'get'],
65
    ];
66
67
    /**
68
     * @var array<string, array{format: string, options: array{}|array<string>, method: string}>
69
     */
70
    public static array $subscriptionParameters = [
71
        'subscribe'   => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'post'],
72
        'check'       => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'get'],
73
        'update'      => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'put'],
74
        'unsubscribe' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'delete'],
75
    ];
76
77
    /**
78
     * @var array<string, array{format: string, options: array{}|array<string>, method: string}>
79
     */
80
    public static array $userParameters = [
81
        'dependencies'             => ['format' => 'github/:login/dependencies', 'options' => ['login'], 'method' => 'get'],
82
        'package_contributions'    => ['format' => 'github/:login/project-contributions', 'options' => ['login'], 'method' => 'get'],
83
        'packages'                 => ['format' => 'github/:login/projects', 'options' => ['login'], 'method' => 'get'],
84
        'repositories'             => ['format' => 'github/:login/repositories', 'options' => ['login'], 'method' => 'get'],
85
        'repository_contributions' => ['format' => 'github/:login/repository-contributions', 'options' => ['login'], 'method' => 'get'],
86
        'subscriptions'            => ['format' => 'subscriptions', 'options' => [], 'method' => 'get'],
87
        'user'                     => ['format' => 'github/:login', 'options' => ['login'], 'method' => 'get'],
88
    ];
89
90
    /**
91
     * @param array<array-key, int|string> $options
92
     *
93
     * @return array{0: string, 1: string}
94
     *
95
     * @throws InvalidEndpointException
96
     * @throws InvalidEndpointOptionsException
97
     */
98 35
    public static function endpointParameters(string $endpoint, string $subset, array $options): array
99
    {
100 35
        $parameters = match($endpoint) {
101 35
            'project'      => Utils::$projectParameters[$subset] ?? null,
102 35
            'repository'   => Utils::$repositoryParameters[$subset] ?? null,
103 35
            'user'         => Utils::$userParameters[$subset] ?? null,
104 35
            'subscription' => Utils::$subscriptionParameters[$subset] ?? null,
105 35
            default        => null
106 35
        };
107
108 35
        if ($parameters === null) {
109 4
            throw new InvalidEndpointException('Invalid endpoint subset specified.');
110
        }
111
112 31
        foreach ($parameters['options'] as $endpointOption) {
113 30
            if (!isset($options[$endpointOption])) {
114 4
                throw new InvalidEndpointOptionsException(
115 4
                    '$options has not specified all required parameters. Parameters needed: ' . implode(', ', $parameters['options'])
116 4
                );
117
            }
118
        }
119
120 27
        $parameters['format'] = Utils::processEndpointFormat($parameters['format'], $options);
121
122 27
        return [$parameters['format'], $parameters['method']];
123
    }
124
125 35
    public static function normalizeEndpoint(string | null $endpoint, string $apiUrl): string
126
    {
127 35
        $endpoint = ltrim($endpoint ?? '', '/');
128
129 35
        if (!str_ends_with($apiUrl, '/')) {
130 2
            $endpoint = '/' . $endpoint;
131
        }
132
133 35
        return $endpoint;
134
    }
135
136 38
    public static function normalizeMethod(string $method): string
137
    {
138 38
        $method = strtoupper($method);
139
140
        // Check for a valid method
141 38
        if (!\in_array($method, ['GET', 'DELETE', 'POST', 'PUT',], true)) {
142 3
            $method = 'GET';
143
        }
144
145 38
        return $method;
146
    }
147
148 3
    public static function raw(ResponseInterface $response): string
149
    {
150 3
        return $response->getBody()->getContents();
151
    }
152
153
    /**
154
     * @param array<array-key, int|string> $options
155
     *
156
     * @return array<array-key, int|string>
157
     */
158 2
    public static function searchAdditionalParams(array $options): array
159
    {
160 2
        $additionalParams = [];
161
162 2
        foreach (['languages', 'licenses', 'keywords', 'platforms'] as $option) {
163 2
            if (isset($options[$option])) {
164 2
                $additionalParams[$option] = $options[$option];
165
            }
166
        }
167
168 2
        return $additionalParams;
169
    }
170
171 2
    public static function searchVerifySortOption(string $sort): string
172
    {
173
        /** @var array<int, string> $sortOptions */
174 2
        static $sortOptions = [
175 2
            'rank', 'stars', 'dependents_count',
176 2
            'dependent_repos_count', 'latest_release_published_at',
177 2
            'contributions_count', 'created_at',
178 2
        ];
179
180 2
        if (!\in_array($sort, $sortOptions, true)) {
181 1
            return 'rank';
182
        }
183
184 1
        return $sort;
185
    }
186
187
    /**
188
     * @return array<mixed>
189
     *
190
     * @throws JsonException
191
     */
192 1
    public static function toArray(ResponseInterface $response): array
193
    {
194
        /** @var array<mixed> $json * */
195 1
        $json = json_decode(Utils::raw($response), true, flags: JSON_THROW_ON_ERROR);
196
197 1
        return $json;
198
    }
199
200
    /**
201
     * @throws JsonException
202
     */
203 1
    public static function toObject(ResponseInterface $response): stdClass
204
    {
205
        /** @var stdClass $json * */
206 1
        $json = json_decode(Utils::raw($response), false, flags: JSON_THROW_ON_ERROR);
207
208 1
        return $json;
209
    }
210
211 42
    public static function validateCachePath(?string $cachePath = null): ?string
212
    {
213 42
        if ($cachePath === null) {
214 1
            return null;
215
        }
216
217 41
        if (!is_dir($cachePath) || !is_writable($cachePath)) {
218 1
            return null;
219
        }
220
221 40
        return $cachePath;
222
    }
223
224
    /**
225
     * @param array<array-key, int|string> $options
226
     *
227
     * @return array<array-key, int|string>
228
     */
229 23
    public static function validatePagination(array $options): array
230
    {
231 23
        if (!isset($options['page'], $options['per_page'])) {
232 20
            $options['page']     = self::MIN_PAGE_ALLOWED;
233 20
            $options['per_page'] = self::MIN_PERPAGE_ALLOWED;
234
        } else {
235 3
            $options['page'] = \intval($options['page']);
236 3
            $options['page'] = max(self::MIN_PAGE_ALLOWED, $options['page']);
237
238 3
            $options['per_page'] = \intval($options['per_page']);
239 3
            $options['per_page'] = max(self::MIN_PERPAGE_ALLOWED, $options['per_page']);
240 3
            $options['per_page'] = min(self::MAX_PERPAGE_ALLOWED, $options['per_page']);
241
        }
242
243 23
        return $options;
244
    }
245
246
    /**
247
     * Each endpoint class will have a 'subset' of endpoints that fall under it. This
248
     * function handles returning a formatted endpoint for the Client.
249
     *
250
     * @param array<array-key, int|string> $options
251
     */
252 27
    private static function processEndpointFormat(string $format, array $options): string
253
    {
254 27
        if (str_contains($format, ':') === false) {
255 3
            return $format;
256
        }
257
258 24
        foreach ($options as $key => $val) {
259 24
            if (\in_array($key, ['page', 'per_page'], true)) {
260 3
                continue;
261
            }
262
263
            /** @var string $val * */
264 24
            $format = str_replace(':' . $key, $val, $format);
265
        }
266
267 24
        return $format;
268
    }
269
}
270