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 <noreply@anthropic.com>
This commit is contained in:
50
SportsTime/Core/Models/Domain/DynamicSport.swift
Normal file
50
SportsTime/Core/Models/Domain/DynamicSport.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
113
SportsTimeTests/Domain/DynamicSportTests.swift
Normal file
113
SportsTimeTests/Domain/DynamicSportTests.swift
Normal file
@@ -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<DynamicSport> = [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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user