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,41 @@
import Foundation
import Testing
@testable import TempestasInfrastructure
@Suite struct OpenMeteoDateDecodingTests {
private static let json = """
{
"latitude": 35.7,
"longitude": 139.7,
"timezone": "Asia/Tokyo",
"daily": {
"time": ["2024-06-01"],
"dew_point_2m_max": [15.0],
"relative_humidity_2m_min": [50.0],
"sunrise": ["2024-06-01T04:25"],
"sunset": ["2024-06-01T18:52"],
"surface_pressure_min": [1008.0],
"apparent_temperature_max": [26.0],
"uv_index_max": [7.0],
"visibility_mean": [12000.0],
"weather_code": [0],
"wind_direction_10m_dominant": [90],
"wind_gusts_10m_min": [10.0],
"wind_speed_10m_min": [8.0]
}
}
"""
@Test func decodesTruncatedIso8601SunriseInRequestedTimeZone() throws {
let timeZone = TimeZone(identifier: "Asia/Tokyo")!
let decoder = OpenMeteoDateDecoding.decoder(timeZone: timeZone)
let contract = try decoder.decode(WeatherApiResponseContract.self, from: Data(Self.json.utf8))
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
let components = calendar.dateComponents([.hour, .minute], from: contract.items.sunriseTimes[0])
#expect(components.hour == 4)
#expect(components.minute == 25)
}
}
@@ -0,0 +1,66 @@
import Foundation
import SwiftData
import Testing
@testable import TempestasInfrastructure
@Suite struct WeatherResponseRecordMapperTests {
private static let json = """
{
"latitude": 45.5,
"longitude": -73.6,
"timezone": "America/Toronto",
"daily": {
"time": ["2026-08-05", "2026-08-06"],
"dew_point_2m_max": [12.3, 13.1],
"relative_humidity_2m_min": [55.4, 60.2],
"sunrise": ["2026-08-05T05:52", "2026-08-06T05:53"],
"sunset": ["2026-08-05T20:31", "2026-08-06T20:30"],
"surface_pressure_min": [1012.5, 1010.1],
"apparent_temperature_max": [24.5, 23.1],
"uv_index_max": [6.0, 5.5],
"visibility_mean": [10000.0, 9500.0],
"weather_code": [1, 61],
"wind_direction_10m_dominant": [180, 200],
"wind_gusts_10m_min": [15.0, 18.0],
"wind_speed_10m_min": [10.0, 12.0]
}
}
"""
private func makeContract() throws -> WeatherApiResponseContract {
let decoder = OpenMeteoDateDecoding.decoder(timeZone: TimeZone(identifier: "America/Toronto")!)
return try decoder.decode(WeatherApiResponseContract.self, from: Data(Self.json.utf8))
}
@Test func mapperProducesOneChildPerDay() throws {
let contract = try makeContract()
let record = WeatherResponseRecordMapper.map(contract)
#expect(record.dailyForecasts.count == 2)
#expect(record.latitude == 45.5)
#expect(record.timeZoneIdentifier == "America/Toronto")
#expect(record.dailyForecasts.allSatisfy { $0.response === record })
}
@Test func roundTripsThroughInMemoryModelContainer() throws {
let schema = Schema([WeatherResponseRecord.self, DailyForecastRecord.self])
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: [configuration])
let context = ModelContext(container)
let contract = try makeContract()
let record = WeatherResponseRecordMapper.map(contract)
context.insert(record)
try context.save()
let fetched = try context.fetch(FetchDescriptor<WeatherResponseRecord>())
#expect(fetched.count == 1)
#expect(fetched.first?.dailyForecasts.count == 2)
let firstDay = fetched.first?.dailyForecasts.sorted { $0.day < $1.day }.first
#expect(firstDay?.year == 2026)
#expect(firstDay?.month == 8)
#expect(firstDay?.day == 5)
#expect(firstDay?.weatherCode == 1)
}
}