chore: initial domain and infrastructure layers

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-08-05 08:07:41 -04:00
parent 89b52e2286
commit 5893b9f99e
21 changed files with 774 additions and 5 deletions
@@ -0,0 +1,43 @@
import Foundation
public struct DailyForecast: Sendable, Hashable {
public let dewPoint: Double
public let humidity: Int
public let sunrise: Date
public let sunset: Date
public let surfacePressure: Double
public let temperature: Double
public let uvIndex: Double
public let visibility: Double
public let weatherCode: WeatherCode
public let windDirection: Int
public let windGusts: Double
public let windSpeed: Double
public init(dewPoint: Double,
humidity: Int,
sunrise: Date,
sunset: Date,
surfacePressure: Double,
temperature: Double,
uvIndex: Double,
visibility: Double,
weatherCode: WeatherCode,
windDirection: Int,
windGusts: Double,
windSpeed: Double
) {
self.dewPoint = dewPoint
self.humidity = humidity
self.sunrise = sunrise
self.sunset = sunset
self.surfacePressure = surfacePressure
self.temperature = temperature
self.uvIndex = uvIndex
self.visibility = visibility
self.weatherCode = weatherCode
self.windDirection = windDirection
self.windGusts = windGusts
self.windSpeed = windSpeed
}
}
@@ -0,0 +1,55 @@
import Foundation
public struct Forecast: Sendable, Hashable {
public let dewPoint: Double
public let futureForecasts: [DateOnly: DailyForecast]
public let humidity: Int
public let latitude: Double
public let longitude: Double
public let sunrise: Date
public let sunset: Date
public let surfacePressure: Double
public let temperature: Double
public let timeZone: TimeZone
public let uvIndex: Double
public let visibility: Double
public let weatherCode: WeatherCode
public let windDirection: Int
public let windGusts: Double
public let windSpeed: Double
public init(dewPoint: Double,
futureForecasts: [DateOnly: DailyForecast] = [:],
humidity: Int,
latitude: Double,
longitude: Double,
sunrise: Date,
sunset: Date,
surfacePressure: Double,
temperature: Double,
timeZone: TimeZone,
uvIndex: Double,
visibility: Double,
weatherCode: WeatherCode,
windDirection: Int,
windGusts: Double,
windSpeed: Double
) {
self.dewPoint = dewPoint
self.futureForecasts = futureForecasts
self.humidity = humidity
self.latitude = latitude
self.longitude = longitude
self.sunrise = sunrise
self.sunset = sunset
self.surfacePressure = surfacePressure
self.temperature = temperature
self.timeZone = timeZone
self.uvIndex = uvIndex
self.visibility = visibility
self.weatherCode = weatherCode
self.windDirection = windDirection
self.windGusts = windGusts
self.windSpeed = windSpeed
}
}
@@ -0,0 +1,63 @@
import Foundation
public enum WeatherCode: Sendable, Hashable, Decodable {
case clearSky
case mainlyClear
case partlyCloudy
case overcast
case fog
case drizzle
case rain
case freezingRain
case rainShowers
case snow
case snowShowers
case thunderstorm
case thunderstormWithHail
case unknown(code: Int)
public static func from(code: Int) -> WeatherCode {
switch code {
case 0: .clearSky
case 1: .mainlyClear
case 2: .partlyCloudy
case 3: .overcast
case 45, 48: .fog
case 51, 53, 55, 56, 57: .drizzle
case 61, 63, 65: .rain
case 66, 67: .freezingRain
case 71, 73, 75, 755: .snow
case 85, 86: .snowShowers
case 95: .thunderstorm
case 96, 99: .thunderstormWithHail
default: .unknown(code: code)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let code = try container.decode(Int.self)
self = .from(code: code)
}
}
public extension WeatherCode {
var code: Int {
switch self {
case .clearSky: 0
case .mainlyClear: 1
case .partlyCloudy: 2
case .overcast: 3
case .fog: 45
case .drizzle: 51
case .rain: 61
case .freezingRain: 66
case .rainShowers: 80
case .snow: 71
case .snowShowers: 85
case .thunderstorm: 95
case .thunderstormWithHail: 96
case .unknown(let code): code
}
}
}
@@ -0,0 +1,9 @@
public protocol ForecastRepository: Sendable {
func get(latitude: Double, longitude: Double, forceRefresh: Bool) async throws -> Forecast?
}
public extension ForecastRepository {
func get(latitude: Double, longitude: Double, forceRefresh: Bool = false) async throws -> Forecast? {
try await get(latitude: latitude, longitude: longitude, forceRefresh: forceRefresh)
}
}
@@ -1,2 +0,0 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
@@ -0,0 +1,47 @@
import Foundation
public struct DateOnly: Sendable, Hashable, Comparable, Codable {
public let year: Int
public let month: Int
public let day: Int
public init(year: Int, month: Int, day: Int) {
self.year = year
self.month = month
self.day = day
}
public init(date: Date, timeZone: TimeZone) {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
let components = calendar.dateComponents([.year, .month, .day], from: date)
self.init(year: components.year!, month: components.month!, day: components.day!)
}
public static func < (lhs: DateOnly, rhs: DateOnly) -> Bool {
(lhs.year, lhs.month, lhs.day) < (rhs.year, rhs.month, rhs.day)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
let components = string.split(separator: "-")
guard components.count == 3,
let year = Int(components[0]),
let month = Int(components[1]),
let day = Int(components[2]) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid date format: \(string)")
}
self.init(year: year, month: month, day: day)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(String(format: "%04d-%02d-%02d", year, month, day))
}
}
@@ -6,3 +6,11 @@ import Testing
// Swift Testing Documentation
// https://developer.apple.com/documentation/testing
}
@Test func weatherCodeRoundTripsKnownCode() async throws {
#expect(WeatherCode.from(code: 61).code == 61)
}
@Test func weatherCodePreservesUnknownCode() async throws {
#expect(WeatherCode.unknown(code: 12345).code == 12345)
}