• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visavi / rzd-api / 30570164785

30 Jul 2026 06:23PM UTC coverage: 99.907% (-0.09%) from 100.0%
30570164785

push

github

visavi
Поиск с пересадками
  Если нужен текст с телом:
  Поиск с пересадками
  Цепочки рейсов там, где прямых поездов нет: ожидание между рейсами и
  переезды между вокзалами. Города задаются идентификаторами узлов сайта,
  запрос собирается из подсказок станций.

  Поиску нужна кука LANG_SITE, поэтому она уходит со всеми запросами.

246 of 247 new or added lines in 13 files covered. (99.6%)

1076 of 1077 relevant lines covered (99.91%)

15.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.15
/src/Model/Model.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Rzd\Model;
6

7
use DateTimeImmutable;
8
use Exception;
9

10
/**
11
 * Основа моделей ответа
12
 *
13
 * Сайт отдает у одного вагона больше восьмидесяти полей, у поезда больше
14
 * семидесяти. Модели описывают то, что нужно на практике, а полный ответ
15
 * остается доступен через raw, поэтому редкое поле не требует правки
16
 * библиотеки и не теряется при разборе
17
 */
18
abstract readonly class Model
19
{
20
    /**
21
     * @param array<string, mixed> $raw Ответ сайта без изменений
22
     */
23
    protected function __construct(public array $raw)
128✔
24
    {
25
    }
128✔
26

27
    /**
28
     * Значение поля из полного ответа
29
     *
30
     * Нужен для полей, не вошедших в модель
31
     */
32
    public function get(string $key, mixed $default = null): mixed
1✔
33
    {
34
        return $this->raw[$key] ?? $default;
1✔
35
    }
36

37
    /**
38
     * @param array<string, mixed> $data
39
     */
40
    protected static function str(array $data, string $key): ?string
100✔
41
    {
42
        $value = $data[$key] ?? null;
100✔
43

44
        // Сайт присылает пустую строку вместо отсутствующего значения
45
        return is_string($value) && $value !== '' ? $value : null;
100✔
46
    }
47

48
    /**
49
     * @param array<string, mixed> $data
50
     */
51
    protected static function int(array $data, string $key): ?int
90✔
52
    {
53
        $value = $data[$key] ?? null;
90✔
54

55
        return is_numeric($value) ? (int) $value : null;
90✔
56
    }
57

58
    /**
59
     * @param array<string, mixed> $data
60
     */
61
    protected static function float(array $data, string $key): ?float
46✔
62
    {
63
        $value = $data[$key] ?? null;
46✔
64

65
        return is_numeric($value) ? (float) $value : null;
46✔
66
    }
67

68
    /**
69
     * @param array<string, mixed> $data
70
     */
71
    protected static function bool(array $data, string $key): bool
55✔
72
    {
73
        return (bool) ($data[$key] ?? false);
55✔
74
    }
75

76
    /**
77
     * Список строк, пустой если поля нет
78
     *
79
     * @param array<string, mixed> $data
80
     *
81
     * @return list<string>
82
     */
83
    protected static function strings(array $data, string $key): array
53✔
84
    {
85
        $value = $data[$key] ?? [];
53✔
86

87
        return is_array($value) ? array_values(array_filter($value, 'is_string')) : [];
53✔
88
    }
89

90
    /**
91
     * Дата из формата сайта, 2026-08-01T00:20:00
92
     *
93
     * @param array<string, mixed> $data
94
     */
95
    protected static function date(array $data, string $key): ?DateTimeImmutable
83✔
96
    {
97
        $value = $data[$key] ?? null;
83✔
98

99
        if (! is_string($value) || $value === '') {
83✔
100
            return null;
45✔
101
        }
102

103
        try {
104
            return new DateTimeImmutable($value);
58✔
105
        } catch (Exception) {
1✔
106
            // Формат даты у сайта единый, но ломать разбор ответа из-за него не стоит
107
            return null;
1✔
108
        }
109
    }
110

111
    /**
112
     * Коды из списка справочных объектов вида {"key": …, "provider_code": …}
113
     *
114
     * Поиск с пересадками ссылается на свои справочники по ключу, но рядом
115
     * кладет код перевозчика, понятный без дополнительного запроса
116
     *
117
     * @param array<string, mixed> $data
118
     *
119
     * @return list<string>
120
     */
121
    protected static function codes(array $data, string $key): array
18✔
122
    {
123
        $items = $data[$key] ?? [];
18✔
124

125
        if (! is_array($items)) {
18✔
NEW
126
            return [];
×
127
        }
128

129
        return array_values(array_filter(array_map(
18✔
130
            static fn(mixed $item): ?string => is_array($item) ? self::str($item, 'provider_code') : null,
18✔
131
            $items,
18✔
132
        )));
18✔
133
    }
134

135
    /**
136
     * Цена в рублях из вложенного объекта с копейками
137
     *
138
     * Поиск с пересадками отдает деньги как {"kopecks": "553410"}, причем
139
     * строкой: суммы не помещаются в целое число у части языков
140
     *
141
     * @param array<string, mixed> $data
142
     */
143
    protected static function money(array $data, string $key): ?float
34✔
144
    {
145
        $value = $data[$key] ?? null;
34✔
146

147
        if (! is_array($value) || ! is_numeric($value['kopecks'] ?? null)) {
34✔
148
            return null;
20✔
149
        }
150

151
        return (int) $value['kopecks'] / 100;
14✔
152
    }
153

154
    /**
155
     * Длительность в секундах из формата Go, например 807.024s
156
     *
157
     * @param array<string, mixed> $data
158
     */
159
    protected static function seconds(array $data, string $key): ?int
24✔
160
    {
161
        $value = $data[$key] ?? null;
24✔
162

163
        if (! is_string($value) || ! preg_match('/^(\d+(?:\.\d+)?)s$/', $value, $match)) {
24✔
164
            return null;
9✔
165
        }
166

167
        return (int) round((float) $match[1]);
15✔
168
    }
169

170
    /**
171
     * Одна вложенная модель, null если поля нет
172
     *
173
     * @template T of self
174
     *
175
     * @param array<string, mixed> $data
176
     * @param class-string<T>      $model
177
     *
178
     * @return T|null
179
     */
180
    protected static function one(array $data, string $key, string $model): ?self
28✔
181
    {
182
        $value = $data[$key] ?? null;
28✔
183

184
        return is_array($value) ? $model::fromArray($value) : null;
28✔
185
    }
186

187
    /**
188
     * Список вложенных моделей
189
     *
190
     * @template T of self
191
     *
192
     * @param array<string, mixed> $data
193
     * @param class-string<T>      $model
194
     *
195
     * @return list<T>
196
     */
197
    protected static function each(array $data, string $key, string $model): array
75✔
198
    {
199
        $items = $data[$key] ?? [];
75✔
200

201
        if (! is_array($items)) {
75✔
202
            return [];
1✔
203
        }
204

205
        return array_values(array_map(
74✔
206
            static fn(array $item): self => $model::fromArray($item),
74✔
207
            array_filter($items, 'is_array'),
74✔
208
        ));
74✔
209
    }
210

211
    /**
212
     * @param array<string, mixed> $data
213
     */
214
    abstract public static function fromArray(array $data): static;
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc