Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Domain/Discipline/IFSCDiscipline.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,18 @@ enum IFSCDiscipline: string
case BOULDER = 'boulder';
case LEAD = 'lead';
case SPEED = 'speed';
case SPEED_RELAY = 'speed_relay';
case COMBINED = 'combined';

public function displayName(): string
{
return str_replace('_', ' ', $this->value);
}

public function calendarDiscipline(): string
{
return $this === self::SPEED_RELAY
? self::SPEED->value
: $this->value;
}
}
3 changes: 2 additions & 1 deletion src/Domain/Event/IFSCEventTagsRegex.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ enum IFSCEventTagsRegex: string
case MEN = '(men|male)';
case LEAD = 'lead';
case BOULDER = 'boulder(ing)?';
case SPEED = 'speed';
case SPEED_RELAY = 'speed[\s_-]*relay';
case SPEED = 'speed(?![\s_-]*relay)';
case COMBINED = 'combined';
case PARACLIMBING = 'para[\s-]?climbing';
case QUALIFICATION = 'qualifications?';
Expand Down
28 changes: 26 additions & 2 deletions src/Domain/Event/IFSCEventsFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,12 @@ private function parseDisciplines(array $eventData): array
throw new RuntimeException(sprintf("Invalid discipline value for %s", $this->eventReference($eventData)));
}

$parsed[$discipline] = IFSCDiscipline::from($discipline);
$parsedDiscipline = $this->parseDiscipline($discipline);
if (!$parsedDiscipline) {
continue;
}

$parsed[$parsedDiscipline->value] = $parsedDiscipline;
}

return array_values($parsed);
Expand Down Expand Up @@ -354,7 +359,9 @@ private function parseEventRound(array $roundPayload, array $eventData): IFSCEve
$disciplineKey = isset($roundPayload['discipline']) ? 'discipline' : 'kind';
$kindKey = isset($roundPayload['discipline']) ? 'kind' : 'name';

$discipline = $this->requiredString($roundPayload, $disciplineKey);
$discipline = $this->parseRoundDiscipline(
$this->requiredString($roundPayload, $disciplineKey),
);
$kind = $this->parseRoundKind(
roundKind: $this->requiredString($roundPayload, $kindKey),
eventData: $eventData,
Expand All @@ -367,6 +374,21 @@ private function parseEventRound(array $roundPayload, array $eventData): IFSCEve
);
}

private function parseRoundDiscipline(string $discipline): string
{
return $this->parseDiscipline($discipline)?->value ?? $this->normalizeDisciplineValue($discipline);
}

private function parseDiscipline(string $discipline): ?IFSCDiscipline
{
return IFSCDiscipline::tryFrom($this->normalizeDisciplineValue($discipline));
}

private function normalizeDisciplineValue(string $discipline): string
{
return strtolower(str_replace([' ', '-'], '_', trim($discipline)));
}

/** @param array<string,mixed> $eventData */
private function parseRoundKind(string $roundKind, array $eventData): IFSCRoundKind
{
Expand Down Expand Up @@ -416,6 +438,8 @@ private function normalizeRoundName(IFSCEventRound $round): string
subject: $round->discipline,
);

$discipline = str_replace('_', ' ', $discipline);

return sprintf("%s's %s %s", $round->category, $discipline, $round->kind->value) |> ucwords(...);
}

Expand Down
6 changes: 3 additions & 3 deletions src/Domain/Round/IFSCRoundNameNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private function upperCaseWords(string $name): string
*/
private function disciplineNames(array $disciplines): array
{
return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $disciplines);
return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->displayName(), $disciplines);
}

private function buildCategories(IFSCParsedTags $tags): string
Expand All @@ -66,9 +66,9 @@ private function buildDisciplines(array $disciplines): string
$lastDiscipline = array_pop($disciplines);
$disciplines = $this->disciplineNames($disciplines);

return implode(', ', $disciplines) . ' & ' . $lastDiscipline->value;
return implode(', ', $disciplines) . ' & ' . $lastDiscipline->displayName();
} else {
return array_first($disciplines)->value;
return array_first($disciplines)->displayName();
}
}
}
3 changes: 2 additions & 1 deletion src/Domain/Tags/IFSCParsedTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
private const array DISCIPLINES = [
IFSCDiscipline::BOULDER->value => Tag::BOULDER,
IFSCDiscipline::LEAD->value => Tag::LEAD,
IFSCDiscipline::SPEED_RELAY->value => Tag::SPEED_RELAY,
IFSCDiscipline::SPEED->value => Tag::SPEED,
IFSCDiscipline::COMBINED->value => Tag::COMBINED,
];
Expand Down Expand Up @@ -51,7 +52,7 @@ public function getDisciplines(): array

foreach (self::DISCIPLINES as $name => $tag) {
if ($this->hasTag($tag)) {
if ($tag === IFSCDiscipline::COMBINED) {
if ($tag === Tag::COMBINED) {
$disciplines[] = IFSCDiscipline::BOULDER;
$disciplines[] = IFSCDiscipline::LEAD;
} else {
Expand Down
1 change: 1 addition & 0 deletions src/Domain/YouTube/YouTubeMatchScorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
private const array DISCIPLINE_TAGS = [
Tag::BOULDER,
Tag::LEAD,
Tag::SPEED_RELAY,
Tag::SPEED,
Tag::COMBINED,
];
Expand Down
20 changes: 18 additions & 2 deletions src/Infrastructure/Calendar/JsonCalendar.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
'summary' => $event->ticketsSummary ?? '',
'purchase_url' => $event->ticketsPurchaseUrl ?? ''
],
'disciplines' => $event->disciplines,
'disciplines' => $this->buildEventDisciplines($event),
'starts_at' => $this->formatDate($event->startsAt),
'ends_at' => $this->formatDate($event->endsAt),
'timezone' => $event->timeZone->getName(),
Expand Down Expand Up @@ -112,10 +112,26 @@ private function buildUrl(IFSCEvent $event): string
return sprintf(self::WORLD_CLIMBING_INFO_URL, $event->slug);
}

/** @return string[] */
private function buildEventDisciplines(IFSCEvent $event): array
{
$disciplines = array_map(
static fn (IFSCDiscipline $discipline): string => $discipline->calendarDiscipline(),
$event->disciplines,
);

return array_values(array_unique($disciplines));
}

/** @return string[] */
private function buildDisciplines(IFSCRound $round): array
{
return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $round->disciplines->all());
$disciplines = array_map(
static fn (IFSCDiscipline $discipline): string => $discipline->calendarDiscipline(),
$round->disciplines->all(),
);

return array_values(array_unique($disciplines));
}

/** @return string[] */
Expand Down
183 changes: 183 additions & 0 deletions tests/unit/Domain/Event/IFSCEventsFetcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php declare(strict_types=1);

/**
* @license http://opensource.org/licenses/mit-license.php MIT
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart <nico@ifsc.stream>
*/
namespace SportClimbing\IfscCalendar\tests\Domain\Event;

use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthlete;
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteProviderInterface;
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteService;
use SportClimbing\IfscCalendar\Domain\Calendar\SiteURLBuilder;
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDiscipline;
use SportClimbing\IfscCalendar\Domain\DomainEvent\Event;
use SportClimbing\IfscCalendar\Domain\DomainEvent\EventDispatcherInterface;
use SportClimbing\IfscCalendar\Domain\Event\IFSCEventFactory;
use SportClimbing\IfscCalendar\Domain\Event\IFSCEventSlug;
use SportClimbing\IfscCalendar\Domain\Event\IFSCEventsFetcher;
use SportClimbing\IfscCalendar\Domain\Event\Info\IFSCEventInfo;
use SportClimbing\IfscCalendar\Domain\Ranking\IFSCAthleteRankingCalculator;
use SportClimbing\IfscCalendar\Domain\Round\IFSCAverageRoundDuration;
use SportClimbing\IfscCalendar\Domain\Round\IFSCAverageRoundDurationLookupKey;
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundFactory;
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer;
use SportClimbing\IfscCalendar\Domain\Round\IFSCSameStreamRoundsMerger;
use SportClimbing\IfscCalendar\Domain\Schedule\IFSCScheduleFactory;
use SportClimbing\IfscCalendar\Domain\Season\IFSCSeasonYear;
use SportClimbing\IfscCalendar\Domain\StartList\IFSCStartListGenerator;
use SportClimbing\IfscCalendar\Domain\StartList\IFSCStartListProviderInterface;
use SportClimbing\IfscCalendar\Domain\Stream\LiveStream;
use SportClimbing\IfscCalendar\Domain\Tags\IFSCTagsParser;
use SportClimbing\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinderInterface;
use Override;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;

final class IFSCEventsFetcherTest extends TestCase
{
#[Test] public function speed_relay_is_parsed_as_a_dedicated_discipline(): void
{
$schedulePath = $this->createSchedulePath([
[
'event_id' => 9001,
'event_name' => 'IFSC Relay Test Event 2026',
'league_name' => 'World Cups and World Championships',
'local_start_date' => '2026-05-26',
'local_end_date' => '2026-05-27',
'timezone' => 'Europe/Madrid',
'location' => 'Madrid',
'country' => 'ES',
'disciplines' => ['speed_relay'],
'categories' => ['men'],
],
]);

try {
$events = $this->createFetcher()->fetchEventsForSeason(
season: IFSCSeasonYear::SEASON_2026,
selectedLeagues: ['World Cups and World Championships'],
schedulePath: $schedulePath,
);
} finally {
@unlink($schedulePath);
}

$this->assertCount(1, $events);
$this->assertSame([IFSCDiscipline::SPEED_RELAY], $events[0]->disciplines);
}

#[Test] public function speed_relay_round_payloads_produce_speed_relay_round_names(): void
{
$schedulePath = $this->createSchedulePath([
[
'event_id' => 9002,
'event_name' => 'IFSC Relay Round Payload Test 2026',
'league_name' => 'World Cups and World Championships',
'local_start_date' => '2026-05-26',
'local_end_date' => '2026-05-27',
'timezone' => 'Europe/Madrid',
'location' => 'Madrid',
'country' => 'ES',
'disciplines' => ['speed_relay'],
'categories' => [[
'rounds' => [[
'discipline' => 'speed_relay',
'kind' => 'qualification',
'category' => 'men',
]],
]],
],
]);

try {
$events = $this->createFetcher()->fetchEventsForSeason(
season: IFSCSeasonYear::SEASON_2026,
selectedLeagues: ['World Cups and World Championships'],
schedulePath: $schedulePath,
);
} finally {
@unlink($schedulePath);
}

$this->assertCount(1, $events);
$this->assertCount(1, $events[0]->rounds);
$this->assertSame("Men's Speed Relay Qualification", $events[0]->rounds[0]->name);
}

private function createFetcher(): IFSCEventsFetcher
{
$tagsParser = new IFSCTagsParser();
$roundNameNormalizer = new IFSCRoundNameNormalizer();

$roundFactory = new IFSCRoundFactory(
tagsParser: $tagsParser,
liveStreamFinder: new class () implements YouTubeLiveStreamFinderInterface {
#[Override] public function findLiveStream(
IFSCEventInfo $event,
string $roundName,
): LiveStream {
return new LiveStream();
}
},
averageRoundDuration: new IFSCAverageRoundDuration(
new IFSCAverageRoundDurationLookupKey(),
),
);

$eventFactory = new IFSCEventFactory(
siteURLBuilder: new SiteURLBuilder('https://ifsc.stream/{season}/{event_id}/{slug}'),
startListGenerator: new IFSCStartListGenerator(
startListProvider: new class () implements IFSCStartListProviderInterface {
#[Override] public function fetchStartListForEvent(int $eventId): array
{
return [];
}
},
athleteService: new IFSCAthleteService(
athleteProvider: new class () implements IFSCAthleteProviderInterface {
#[Override] public function fetchAthlete(int $athleteId): IFSCAthlete
{
throw new RuntimeException('Athletes are not expected in this test');
}
},
),
rankingCalculator: new IFSCAthleteRankingCalculator(),
),
sameStreamRoundsMerger: new IFSCSameStreamRoundsMerger(
tagsParser: $tagsParser,
nameNormalizer: $roundNameNormalizer,
),
);

return new IFSCEventsFetcher(
eventFactory: $eventFactory,
roundFactory: $roundFactory,
scheduleFactory: new IFSCScheduleFactory($tagsParser, $roundNameNormalizer),
eventDispatcher: new class () implements EventDispatcherInterface {
#[Override] public function dispatch(Event $event): void
{
}
},
eventSlug: new IFSCEventSlug(),
);
}

/** @param array<int,array<string,mixed>> $events */
private function createSchedulePath(array $events): string
{
$schedulePath = tempnam(sys_get_temp_dir(), 'ifsc-events-test-');
if ($schedulePath === false) {
throw new RuntimeException('Unable to create temporary schedule file');
}

$json = json_encode(['events' => $events], JSON_THROW_ON_ERROR);
if (file_put_contents($schedulePath, $json) === false) {
throw new RuntimeException('Unable to write temporary schedule file');
}

return $schedulePath;
}
}
2 changes: 2 additions & 0 deletions tests/unit/Domain/Round/IFSCRoundNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static function event_names(): array
["Speed Warm Up Zone Open", "Speed Warm Up Zone Open"],
["Speed Practice", "Speed Practice"],
["Speed Qualification", "Men's & Women's Speed Qualification"],
["Speed Relay Qualification", "Men's & Women's Speed Relay Qualification"],
["Isolation Opens", "Isolation Opens"],
["Isolation Closes", "Isolation Closes"],
["Men’s & Women’s Lead Semi Finals", "Men's & Women's Lead Semi-Final"],
Expand All @@ -68,6 +69,7 @@ public static function event_names(): array
["Men’s Speed warm-up", "Men’s Speed Warm-Up"],
["Men’s Speed practice", "Men’s Speed Practice"],
["Men’s Speed Qualification", "Men's Speed Qualification"],
["Men’s Speed Relay Final", "Men's Speed Relay Final"],
["Women’s Boulder final", "Women's Boulder Final"],
["Men’s Speed Final", "Men's Speed Final"],
["Women’s Boulder Semi-Final", "Women's Boulder Semi-Final"],
Expand Down
Loading