From dc278085de48e3b6d3e1e923a017dfd7dcc461d3 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 13 Jan 2026 16:22:02 -0600 Subject: [PATCH] feat(domain): add DynamicSport model for CloudKit-defined sports Struct representing sports synced from CloudKit. Conforms to AnySport protocol for interchangeable use with Sport enum in UI and planning. Co-Authored-By: Claude Opus 4.5 --- .../Core/Models/Domain/DynamicSport.swift | 50 ++++++++ .../Domain/DynamicSportTests.swift | 113 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 SportsTime/Core/Models/Domain/DynamicSport.swift create mode 100644 SportsTimeTests/Domain/DynamicSportTests.swift diff --git a/SportsTime/Core/Models/Domain/DynamicSport.swift b/SportsTime/Core/Models/Domain/DynamicSport.swift new file mode 100644 index 0000000..14e2c5f --- /dev/null +++ b/SportsTime/Core/Models/Domain/DynamicSport.swift @@ -0,0 +1,50 @@ +// +// DynamicSport.swift +// SportsTime +// +// Domain model for CloudKit-defined sports. +// + +import SwiftUI + +/// A sport defined via CloudKit, as opposed to the hardcoded Sport enum. +/// Conforms to AnySport for interchangeable use with Sport enum. +nonisolated struct DynamicSport: Identifiable, Hashable, Codable, Sendable { + let id: String + let abbreviation: String + let displayName: String + let iconName: String + let colorHex: String + let seasonStartMonth: Int + let seasonEndMonth: Int + + init( + id: String, + abbreviation: String, + displayName: String, + iconName: String, + colorHex: String, + seasonStartMonth: Int, + seasonEndMonth: Int + ) { + self.id = id + self.abbreviation = abbreviation + self.displayName = displayName + self.iconName = iconName + self.colorHex = colorHex + self.seasonStartMonth = seasonStartMonth + self.seasonEndMonth = seasonEndMonth + } +} + +// MARK: - AnySport Conformance + +extension DynamicSport: AnySport { + var sportId: String { id } + + var color: Color { Color(hex: colorHex) } + + var seasonMonths: (start: Int, end: Int) { + (seasonStartMonth, seasonEndMonth) + } +} diff --git a/SportsTimeTests/Domain/DynamicSportTests.swift b/SportsTimeTests/Domain/DynamicSportTests.swift new file mode 100644 index 0000000..de5a8b0 --- /dev/null +++ b/SportsTimeTests/Domain/DynamicSportTests.swift @@ -0,0 +1,113 @@ +// +// DynamicSportTests.swift +// SportsTimeTests +// + +import Testing +import SwiftUI +@testable import SportsTime + +@Suite("DynamicSport") +struct DynamicSportTests { + + @Test("DynamicSport conforms to AnySport protocol") + func dynamicSportConformsToAnySport() { + let xfl = DynamicSport( + id: "xfl", + abbreviation: "XFL", + displayName: "XFL Football", + iconName: "football.fill", + colorHex: "#E31837", + seasonStartMonth: 2, + seasonEndMonth: 5 + ) + + let sport: any AnySport = xfl + #expect(sport.sportId == "xfl") + #expect(sport.displayName == "XFL Football") + #expect(sport.iconName == "football.fill") + } + + @Test("DynamicSport color parses from hex") + func dynamicSportColorParsesFromHex() { + let sport = DynamicSport( + id: "test", + abbreviation: "TST", + displayName: "Test Sport", + iconName: "star.fill", + colorHex: "#FF0000", + seasonStartMonth: 1, + seasonEndMonth: 12 + ) + + // Color should be red + #expect(sport.color != Color.clear) + } + + @Test("DynamicSport isInSeason works correctly") + func dynamicSportIsInSeason() { + let xfl = DynamicSport( + id: "xfl", + abbreviation: "XFL", + displayName: "XFL Football", + iconName: "football.fill", + colorHex: "#E31837", + seasonStartMonth: 2, + seasonEndMonth: 5 + ) + + // March is in XFL season (Feb-May) + let march = Calendar.current.date(from: DateComponents(year: 2026, month: 3, day: 15))! + #expect(xfl.isInSeason(for: march)) + + // September is not in XFL season + let september = Calendar.current.date(from: DateComponents(year: 2026, month: 9, day: 15))! + #expect(!xfl.isInSeason(for: september)) + } + + @Test("DynamicSport is Hashable") + func dynamicSportIsHashable() { + let sport1 = DynamicSport( + id: "xfl", + abbreviation: "XFL", + displayName: "XFL Football", + iconName: "football.fill", + colorHex: "#E31837", + seasonStartMonth: 2, + seasonEndMonth: 5 + ) + + let sport2 = DynamicSport( + id: "xfl", + abbreviation: "XFL", + displayName: "XFL Football", + iconName: "football.fill", + colorHex: "#E31837", + seasonStartMonth: 2, + seasonEndMonth: 5 + ) + + let set: Set = [sport1, sport2] + #expect(set.count == 1) + } + + @Test("DynamicSport is Codable") + func dynamicSportIsCodable() throws { + let original = DynamicSport( + id: "xfl", + abbreviation: "XFL", + displayName: "XFL Football", + iconName: "football.fill", + colorHex: "#E31837", + seasonStartMonth: 2, + seasonEndMonth: 5 + ) + + let encoded = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(DynamicSport.self, from: encoded) + + #expect(decoded.id == original.id) + #expect(decoded.abbreviation == original.abbreviation) + #expect(decoded.displayName == original.displayName) + } +}