From 57c032654b9309158753a7538cbb8e987ff9e715 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 16:43:49 +0200 Subject: [PATCH 01/17] Big rewrite incoming Signed-off-by: Henrik Panhans --- .vscode/launch.json | 15 - Package.resolved | 66 +- Package.swift | 19 +- .../Extensions/NSError+Extensions.swift | 17 - Sources/HPOpenWeather/Models/City.swift | 23 - .../Models/DailyTemperature.swift | 22 +- .../Models/Forecasts/CurrentWeather.swift | 79 +- .../Models/Forecasts/DailyForecast.swift | 72 +- ...{BasicWeather.swift => ForecastBase.swift} | 11 +- .../Models/Forecasts/HourlyForecast.swift | 65 +- .../Models/Forecasts/MinutelyForecast.swift | 17 + Sources/HPOpenWeather/Models/Moon.swift | 11 + .../HPOpenWeather/Models/Precipitation.swift | 4 +- .../HPOpenWeather/Models/Temperature.swift | 20 +- .../Weather+Language.swift} | 2 +- .../Weather+Units.swift} | 2 +- Sources/HPOpenWeather/Models/Weather.swift | 47 + .../HPOpenWeather/Models/WeatherAlert.swift | 20 +- Sources/HPOpenWeather/Models/Wind.swift | 9 +- Sources/HPOpenWeather/OpenWeather.swift | 68 +- Sources/HPOpenWeather/OpenWeatherError.swift | 8 + .../Requests/APINetworkRequest.swift | 30 - .../Requests/ExcludableField.swift | 9 + .../Requests/WeatherRequest+Combine.swift | 25 - .../WeatherRequest+ExcludableField.swift | 13 - .../Requests/WeatherRequest.swift | 60 +- .../Response/WeatherResponse.swift | 35 - .../HPOpenWeatherTests.swift | 172 +- .../Resources/2-5-test-response.json | 1487 ++++++++++++++ .../Resources/3-0-test-response.json | 1713 +++++++++++++++++ Tests/HPOpenWeatherTests/TestSecret.swift | 4 +- 31 files changed, 3627 insertions(+), 518 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 Sources/HPOpenWeather/Extensions/NSError+Extensions.swift delete mode 100644 Sources/HPOpenWeather/Models/City.swift rename Sources/HPOpenWeather/Models/Forecasts/{BasicWeather.swift => ForecastBase.swift} (76%) create mode 100644 Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift create mode 100644 Sources/HPOpenWeather/Models/Moon.swift rename Sources/HPOpenWeather/{Response/WeatherResponse+Language.swift => Models/Weather+Language.swift} (97%) rename Sources/HPOpenWeather/{Response/WeatherResponse+Units.swift => Models/Weather+Units.swift} (96%) create mode 100644 Sources/HPOpenWeather/Models/Weather.swift create mode 100644 Sources/HPOpenWeather/OpenWeatherError.swift delete mode 100644 Sources/HPOpenWeather/Requests/APINetworkRequest.swift create mode 100644 Sources/HPOpenWeather/Requests/ExcludableField.swift delete mode 100644 Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift delete mode 100644 Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift delete mode 100644 Sources/HPOpenWeather/Response/WeatherResponse.swift create mode 100644 Tests/HPOpenWeatherTests/Resources/2-5-test-response.json create mode 100644 Tests/HPOpenWeatherTests/Resources/3-0-test-response.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index b7e1bb4..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Test HPOpenWeather", - "program": "/Applications/Xcode.app/Contents/Developer/usr/bin/xctest", - "args": [ - ".build/debug/HPOpenWeatherPackageTests.xctest" - ], - "cwd": "${workspaceFolder:HPOpenWeather}", - "preLaunchTask": "swift: Build All" - } - ] -} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 0ca497e..b4e3224 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/henrik-dmg/HPNetwork", "state": { "branch": null, - "revision": "fba08b668713bad984ad4d76bf99b55e9192fcce", - "version": "3.1.2" + "revision": "6e1266a06692dab7df6d57faffaee59f2b46c4f5", + "version": "4.0.0" } }, { @@ -20,66 +20,12 @@ } }, { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", + "package": "swift-http-types", + "repositoryURL": "https://github.com/apple/swift-http-types.git", "state": { "branch": null, - "revision": "9f39744e025c7d377987f30b03770805dcb0bcd1", - "version": "1.1.4" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": "main", - "revision": "a3217706ad049ca058743003e065767773cc56cc", - "version": null - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": "main", - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": null - } - }, - { - "package": "swift-format", - "repositoryURL": "https://github.com/apple/swift-format", - "state": { - "branch": "main", - "revision": "e5875f32d37d0de760bd4ca3b988f42373866f96", - "version": null - } - }, - { - "package": "SwiftSyntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": "main", - "revision": "1e61cc3bd13c0f61d75e509994e00e64fecf8bf3", - "version": null - } - }, - { - "package": "swift-system", - "repositoryURL": "https://github.com/apple/swift-system.git", - "state": { - "branch": null, - "revision": "836bc4557b74fe6d2660218d56e3ce96aff76574", - "version": "1.1.1" - } - }, - { - "package": "swift-tools-support-core", - "repositoryURL": "https://github.com/apple/swift-tools-support-core.git", - "state": { - "branch": null, - "revision": "284a41800b7c5565512ec6ae21ee818aac1f84ac", - "version": "0.4.0" + "revision": "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version": "1.0.3" } } ] diff --git a/Package.swift b/Package.swift index f223b28..fe63d8f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "HPOpenWeather", platforms: [ - .iOS(.v13), .tvOS(.v13), .watchOS(.v7), .macOS(.v10_15) + .iOS(.v15), .tvOS(.v15), .watchOS(.v7), .macOS(.v12) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. @@ -17,21 +17,26 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "3.0.0"), + .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "4.0.0"), .package(url: "https://github.com/henrik-dmg/HPURLBuilder", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"), - .package(url: "https://github.com/apple/swift-format", branch: "main") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "HPOpenWeather", - dependencies: ["HPNetwork", "HPURLBuilder"] + dependencies: [ + .product(name: "HPNetworkMock", package: "HPNetwork"), + "HPURLBuilder" + ] ), .testTarget( name: "HPOpenWeatherTests", - dependencies: ["HPOpenWeather"] + dependencies: [ + "HPOpenWeather", + .product(name: "HPNetworkMock", package: "HPNetwork") + ], + resources: [.process("Resources")] ) ] ) diff --git a/Sources/HPOpenWeather/Extensions/NSError+Extensions.swift b/Sources/HPOpenWeather/Extensions/NSError+Extensions.swift deleted file mode 100644 index 3252af9..0000000 --- a/Sources/HPOpenWeather/Extensions/NSError+Extensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import HPNetwork - -extension NSError { - - convenience init(domain: String = "com.henrikpanhans.HPOpenWeather", code: Int, description: String) { - self.init( - domain: domain, - code: code, - userInfo: [NSLocalizedDescriptionKey: description] - ) - } - - static let noApiKey = NSError(code: 2, description: "API key was not provided") - static let timeMachineDate = NSError(code: 3, description: "TimeMachineRequest's date has to be at least 6 hours in the past") - -} diff --git a/Sources/HPOpenWeather/Models/City.swift b/Sources/HPOpenWeather/Models/City.swift deleted file mode 100644 index 09c2570..0000000 --- a/Sources/HPOpenWeather/Models/City.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import CoreLocation - -/// Type that holds information about the reqeuest's nearest city -public struct City: Codable, Equatable, Hashable, Identifiable { - - /// The ID assigned to the city - public let id: Int - /// The name of the city - public let name: String - /// The location of the city - public let location: CLLocationCoordinate2D - /// The country code of the city - public let countryCode: String - - enum CodingKeys: String, CodingKey { - case id - case name - case location = "coord" - case countryCode = "country" - } - -} diff --git a/Sources/HPOpenWeather/Models/DailyTemperature.swift b/Sources/HPOpenWeather/Models/DailyTemperature.swift index fa9a51a..143b453 100644 --- a/Sources/HPOpenWeather/Models/DailyTemperature.swift +++ b/Sources/HPOpenWeather/Models/DailyTemperature.swift @@ -3,6 +3,19 @@ import Foundation /// Type that holds information about daily temperature changes public struct DailyTemperature: Codable, Equatable, Hashable { + // MARK: - Nested Types + + enum CodingKeys: String, CodingKey { + case day + case night + case min + case max + case evening = "eve" + case morning = "morn" + } + + // MARK: - Properties + /// Day temperature. public let day: Double /// Night temperature. @@ -16,13 +29,4 @@ public struct DailyTemperature: Codable, Equatable, Hashable { /// Morning temperature. public let morning: Double - enum CodingKeys: String, CodingKey { - case day - case night - case min - case max - case evening = "eve" - case morning = "morn" - } - } diff --git a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift index 3638d71..be98c49 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift @@ -1,6 +1,6 @@ import Foundation -public struct CurrentWeather: BasicWeatherResponse, SunResponse { +public struct CurrentWeather: ForecastBase, SunForecast { // MARK: - Coding Keys @@ -19,7 +19,7 @@ public struct CurrentWeather: BasicWeatherResponse, SunResponse { case windSpeed = "wind_speed" case windGust = "wind_gust" case windDirection = "wind_deg" - case weatherArray = "weather" + case weather case sunrise case sunset } @@ -35,41 +35,44 @@ public struct CurrentWeather: BasicWeatherResponse, SunResponse { public let cloudCoverage: Double? public let rain: Precipitation? public let snow: Precipitation? - - // Temperature - - private let actualTemperature: Double - private let feelsLikeTemperature: Double - - public var temperature: Temperature { - Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) - } - - // Weather Conditions - - private let weatherArray: [WeatherCondition] - - public var condition: WeatherCondition? { - weatherArray.first - } - - // Wind - - private let windSpeed: Double? - private let windGust: Double? - private let windDirection: Double? - - public var wind: Wind { - Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - } - - // Sun - - private let sunrise: Date - private let sunset: Date - - public var sun: Sun { - Sun(sunset: sunset, sunrise: sunrise) + public let wind: Wind + public let sun: Sun + public let currentCondition: WeatherCondition + public let temperature: Temperature + + // MARK: - Init + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let weatherArray = try container.decode([WeatherCondition].self, forKey: .weather) + guard let currentCondition = weatherArray.first else { + throw OpenWeatherError.noCurrentConditionReturned + } + self.currentCondition = currentCondition + + self.snow = try container.decodeIfPresent(Precipitation.self, forKey: .snow) + self.rain = try container.decodeIfPresent(Precipitation.self, forKey: .rain) + self.timestamp = try container.decode(Date.self, forKey: .timestamp) + self.pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) + self.humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) + self.dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) + self.uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) + self.cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) + self.visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) + + let actualTemperature = try container.decode(Double.self, forKey: .actualTemperature) + let feelsLikeTemperature = try container.decode(Double.self, forKey: .feelsLikeTemperature) + self.temperature = Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) + + let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) + let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) + let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) + self.wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) + + let sunrise = try container.decode(Date.self, forKey: .sunrise) + let sunset = try container.decode(Date.self, forKey: .sunset) + self.sun = Sun(sunset: sunset, sunrise: sunrise) } - + } diff --git a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift index 1b9f063..cf25958 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift @@ -1,6 +1,6 @@ import Foundation -public struct DailyForecast: BasicWeatherResponse, SunResponse { +public struct DailyForecast: ForecastBase, SunForecast, MoonForecast { // MARK: - Coding Keys @@ -19,9 +19,11 @@ public struct DailyForecast: BasicWeatherResponse, SunResponse { case windSpeed = "wind_speed" case windGust = "wind_gust" case windDirection = "wind_deg" - case weatherArray = "weather" + case weather case sunrise case sunset + case moonrise + case moonset } // MARK: - Properties @@ -38,32 +40,46 @@ public struct DailyForecast: BasicWeatherResponse, SunResponse { public let uvIndex: Double? public let visibility: Double? public let cloudCoverage: Double? - - // Weather Conditions - - private let weatherArray: [WeatherCondition] - - public var condition: WeatherCondition? { - weatherArray.first - } - - // Wind - - private let windSpeed: Double? - private let windGust: Double? - private let windDirection: Double? - - public var wind: Wind { - Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - } - - // Sun - - private let sunrise: Date - private let sunset: Date - - public var sun: Sun { - Sun(sunset: sunset, sunrise: sunrise) + public let condition: WeatherCondition + public let sun: Sun + public let wind: Wind + public let moon: Moon + + // MARK: - Init + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let weatherArray = try container.decode([WeatherCondition].self, forKey: .weather) + guard let condition = weatherArray.first else { + throw OpenWeatherError.noCurrentConditionReturned + } + self.condition = condition + + self.timestamp = try container.decode(Date.self, forKey: .timestamp) + self.pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) + self.humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) + self.dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) + self.uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) + self.cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) + self.visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) + self.temperature = try container.decode(DailyTemperature.self, forKey: .temperature) + self.feelsLikeTemperature = try container.decode(DailyTemperature.self, forKey: .feelsLikeTemperature) + self.totalRain = try container.decodeIfPresent(Double.self, forKey: .totalRain) + self.totalSnow = try container.decodeIfPresent(Double.self, forKey: .totalSnow) + + let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) + let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) + let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) + self.wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) + + let sunrise = try container.decode(Date.self, forKey: .sunrise) + let sunset = try container.decode(Date.self, forKey: .sunset) + self.sun = Sun(sunset: sunset, sunrise: sunrise) + + let moonrise = try container.decode(Date.self, forKey: .moonrise) + let moonset = try container.decode(Date.self, forKey: .moonset) + self.moon = Moon(moonset: moonset, moonrise: moonrise) } } diff --git a/Sources/HPOpenWeather/Models/Forecasts/BasicWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift similarity index 76% rename from Sources/HPOpenWeather/Models/Forecasts/BasicWeather.swift rename to Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift index c6c5cf3..f058c29 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/BasicWeather.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift @@ -1,6 +1,6 @@ import Foundation -public protocol BasicWeatherResponse: Codable, Hashable { +public protocol ForecastBase: Decodable, Hashable { /// The timestamp when the data was collected var timestamp: Date { get } @@ -22,9 +22,16 @@ public protocol BasicWeatherResponse: Codable, Hashable { } -public protocol SunResponse: Codable, Hashable { +public protocol SunForecast: Decodable, Hashable { /// A container that holds information about sunset and sunrise timestamps var sun: Sun { get } } + +public protocol MoonForecast: Decodable, Hashable { + + /// A container that holds information about moonrise and moonset timestamps + var moon: Moon { get } + +} diff --git a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift index 8a2ffbb..95ff0bf 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift @@ -1,6 +1,6 @@ import Foundation -public struct HourlyForecast: BasicWeatherResponse { +public struct HourlyForecast: ForecastBase { // MARK: - Coding Keys @@ -19,7 +19,7 @@ public struct HourlyForecast: BasicWeatherResponse { case windSpeed = "wind_speed" case windGust = "wind_gust" case windDirection = "wind_deg" - case weatherArray = "weather" + case weather } // MARK: - Properties @@ -33,32 +33,39 @@ public struct HourlyForecast: BasicWeatherResponse { public let cloudCoverage: Double? public let rain: Precipitation? public let snow: Precipitation? - - // Temperature - - private let actualTemperature: Double - private let feelsLikeTemperature: Double - - public var temperature: Temperature { - Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) - } - - // Wind - - private let windSpeed: Double? - private let windGust: Double? - private let windDirection: Double? - - public var wind: Wind { - Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - } - - // Weather - - private let weatherArray: [WeatherCondition] - - public var weather: [WeatherCondition] { - weatherArray + public let temperature: Temperature + public let wind: Wind + public let condition: WeatherCondition + + // MARK: - Init + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let weatherArray = try container.decode([WeatherCondition].self, forKey: .weather) + guard let condition = weatherArray.first else { + throw OpenWeatherError.noCurrentConditionReturned + } + self.condition = condition + + self.snow = try container.decodeIfPresent(Precipitation.self, forKey: .snow) + self.rain = try container.decodeIfPresent(Precipitation.self, forKey: .rain) + self.timestamp = try container.decode(Date.self, forKey: .timestamp) + self.pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) + self.humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) + self.dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) + self.uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) + self.cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) + self.visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) + + let actualTemperature = try container.decode(Double.self, forKey: .actualTemperature) + let feelsLikeTemperature = try container.decode(Double.self, forKey: .feelsLikeTemperature) + self.temperature = Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) + + let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) + let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) + let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) + self.wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) } - + } diff --git a/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift new file mode 100644 index 0000000..9f3216b --- /dev/null +++ b/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct MinutelyForecast: Decodable, Equatable, Hashable { + + // MARK: - Nested Types + + enum CodingKeys: String, CodingKey { + case timestamp = "dt" + case precipitation + } + + // MARK: - Properties + + public let timestamp: Date + public let precipitation: Double + +} diff --git a/Sources/HPOpenWeather/Models/Moon.swift b/Sources/HPOpenWeather/Models/Moon.swift new file mode 100644 index 0000000..c378e8b --- /dev/null +++ b/Sources/HPOpenWeather/Models/Moon.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Type that holds information about moonset and moonrise times in UTC time +public struct Moon: Codable, Equatable, Hashable { + + /// Moonset time + public let moonset: Date + /// Moonrise time + public let moonrise: Date + +} diff --git a/Sources/HPOpenWeather/Models/Precipitation.swift b/Sources/HPOpenWeather/Models/Precipitation.swift index fcdccc7..a0f1c7a 100644 --- a/Sources/HPOpenWeather/Models/Precipitation.swift +++ b/Sources/HPOpenWeather/Models/Precipitation.swift @@ -15,7 +15,7 @@ public struct Precipitation: Codable, Equatable, Hashable { /// A convertible measurement of how much precipitation occured in the last hour if any public var lastHourMeasurement: Measurement? { - guard let lastHour = lastHour else { + guard let lastHour else { return nil } return Measurement(value: lastHour, unit: .millimeters) @@ -23,7 +23,7 @@ public struct Precipitation: Codable, Equatable, Hashable { /// A convertible measurement of how much precipitation occured in the last three hours if any public var lastThreeHoursMeasurement: Measurement? { - guard let lastThreeHours = lastThreeHours else { + guard let lastThreeHours else { return nil } return Measurement(value: lastThreeHours, unit: .millimeters) diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift index f2bc8fd..fed9b14 100644 --- a/Sources/HPOpenWeather/Models/Temperature.swift +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -1,33 +1,23 @@ import Foundation /// Type that holds information about daily temperature changes -public struct Temperature: Codable, Equatable, Hashable { +public struct Temperature: Equatable, Hashable { /// The actually measured temperature public let actual: Double /// The feels-like temperature public let feelsLike: Double - /// A convertible measurement of the actually measured temperature - public var actualMeasurement: Measurement { - actualMeasurement(units: OpenWeather.shared.units) - } - /// A convertible measurement of the actually measured temperature /// - Parameter units: The units to use when formatting the `actual` property - public func actualMeasurement(units: WeatherResponse.Units) -> Measurement { - Measurement(value: actual, unit: units.temperatureUnit) - } - - /// A convertible measurement of how the actually measured temperature feels like - public var feelsLikeMeasurement: Measurement { - feelsLikeMeasurement(units: OpenWeather.shared.units) + public func actualMeasurement(unit: Weather.Units) -> Measurement { + Measurement(value: actual, unit: unit.temperatureUnit) } /// A convertible measurement of how the actually measured temperature feels like /// - Parameter units: The units to use when formatting the `feelsLike` property - public func feelsLikeMeasurement(units: WeatherResponse.Units) -> Measurement { - Measurement(value: feelsLike, unit: units.temperatureUnit) + public func feelsLikeMeasurement(unit: Weather.Units) -> Measurement { + Measurement(value: feelsLike, unit: unit.temperatureUnit) } } diff --git a/Sources/HPOpenWeather/Response/WeatherResponse+Language.swift b/Sources/HPOpenWeather/Models/Weather+Language.swift similarity index 97% rename from Sources/HPOpenWeather/Response/WeatherResponse+Language.swift rename to Sources/HPOpenWeather/Models/Weather+Language.swift index 624b882..af2aca8 100644 --- a/Sources/HPOpenWeather/Response/WeatherResponse+Language.swift +++ b/Sources/HPOpenWeather/Models/Weather+Language.swift @@ -1,6 +1,6 @@ import Foundation -public extension WeatherResponse { +public extension Weather { /// The language that should be used in API responses for example for weather condition descriptions enum Language: String, Codable { diff --git a/Sources/HPOpenWeather/Response/WeatherResponse+Units.swift b/Sources/HPOpenWeather/Models/Weather+Units.swift similarity index 96% rename from Sources/HPOpenWeather/Response/WeatherResponse+Units.swift rename to Sources/HPOpenWeather/Models/Weather+Units.swift index 38c0fbf..2f16e86 100644 --- a/Sources/HPOpenWeather/Response/WeatherResponse+Units.swift +++ b/Sources/HPOpenWeather/Models/Weather+Units.swift @@ -1,6 +1,6 @@ import Foundation -public extension WeatherResponse { +public extension Weather { /// The units that should the data in the API responses should be formatted in enum Units: String, Codable { diff --git a/Sources/HPOpenWeather/Models/Weather.swift b/Sources/HPOpenWeather/Models/Weather.swift new file mode 100644 index 0000000..8c49025 --- /dev/null +++ b/Sources/HPOpenWeather/Models/Weather.swift @@ -0,0 +1,47 @@ +import Foundation + +public struct Weather: Decodable, Equatable, Hashable { + + // MARK: - Nested Types + + enum CodingKeys: String, CodingKey { + case timezoneIdentifier = "timezone" + case currentWeather = "current" + case minutelyForecasts = "minutely" + case hourlyForecasts = "hourly" + case dailyForecasts = "daily" + case alerts + } + + // MARK: - Properties + + public let timezone: TimeZone + public let currentWeather: CurrentWeather? + public let minutelyForecasts: [MinutelyForecast]? + public let hourlyForecasts: [HourlyForecast]? + public let dailyForecasts: [DailyForecast]? + /// Government weather alerts data from major national weather warning systems + public let alerts: [WeatherAlert]? + + public internal(set) var language: Weather.Language! + public internal(set) var units: Weather.Units! + + // MARK: - Init + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let timezoneIdentifier = try container.decode(String.self, forKey: .timezoneIdentifier) + guard let timezone = TimeZone(identifier: timezoneIdentifier) else { + throw OpenWeatherError.invalidTimeZoneIdentifier(timezoneIdentifier) + } + self.timezone = timezone + + self.currentWeather = try container.decodeIfPresent(CurrentWeather.self, forKey: .currentWeather) + self.minutelyForecasts = try container.decodeIfPresent([MinutelyForecast].self, forKey: .minutelyForecasts) + self.hourlyForecasts = try container.decodeIfPresent([HourlyForecast].self, forKey: .hourlyForecasts) + self.dailyForecasts = try container.decodeIfPresent([DailyForecast].self, forKey: .dailyForecasts) + self.alerts = try container.decodeIfPresent([WeatherAlert].self, forKey: .alerts) + } + +} diff --git a/Sources/HPOpenWeather/Models/WeatherAlert.swift b/Sources/HPOpenWeather/Models/WeatherAlert.swift index 7659d38..93a68fc 100644 --- a/Sources/HPOpenWeather/Models/WeatherAlert.swift +++ b/Sources/HPOpenWeather/Models/WeatherAlert.swift @@ -3,6 +3,18 @@ import Foundation /// Type that holds information about weather alerts public struct WeatherAlert: Codable, Hashable, Equatable { + // MARK: - Nested Types + + enum CodingKeys: String, CodingKey { + case senderName = "sender_name" + case eventName = "event" + case startDate = "start" + case endDate = "end" + case description + } + + // MARK: - Properties + /// Name of the alert source. Please read here the full list of alert sources public let senderName: String /// Alert event name @@ -14,12 +26,4 @@ public struct WeatherAlert: Codable, Hashable, Equatable { /// Description of the alert public let description: String - enum CodingKeys: String, CodingKey { - case senderName = "sender_name" - case eventName = "event" - case startDate = "start" - case endDate = "end" - case description - } - } diff --git a/Sources/HPOpenWeather/Models/Wind.swift b/Sources/HPOpenWeather/Models/Wind.swift index 414e32d..c40e7f8 100644 --- a/Sources/HPOpenWeather/Models/Wind.swift +++ b/Sources/HPOpenWeather/Models/Wind.swift @@ -9,16 +9,11 @@ public struct Wind: Codable, Equatable, Hashable { public let gust: Double? /// The wind direction measured in degrees from North public let degrees: Double? - - /// A measurement of the `speed` property if existing, measured in the units currently specified in `OpenWeather.shared` - public var speedMeasurement: Measurement? { - speedMeasurement(units: OpenWeather.shared.units) - } /// A measurement of the `speed` property if existing, measured in the passed in units /// - Parameter units: The units to use when formatting the `speed` property - public func speedMeasurement(units: WeatherResponse.Units) -> Measurement? { - guard let speed = speed else { + public func speedMeasurement(units: Weather.Units) -> Measurement? { + guard let speed else { return nil } return Measurement(value: speed, unit: units.windSpeedUnit) diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index 3ab6e95..c6580b1 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -11,16 +11,16 @@ public final class OpenWeather { /// The API key to use for weather requests let apiKey : String /// The language that will be used in weather responses - let language: WeatherResponse.Language + let language: Weather.Language /// The units that will be used in weather responses - let units: WeatherResponse.Units + let units: Weather.Units /// Initialises a new settings instance /// - Parameters: /// - apiKey: The API key to use for weather requests /// - language: The language that will be used in weather responses /// - units: The units that will be used in weather responses - public init(apiKey: String, language: WeatherResponse.Language = .english, units: WeatherResponse.Units = .metric) { + public init(apiKey: String, language: Weather.Language = .english, units: Weather.Units = .metric) { self.language = language self.units = units self.apiKey = apiKey @@ -29,15 +29,12 @@ public final class OpenWeather { // MARK: - Properties - /// A shared instance of the weather client - public static let shared = OpenWeather() - /// The OpenWeatherMap API key to authorize requests public var apiKey : String? /// The language that should be used in API responses - public var language: WeatherResponse.Language = .english + public var language: Weather.Language = .english /// The units that should be used to format the API responses - public var units: WeatherResponse.Units = .metric + public var units: Weather.Units = .metric // MARK: - Init @@ -64,37 +61,20 @@ public final class OpenWeather { /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved /// - urlSession: The `URLSession` that will be used schedule requests /// - Returns: A weather response object - public func weatherResponse( - coordinate: CLLocationCoordinate2D, - excludedFields: [WeatherRequest.ExcludableField]? = nil, + public func weather( + for coordinate: CLLocationCoordinate2D, + excludedFields: [ExcludableField]? = nil, date: Date? = nil, urlSession: URLSession = .shared - ) async throws -> WeatherResponse { - let request = WeatherRequest( - coordinate: coordinate, - excludedFields: excludedFields, - date: date - ) - return try await weatherResponse(request, urlSession: urlSession) - } - - /// Sends the specified request to the OpenWeather API - /// - Parameters: - /// - request: The request object that holds information about request location, date, etc. - /// - urlSession: The `URLSession` that will be used schedule requests - /// - Returns: A weather response object - public func weatherResponse(_ request: WeatherRequest, urlSession: URLSession = .shared) async throws -> WeatherRequest.Output { - guard let apiKey = apiKey else { - throw NSError.noApiKey + ) async throws -> Weather { + guard let apiKey else { + throw OpenWeatherError.invalidAPIKey } let settings = Settings(apiKey: apiKey, language: language, units: units) + let request = WeatherRequest(coordinate: coordinate, excludedFields: excludedFields, date: date , settings: settings, version: .new) - let networkRequest = try request.makeNetworkRequest(settings: settings, urlSession: urlSession) - var response = try await networkRequest.response().output - response.units = settings.units - response.language = settings.language - return response + return try await request.response(urlSession: urlSession).output } /// Sends the specified request to the OpenWeather API @@ -103,17 +83,17 @@ public final class OpenWeather { /// - urlSession: The `URLSession` that will be used schedule requests /// - completion: A completion that will be called with the result of the network request /// - Returns: A network task that can be used to cancel the request - @discardableResult - public func schedule(_ request: WeatherRequest, urlSession: URLSession = .shared, completion: @escaping (Result) -> Void) -> Task { - Task { - do { - let response = try await weatherResponse(request, urlSession: urlSession) - completion(.success(response)) - } catch { - completion(.failure(error)) - } - } - } + // @discardableResult + // public func schedule(_ request: WeatherRequest, urlSession: URLSession = .shared, completion: @escaping (Result) -> Void) -> Task { + // Task { + // do { + // let response = try await weatherResponse(request, urlSession: urlSession) + // completion(.success(response)) + // } catch { + // completion(.failure(error)) + // } + // } + // } // MARK: - Applying Settings diff --git a/Sources/HPOpenWeather/OpenWeatherError.swift b/Sources/HPOpenWeather/OpenWeatherError.swift new file mode 100644 index 0000000..efc69a3 --- /dev/null +++ b/Sources/HPOpenWeather/OpenWeatherError.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum OpenWeatherError: Error { + case invalidTimeMachineDate + case invalidAPIKey + case noCurrentConditionReturned + case invalidTimeZoneIdentifier(_ identifier: String) +} diff --git a/Sources/HPOpenWeather/Requests/APINetworkRequest.swift b/Sources/HPOpenWeather/Requests/APINetworkRequest.swift deleted file mode 100644 index 9e799ec..0000000 --- a/Sources/HPOpenWeather/Requests/APINetworkRequest.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import HPNetwork - -struct APINetworkRequest: DecodableRequest { - - typealias Output = WeatherResponse - - static let decoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - return decoder - }() - - let url: URL? - let urlSession: URLSession - let requestMethod: NetworkRequestMethod = .get - let headerFields = [NetworkRequestHeaderField.contentTypeJSON] - - var decoder: JSONDecoder { - APINetworkRequest.decoder - } - - func makeURL() throws -> URL { - guard let url = url else { - throw NSError(code: 6, description: "Could not create URL") - } - return url - } - -} diff --git a/Sources/HPOpenWeather/Requests/ExcludableField.swift b/Sources/HPOpenWeather/Requests/ExcludableField.swift new file mode 100644 index 0000000..3969d2d --- /dev/null +++ b/Sources/HPOpenWeather/Requests/ExcludableField.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum ExcludableField: String, Codable { + case current + case minutely + case hourly + case daily + case alerts +} diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift b/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift deleted file mode 100644 index d32b1f5..0000000 --- a/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Combine -import Foundation - -public extension WeatherRequest { - - func publisher( - apiKey: String, - language: WeatherResponse.Language = .english, - units: WeatherResponse.Units = .metric, - urlSession: URLSession = .shared, - finishingQueue: DispatchQueue = .main) -> AnyPublisher - { - publisher( - settings: OpenWeather.Settings(apiKey: apiKey, language: language, units: units), - urlSession: urlSession, - finishingQueue: finishingQueue - ) - } - - func publisher(settings: OpenWeather.Settings, urlSession: URLSession = .shared, finishingQueue: DispatchQueue = .main) -> AnyPublisher { - let request = APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession) - return request.dataTaskPublisher() - } - -} diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift b/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift deleted file mode 100644 index bf70b80..0000000 --- a/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -public extension WeatherRequest { - - enum ExcludableField: String, Codable { - case current - case minutely - case hourly - case daily - case alerts - } - -} diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index 8123b16..0b3c952 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -3,66 +3,66 @@ import CoreLocation import HPNetwork import HPURLBuilder -public struct WeatherRequest: Codable { +struct WeatherRequest: DecodableRequest { // MARK: - Associated Types - public typealias Output = WeatherResponse + typealias Output = Weather + + enum Version: String { + case old = "2.5" + case new = "3.0" + } // MARK: - Properties - public let coordinate: CLLocationCoordinate2D - public let excludedFields: [ExcludableField]? - public let date: Date? + let coordinate: CLLocationCoordinate2D + let excludedFields: [ExcludableField]? + let date: Date? + let settings: OpenWeather.Settings + let version: Version - // MARK: - Init + let requestMethod: HTTPRequest.Method = .get - public init(coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil) { - self.coordinate = coordinate - self.excludedFields = excludedFields?.hp_nilIfEmpty() - self.date = date + var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + return decoder } // MARK: - OpenWeatherRequest - func makeURL(settings: OpenWeather.Settings) -> URL? { - URL.build { + func makeURL() throws -> URL { + if let date = date, date < Date(), abs(date.timeIntervalSinceNow) <= 6 * .hour { + throw OpenWeatherError.invalidTimeMachineDate + } + return try URL.buildThrowing { Host("api.openweathermap.org") PathComponent("data") - PathComponent("2.5") + PathComponent(version.rawValue) PathComponent("onecall") PathComponent(date != nil ? "timemachine" : nil) QueryItem(name: "lat", value: coordinate.latitude, digits: 5) QueryItem(name: "lon", value: coordinate.longitude, digits: 5) + QueryItem(name: "appid", value: settings.apiKey) QueryItem(name: "dt", value: date.flatMap({ Int($0.timeIntervalSince1970) })) QueryItem(name: "exclude", value: excludedFields?.compactMap({ $0.rawValue })) - QueryItem(name: "appid", value: settings.apiKey) - QueryItem(name: "units", value: settings.units.rawValue) + QueryItem(name: "units", value: "metric") QueryItem(name: "lang", value: settings.language.rawValue) } } - func makeNetworkRequest(settings: OpenWeather.Settings, urlSession: URLSession) throws -> APINetworkRequest { - if let date = date, date < Date(), abs(date.timeIntervalSinceNow) <= 6 * .hour { - throw NSError.timeMachineDate - } - return APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession) - } - -} - -extension Collection { - - func hp_nilIfEmpty() -> Self? { - isEmpty ? nil : self + func convertResponse(data: Data, response: HTTPResponse) throws -> Weather { + var weather = try decoder.decode(Weather.self, from: data) + weather.units = settings.units + weather.language = settings.language + return weather } } extension TimeInterval { - static let minute = 60.00 static let hour = 3600.00 - static let day = 86400.00 } diff --git a/Sources/HPOpenWeather/Response/WeatherResponse.swift b/Sources/HPOpenWeather/Response/WeatherResponse.swift deleted file mode 100644 index 3e1804e..0000000 --- a/Sources/HPOpenWeather/Response/WeatherResponse.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -public struct WeatherResponse: Codable, Equatable, Hashable { - - private let timezoneIdentifier: String - public let currentWeather: CurrentWeather? - public let hourlyForecasts: [HourlyForecast]? - public let dailyForecasts: [DailyForecast]? - /// Government weather alerts data from major national weather warning systems - public let alerts: [WeatherAlert]? - - public internal(set) var language: WeatherResponse.Language? - public internal(set) var units: WeatherResponse.Units? - - public var timezone: TimeZone? { - TimeZone(identifier: timezoneIdentifier) - } - - enum CodingKeys: String, CodingKey { - case timezoneIdentifier = "timezone" - case currentWeather = "current" - case hourlyForecasts = "hourly" - case dailyForecasts = "daily" - case alerts - } - -} - -public struct WeatherResponseContainer: Codable, Equatable, Hashable { - - public let response: WeatherResponse - public let language: WeatherResponse.Language - public let units: WeatherResponse.Units - -} diff --git a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift index 1414432..9de6f90 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift @@ -1,69 +1,118 @@ import XCTest import HPNetwork +import HPNetworkMock + @testable import HPOpenWeather final class HPOpenWeatherTests: XCTestCase { - override class func setUp() { - super.setUp() - OpenWeather.shared.apiKey = TestSecret.apiKey - } + // MARK: - Properties + + private lazy var mockedURLSession: URLSession = { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [URLSessionMock.self] + return URLSession(configuration: configuration) + }() - override class func tearDown() { + // MARK: - Test Lifecycle + + override func tearDown() { + URLSessionMock.unregisterAllMockedRequests() super.tearDown() - OpenWeather.shared.apiKey = nil } - func testCurrentRequest() async throws { - do { - _ = try await OpenWeather.shared.weatherResponse(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) - } catch let error as NSError { - print(error) - throw error - } - } + // MARK: - Tests - func testTimeMachineRequestFailing() async throws { - let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) + func testOldApiRequest() async throws { + try make25WeatherResponse(version: .old) - await HPAssertThrowsError { - try await OpenWeather.shared.weatherResponse(request) - } + let settings = OpenWeather.Settings(apiKey: "debug") + let request = WeatherRequest( + coordinate: .init(latitude: 52.5200, longitude: 13.4050), + excludedFields: nil, + date: nil, + settings: settings, + version: .old + ) + _ = try await request.response(urlSession: mockedURLSession) } - func testTimeMachineRequest() async { - let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) + func testNewApiRequest() async throws { + try make25WeatherResponse(version: .new) - await HPAssertThrowsNoError { - try await OpenWeather.shared.weatherResponse(request) - } - } - - func testPublisher() { - let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) - - let expectationFinished = expectation(description: "finished") - let expectationReceive = expectation(description: "receiveValue") - //let expectationFailure = expectation(description: "failure") - - let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( - receiveCompletion: { result in - switch result { - case .failure(let error): - XCTFail(error.localizedDescription) - case .finished: - expectationFinished.fulfill() - } - }, receiveValue: { response in - expectationReceive.fulfill() - } + let settings = OpenWeather.Settings(apiKey: "debug") + let request = WeatherRequest( + coordinate: .init(latitude: 52.5200, longitude: 13.4050), + excludedFields: nil, + date: nil, + settings: settings, + version: .new ) + let weather = try await request.response(urlSession: mockedURLSession).output - waitForExpectations(timeout: 10) { error in - cancellable.cancel() + let currentWeather = try XCTUnwrap(weather.currentWeather) + XCTAssertEqual(currentWeather.timestamp, Date(timeIntervalSince1970: 1713795125)) + } + + // MARK: - Helpers + + private func make25WeatherResponse(version: WeatherRequest.Version) throws { + let url = try XCTUnwrap(URL(string: "https://api.openweathermap.org/data/\(version.rawValue)/onecall")) + let jsonDataURL = try XCTUnwrap(Bundle.module.url(forResource: "\(version.rawValue.replacingOccurrences(of: ".", with: "-"))-test-response", withExtension: "json")) + let jsonData = try Data(contentsOf: jsonDataURL) + + _ = URLSessionMock.mockRequest(to: url, ignoresQuery: true) { _ in + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": ContentType.applicationJSON.rawValue] + )! + return (jsonData, response) } } +// func testTimeMachineRequestFailing() async throws { +// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) +// +// await HPAssertThrowsError { +// try await OpenWeather.shared.weatherResponse(request) +// } +// } +// +// func testTimeMachineRequest() async { +// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) +// +// await HPAssertThrowsNoError { +// try await OpenWeather.shared.weatherResponse(request) +// } +// } +// +// func testPublisher() { +// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) +// +// let expectationFinished = expectation(description: "finished") +// let expectationReceive = expectation(description: "receiveValue") +// //let expectationFailure = expectation(description: "failure") +// +// let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( +// receiveCompletion: { result in +// switch result { +// case .failure(let error): +// XCTFail(error.localizedDescription) +// case .finished: +// expectationFinished.fulfill() +// } +// }, receiveValue: { response in +// expectationReceive.fulfill() +// } +// ) +// +// waitForExpectations(timeout: 10) { error in +// cancellable.cancel() +// } +// } + } extension Encodable { @@ -79,34 +128,3 @@ extension Encodable { } } - -func HPAssertThrowsError(_ work: () async throws -> T) async { - do { - _ = try await work() - XCTFail("Block should throw") - } catch { - return - } -} - -func HPAssertThrowsNoError(_ work: () async throws -> T) async { - do { - _ = try await work() - } catch let error { - XCTFail(error.localizedDescription) - } -} - -/// Asserts that the result is not a failure -func XCTAssertResult(_ result: Result) { - if case .failure(let error as NSError) = result { - XCTFail(error.localizedDescription) - } -} - -/// Asserts that the result is not a failure -func XCTAssertResultError(_ result: Result) { - if case .success(_) = result { - XCTFail("Result was not an error") - } -} diff --git a/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json b/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json new file mode 100644 index 0000000..56f0670 --- /dev/null +++ b/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json @@ -0,0 +1,1487 @@ +{ + "lat": 52.52, + "lon": 13.405, + "timezone": "Europe/Berlin", + "timezone_offset": 7200, + "current": { + "dt": 1713733621, + "sunrise": 1713671679, + "sunset": 1713723300, + "temp": 3.32, + "feels_like": -1.08, + "pressure": 1012, + "humidity": 71, + "dew_point": -1.26, + "uvi": 0, + "clouds": 0, + "visibility": 10000, + "wind_speed": 5.66, + "wind_deg": 20, + "weather": [ + { "id": 800, "main": "Clear", "description": "clear sky", "icon": "01n" } + ] + }, + "minutely": [ + { "dt": 1713733680, "precipitation": 0 }, + { "dt": 1713733740, "precipitation": 0 }, + { "dt": 1713733800, "precipitation": 0 }, + { "dt": 1713733860, "precipitation": 0 }, + { "dt": 1713733920, "precipitation": 0 }, + { "dt": 1713733980, "precipitation": 0 }, + { "dt": 1713734040, "precipitation": 0 }, + { "dt": 1713734100, "precipitation": 0 }, + { "dt": 1713734160, "precipitation": 0 }, + { "dt": 1713734220, "precipitation": 0 }, + { "dt": 1713734280, "precipitation": 0 }, + { "dt": 1713734340, "precipitation": 0 }, + { "dt": 1713734400, "precipitation": 0 }, + { "dt": 1713734460, "precipitation": 0 }, + { "dt": 1713734520, "precipitation": 0 }, + { "dt": 1713734580, "precipitation": 0 }, + { "dt": 1713734640, "precipitation": 0 }, + { "dt": 1713734700, "precipitation": 0 }, + { "dt": 1713734760, "precipitation": 0 }, + { "dt": 1713734820, "precipitation": 0 }, + { "dt": 1713734880, "precipitation": 0 }, + { "dt": 1713734940, "precipitation": 0 }, + { "dt": 1713735000, "precipitation": 0 }, + { "dt": 1713735060, "precipitation": 0 }, + { "dt": 1713735120, "precipitation": 0 }, + { "dt": 1713735180, "precipitation": 0 }, + { "dt": 1713735240, "precipitation": 0 }, + { "dt": 1713735300, "precipitation": 0 }, + { "dt": 1713735360, "precipitation": 0 }, + { "dt": 1713735420, "precipitation": 0 }, + { "dt": 1713735480, "precipitation": 0 }, + { "dt": 1713735540, "precipitation": 0 }, + { "dt": 1713735600, "precipitation": 0 }, + { "dt": 1713735660, "precipitation": 0 }, + { "dt": 1713735720, "precipitation": 0 }, + { "dt": 1713735780, "precipitation": 0 }, + { "dt": 1713735840, "precipitation": 0 }, + { "dt": 1713735900, "precipitation": 0 }, + { "dt": 1713735960, "precipitation": 0 }, + { "dt": 1713736020, "precipitation": 0 }, + { "dt": 1713736080, "precipitation": 0 }, + { "dt": 1713736140, "precipitation": 0 }, + { "dt": 1713736200, "precipitation": 0 }, + { "dt": 1713736260, "precipitation": 0 }, + { "dt": 1713736320, "precipitation": 0 }, + { "dt": 1713736380, "precipitation": 0 }, + { "dt": 1713736440, "precipitation": 0 }, + { "dt": 1713736500, "precipitation": 0 }, + { "dt": 1713736560, "precipitation": 0 }, + { "dt": 1713736620, "precipitation": 0 }, + { "dt": 1713736680, "precipitation": 0 }, + { "dt": 1713736740, "precipitation": 0 }, + { "dt": 1713736800, "precipitation": 0 }, + { "dt": 1713736860, "precipitation": 0 }, + { "dt": 1713736920, "precipitation": 0 }, + { "dt": 1713736980, "precipitation": 0 }, + { "dt": 1713737040, "precipitation": 0 }, + { "dt": 1713737100, "precipitation": 0 }, + { "dt": 1713737160, "precipitation": 0 }, + { "dt": 1713737220, "precipitation": 0 } + ], + "hourly": [ + { + "dt": 1713733200, + "temp": 3.32, + "feels_like": -0.5, + "pressure": 1012, + "humidity": 71, + "dew_point": -1.26, + "uvi": 0, + "clouds": 0, + "visibility": 10000, + "wind_speed": 4.5, + "wind_deg": 35, + "wind_gust": 8.91, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1713736800, + "temp": 2.99, + "feels_like": -0.78, + "pressure": 1015, + "humidity": 69, + "dew_point": -1.88, + "uvi": 0, + "clouds": 9, + "visibility": 10000, + "wind_speed": 4.28, + "wind_deg": 36, + "wind_gust": 8.78, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1713740400, + "temp": 2.4, + "feels_like": -1.5, + "pressure": 1017, + "humidity": 68, + "dew_point": -2.56, + "uvi": 0, + "clouds": 18, + "visibility": 10000, + "wind_speed": 4.26, + "wind_deg": 37, + "wind_gust": 9.23, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1713744000, + "temp": 1.74, + "feels_like": -2.13, + "pressure": 1020, + "humidity": 68, + "dew_point": -3.12, + "uvi": 0, + "clouds": 23, + "visibility": 10000, + "wind_speed": 3.98, + "wind_deg": 40, + "wind_gust": 9.66, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1713747600, + "temp": 1.02, + "feels_like": -2.61, + "pressure": 1022, + "humidity": 69, + "dew_point": -3.56, + "uvi": 0, + "clouds": 10, + "visibility": 10000, + "wind_speed": 3.42, + "wind_deg": 43, + "wind_gust": 9.41, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1713751200, + "temp": 0.11, + "feels_like": -3.25, + "pressure": 1025, + "humidity": 71, + "dew_point": -4.6, + "uvi": 0, + "clouds": 17, + "visibility": 10000, + "wind_speed": 2.87, + "wind_deg": 43, + "wind_gust": 7.87, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1713754800, + "temp": -0.11, + "feels_like": -3.24, + "pressure": 1025, + "humidity": 73, + "dew_point": -4.36, + "uvi": 0, + "clouds": 30, + "visibility": 10000, + "wind_speed": 2.59, + "wind_deg": 32, + "wind_gust": 6.69, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1713758400, + "temp": -0.25, + "feels_like": -3.22, + "pressure": 1025, + "humidity": 74, + "dew_point": -4.26, + "uvi": 0, + "clouds": 39, + "visibility": 10000, + "wind_speed": 2.41, + "wind_deg": 22, + "wind_gust": 5.86, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713762000, + "temp": 0.39, + "feels_like": -2.39, + "pressure": 1025, + "humidity": 72, + "dew_point": -4.19, + "uvi": 0.15, + "clouds": 44, + "visibility": 10000, + "wind_speed": 2.34, + "wind_deg": 20, + "wind_gust": 4.91, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713765600, + "temp": 1.87, + "feels_like": -0.62, + "pressure": 1025, + "humidity": 64, + "dew_point": -4.15, + "uvi": 0.5, + "clouds": 42, + "visibility": 10000, + "wind_speed": 2.31, + "wind_deg": 17, + "wind_gust": 4.62, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713769200, + "temp": 3.37, + "feels_like": 0.76, + "pressure": 1025, + "humidity": 57, + "dew_point": -4.38, + "uvi": 1.09, + "clouds": 44, + "visibility": 10000, + "wind_speed": 2.74, + "wind_deg": 16, + "wind_gust": 4.61, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713772800, + "temp": 4.67, + "feels_like": 2.15, + "pressure": 1024, + "humidity": 50, + "dew_point": -4.83, + "uvi": 1.88, + "clouds": 56, + "visibility": 10000, + "wind_speed": 2.94, + "wind_deg": 12, + "wind_gust": 4.75, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713776400, + "temp": 5.36, + "feels_like": 2.66, + "pressure": 1024, + "humidity": 46, + "dew_point": -5.19, + "uvi": 1.84, + "clouds": 71, + "visibility": 10000, + "wind_speed": 3.41, + "wind_deg": 15, + "wind_gust": 5.1, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713780000, + "temp": 5.13, + "feels_like": 2.07, + "pressure": 1024, + "humidity": 48, + "dew_point": -5.1, + "uvi": 1.66, + "clouds": 78, + "visibility": 10000, + "wind_speed": 3.91, + "wind_deg": 23, + "wind_gust": 4.9, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713783600, + "temp": 6.32, + "feels_like": 3.97, + "pressure": 1024, + "humidity": 44, + "dew_point": -4.93, + "uvi": 3.11, + "clouds": 82, + "visibility": 10000, + "wind_speed": 3.18, + "wind_deg": 14, + "wind_gust": 4.72, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713787200, + "temp": 6.04, + "feels_like": 3.59, + "pressure": 1023, + "humidity": 45, + "dew_point": -5.08, + "uvi": 1.61, + "clouds": 85, + "visibility": 10000, + "wind_speed": 3.24, + "wind_deg": 6, + "wind_gust": 4.19, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713790800, + "temp": 7.1, + "feels_like": 4.96, + "pressure": 1023, + "humidity": 42, + "dew_point": -4.95, + "uvi": 2.33, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.11, + "wind_deg": 1, + "wind_gust": 4, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713794400, + "temp": 6.78, + "feels_like": 4.27, + "pressure": 1023, + "humidity": 42, + "dew_point": -5.19, + "uvi": 1.6, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.59, + "wind_deg": 3, + "wind_gust": 4.07, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713798000, + "temp": 7.25, + "feels_like": 4.71, + "pressure": 1023, + "humidity": 41, + "dew_point": -5.04, + "uvi": 0.97, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.84, + "wind_deg": 12, + "wind_gust": 4.21, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713801600, + "temp": 6.74, + "feels_like": 4.23, + "pressure": 1022, + "humidity": 43, + "dew_point": -5.11, + "uvi": 0.56, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.59, + "wind_deg": 20, + "wind_gust": 3.88, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713805200, + "temp": 6.3, + "feels_like": 4.25, + "pressure": 1022, + "humidity": 44, + "dew_point": -4.86, + "uvi": 0.22, + "clouds": 96, + "visibility": 10000, + "wind_speed": 2.74, + "wind_deg": 32, + "wind_gust": 2.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713808800, + "temp": 5.07, + "feels_like": 3.04, + "pressure": 1023, + "humidity": 50, + "dew_point": -4.57, + "uvi": 0, + "clouds": 81, + "visibility": 10000, + "wind_speed": 2.43, + "wind_deg": 44, + "wind_gust": 3.03, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713812400, + "temp": 4.22, + "feels_like": 2.49, + "pressure": 1023, + "humidity": 54, + "dew_point": -4.24, + "uvi": 0, + "clouds": 9, + "visibility": 10000, + "wind_speed": 1.97, + "wind_deg": 70, + "wind_gust": 3.53, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "pop": 0 + }, + { + "dt": 1713816000, + "temp": 3.63, + "feels_like": 2, + "pressure": 1023, + "humidity": 57, + "dew_point": -4.02, + "uvi": 0, + "clouds": 12, + "visibility": 10000, + "wind_speed": 1.8, + "wind_deg": 95, + "wind_gust": 3.29, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1713819600, + "temp": 3.18, + "feels_like": 1.71, + "pressure": 1023, + "humidity": 59, + "dew_point": -4.13, + "uvi": 0, + "clouds": 23, + "visibility": 10000, + "wind_speed": 1.62, + "wind_deg": 125, + "wind_gust": 2.62, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1713823200, + "temp": 2.69, + "feels_like": 1.11, + "pressure": 1022, + "humidity": 61, + "dew_point": -4.2, + "uvi": 0, + "clouds": 34, + "visibility": 10000, + "wind_speed": 1.65, + "wind_deg": 149, + "wind_gust": 2.84, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1713826800, + "temp": 2.34, + "feels_like": 0.7, + "pressure": 1022, + "humidity": 62, + "dew_point": -4.3, + "uvi": 0, + "clouds": 46, + "visibility": 10000, + "wind_speed": 1.65, + "wind_deg": 161, + "wind_gust": 2.82, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1713830400, + "temp": 2.07, + "feels_like": 0.38, + "pressure": 1022, + "humidity": 62, + "dew_point": -4.44, + "uvi": 0, + "clouds": 55, + "visibility": 10000, + "wind_speed": 1.66, + "wind_deg": 169, + "wind_gust": 2.73, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713834000, + "temp": 1.89, + "feels_like": 0.3, + "pressure": 1022, + "humidity": 63, + "dew_point": -4.44, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 180, + "wind_gust": 2.67, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713837600, + "temp": 1.66, + "feels_like": -0.17, + "pressure": 1022, + "humidity": 65, + "dew_point": -4.3, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.72, + "wind_deg": 194, + "wind_gust": 3.04, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713841200, + "temp": 1.24, + "feels_like": -0.84, + "pressure": 1021, + "humidity": 67, + "dew_point": -4.12, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.86, + "wind_deg": 210, + "wind_gust": 3.35, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713844800, + "temp": 1.1, + "feels_like": -1.43, + "pressure": 1021, + "humidity": 69, + "dew_point": -3.97, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.22, + "wind_deg": 217, + "wind_gust": 3.99, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713848400, + "temp": 1.47, + "feels_like": -1.12, + "pressure": 1021, + "humidity": 69, + "dew_point": -3.56, + "uvi": 0.13, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.34, + "wind_deg": 230, + "wind_gust": 4, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713852000, + "temp": 2.6, + "feels_like": 0.22, + "pressure": 1021, + "humidity": 66, + "dew_point": -3.23, + "uvi": 0.4, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.33, + "wind_deg": 253, + "wind_gust": 3.81, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713855600, + "temp": 4.13, + "feels_like": 1.81, + "pressure": 1020, + "humidity": 62, + "dew_point": -2.49, + "uvi": 0.92, + "clouds": 63, + "visibility": 10000, + "wind_speed": 2.57, + "wind_deg": 265, + "wind_gust": 3.94, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713859200, + "temp": 5.51, + "feels_like": 3.42, + "pressure": 1020, + "humidity": 60, + "dew_point": -1.66, + "uvi": 1.63, + "clouds": 57, + "visibility": 10000, + "wind_speed": 2.6, + "wind_deg": 267, + "wind_gust": 3.64, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713862800, + "temp": 6.7, + "feels_like": 4.78, + "pressure": 1020, + "humidity": 53, + "dew_point": -2.06, + "uvi": 2.56, + "clouds": 64, + "visibility": 10000, + "wind_speed": 2.67, + "wind_deg": 264, + "wind_gust": 3.96, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713866400, + "temp": 7.99, + "feels_like": 6.22, + "pressure": 1019, + "humidity": 42, + "dew_point": -4.09, + "uvi": 3.47, + "clouds": 59, + "visibility": 10000, + "wind_speed": 2.81, + "wind_deg": 272, + "wind_gust": 3.94, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713870000, + "temp": 8.81, + "feels_like": 7.26, + "pressure": 1018, + "humidity": 36, + "dew_point": -5.29, + "uvi": 3.45, + "clouds": 57, + "visibility": 10000, + "wind_speed": 2.72, + "wind_deg": 277, + "wind_gust": 3.79, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713873600, + "temp": 9.67, + "feels_like": 8.41, + "pressure": 1018, + "humidity": 34, + "dew_point": -5.52, + "uvi": 2.82, + "clouds": 61, + "visibility": 10000, + "wind_speed": 2.53, + "wind_deg": 271, + "wind_gust": 3.56, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713877200, + "temp": 9.91, + "feels_like": 8.59, + "pressure": 1017, + "humidity": 34, + "dew_point": -5.29, + "uvi": 1.83, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.69, + "wind_deg": 274, + "wind_gust": 3.44, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713880800, + "temp": 9.78, + "feels_like": 8.42, + "pressure": 1016, + "humidity": 35, + "dew_point": -5.05, + "uvi": 1.69, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.72, + "wind_deg": 293, + "wind_gust": 3.32, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713884400, + "temp": 9.98, + "feels_like": 8.86, + "pressure": 1015, + "humidity": 35, + "dew_point": -4.64, + "uvi": 0.95, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.41, + "wind_deg": 299, + "wind_gust": 2.93, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713888000, + "temp": 9.71, + "feels_like": 8.63, + "pressure": 1015, + "humidity": 37, + "dew_point": -4.34, + "uvi": 0.52, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.29, + "wind_deg": 298, + "wind_gust": 2.35, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713891600, + "temp": 9.06, + "feels_like": 8.13, + "pressure": 1015, + "humidity": 41, + "dew_point": -3.38, + "uvi": 0.2, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.96, + "wind_deg": 312, + "wind_gust": 1.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713895200, + "temp": 7.9, + "feels_like": 7.27, + "pressure": 1015, + "humidity": 46, + "dew_point": -2.97, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.49, + "wind_deg": 351, + "wind_gust": 1.66, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713898800, + "temp": 6.85, + "feels_like": 6.16, + "pressure": 1015, + "humidity": 51, + "dew_point": -2.68, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.42, + "wind_deg": 35, + "wind_gust": 2.15, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713902400, + "temp": 6.26, + "feels_like": 5.07, + "pressure": 1015, + "humidity": 53, + "dew_point": -2.62, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.77, + "wind_deg": 62, + "wind_gust": 3.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1713697200, + "sunrise": 1713671679, + "sunset": 1713723300, + "moonrise": 1713713940, + "moonset": 1713669360, + "moon_phase": 0.42, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 8.02, + "min": 1.1, + "max": 8.86, + "night": 3.32, + "eve": 6.26, + "morn": 1.3 + }, + "feels_like": { "day": 4.88, "night": -0.5, "eve": 3.33, "morn": -2.82 }, + "pressure": 1023, + "humidity": 41, + "dew_point": -4.55, + "wind_speed": 5.58, + "wind_deg": 51, + "wind_gust": 9.18, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 71, + "pop": 1, + "rain": 0.8, + "uvi": 3.47 + }, + { + "dt": 1713783600, + "sunrise": 1713757952, + "sunset": 1713809805, + "moonrise": 1713804660, + "moonset": 1713756240, + "moon_phase": 0.45, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 6.32, + "min": -0.25, + "max": 7.25, + "night": 3.18, + "eve": 6.3, + "morn": 0.39 + }, + "feels_like": { "day": 3.97, "night": 1.71, "eve": 4.25, "morn": -2.39 }, + "pressure": 1024, + "humidity": 44, + "dew_point": -4.93, + "wind_speed": 4.28, + "wind_deg": 36, + "wind_gust": 9.66, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 82, + "pop": 0, + "uvi": 3.11 + }, + { + "dt": 1713870000, + "sunrise": 1713844225, + "sunset": 1713896310, + "moonrise": 1713895500, + "moonset": 1713843180, + "moon_phase": 0.48, + "summary": "There will be partly cloudy today", + "temp": { + "day": 8.81, + "min": 1.1, + "max": 9.98, + "night": 5.59, + "eve": 9.06, + "morn": 1.47 + }, + "feels_like": { "day": 7.26, "night": 3.91, "eve": 8.13, "morn": -1.12 }, + "pressure": 1018, + "humidity": 36, + "dew_point": -5.29, + "wind_speed": 2.81, + "wind_deg": 272, + "wind_gust": 4.08, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 57, + "pop": 0, + "uvi": 3.47 + }, + { + "dt": 1713956400, + "sunrise": 1713930500, + "sunset": 1713982815, + "moonrise": 1713986520, + "moonset": 1713930240, + "moon_phase": 0.5, + "summary": "There will be partly cloudy today", + "temp": { + "day": 11, + "min": 2.71, + "max": 11.78, + "night": 6.66, + "eve": 9.64, + "morn": 3.21 + }, + "feels_like": { "day": 9.17, "night": 3.78, "eve": 7.24, "morn": 0.78 }, + "pressure": 1009, + "humidity": 39, + "dew_point": -2.38, + "wind_speed": 4.86, + "wind_deg": 19, + "wind_gust": 9.16, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 97, + "pop": 0, + "uvi": 3.4 + }, + { + "dt": 1714042800, + "sunrise": 1714016776, + "sunset": 1714069320, + "moonrise": 1714077660, + "moonset": 1714017480, + "moon_phase": 0.55, + "summary": "There will be partly cloudy today", + "temp": { + "day": 9.53, + "min": 4.57, + "max": 10.36, + "night": 5.95, + "eve": 8.04, + "morn": 4.92 + }, + "feels_like": { "day": 7.32, "night": 4.71, "eve": 6, "morn": 1.66 }, + "pressure": 1006, + "humidity": 36, + "dew_point": -4.62, + "wind_speed": 4.99, + "wind_deg": 350, + "wind_gust": 10.09, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 2.66 + }, + { + "dt": 1714129200, + "sunrise": 1714103053, + "sunset": 1714155825, + "moonrise": 0, + "moonset": 1714105020, + "moon_phase": 0.58, + "summary": "There will be partly cloudy today", + "temp": { + "day": 11.55, + "min": 3.66, + "max": 12.67, + "night": 7.8, + "eve": 10.31, + "morn": 4.96 + }, + "feels_like": { "day": 9.83, "night": 7.8, "eve": 8.65, "morn": 2.49 }, + "pressure": 1011, + "humidity": 41, + "dew_point": -1.11, + "wind_speed": 2.96, + "wind_deg": 287, + "wind_gust": 5.89, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 3.19 + }, + { + "dt": 1714215600, + "sunrise": 1714189331, + "sunset": 1714242329, + "moonrise": 1714168860, + "moonset": 1714193160, + "moon_phase": 0.61, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 15.79, + "min": 5.26, + "max": 15.79, + "night": 9.76, + "eve": 9.96, + "morn": 7.44 + }, + "feels_like": { "day": 14.34, "night": 7.6, "eve": 7.16, "morn": 5.53 }, + "pressure": 1010, + "humidity": 35, + "dew_point": 0.21, + "wind_speed": 6.09, + "wind_deg": 51, + "wind_gust": 9.68, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 94, + "pop": 1, + "rain": 18.54, + "uvi": 4 + }, + { + "dt": 1714302000, + "sunrise": 1714275610, + "sunset": 1714328834, + "moonrise": 1714259640, + "moonset": 1714282140, + "moon_phase": 0.64, + "summary": "There will be rain until morning, then partly cloudy", + "temp": { + "day": 9.68, + "min": 7.88, + "max": 10.51, + "night": 7.88, + "eve": 9.71, + "morn": 8.15 + }, + "feels_like": { "day": 7.73, "night": 7.88, "eve": 9.12, "morn": 5.52 }, + "pressure": 1010, + "humidity": 84, + "dew_point": 7.07, + "wind_speed": 4.43, + "wind_deg": 247, + "wind_gust": 8.15, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 0.18, + "uvi": 4 + } + ], + "alerts": [ + { + "sender_name": "Deutscher Wetterdienst", + "event": "frost", + "start": 1713729600, + "end": 1713769200, + "description": "There is a risk of frost (level 1 of 2).\nMinimum temperature: 0 - -4 °C; near surface: -3 - -7 °C", + "tags": ["Extreme low temperature"] + } + ] +} diff --git a/Tests/HPOpenWeatherTests/Resources/3-0-test-response.json b/Tests/HPOpenWeatherTests/Resources/3-0-test-response.json new file mode 100644 index 0000000..d005c13 --- /dev/null +++ b/Tests/HPOpenWeatherTests/Resources/3-0-test-response.json @@ -0,0 +1,1713 @@ +{ + "lat": 52.52, + "lon": 13.405, + "timezone": "Europe/Berlin", + "timezone_offset": 7200, + "current": { + "dt": 1713795125, + "sunrise": 1713757952, + "sunset": 1713809805, + "temp": 6.31, + "feels_like": 5.11, + "pressure": 1006, + "humidity": 69, + "dew_point": 1.05, + "uvi": 1.6, + "clouds": 0, + "visibility": 10000, + "wind_speed": 1.79, + "wind_deg": 110, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ] + }, + "minutely": [ + { + "dt": 1713795180, + "precipitation": 0 + }, + { + "dt": 1713795240, + "precipitation": 0 + }, + { + "dt": 1713795300, + "precipitation": 0 + }, + { + "dt": 1713795360, + "precipitation": 0 + }, + { + "dt": 1713795420, + "precipitation": 0 + }, + { + "dt": 1713795480, + "precipitation": 0 + }, + { + "dt": 1713795540, + "precipitation": 0 + }, + { + "dt": 1713795600, + "precipitation": 0 + }, + { + "dt": 1713795660, + "precipitation": 0 + }, + { + "dt": 1713795720, + "precipitation": 0 + }, + { + "dt": 1713795780, + "precipitation": 0 + }, + { + "dt": 1713795840, + "precipitation": 0 + }, + { + "dt": 1713795900, + "precipitation": 0 + }, + { + "dt": 1713795960, + "precipitation": 0 + }, + { + "dt": 1713796020, + "precipitation": 0 + }, + { + "dt": 1713796080, + "precipitation": 0 + }, + { + "dt": 1713796140, + "precipitation": 0 + }, + { + "dt": 1713796200, + "precipitation": 0 + }, + { + "dt": 1713796260, + "precipitation": 0 + }, + { + "dt": 1713796320, + "precipitation": 0 + }, + { + "dt": 1713796380, + "precipitation": 0 + }, + { + "dt": 1713796440, + "precipitation": 0 + }, + { + "dt": 1713796500, + "precipitation": 0 + }, + { + "dt": 1713796560, + "precipitation": 0 + }, + { + "dt": 1713796620, + "precipitation": 0 + }, + { + "dt": 1713796680, + "precipitation": 0 + }, + { + "dt": 1713796740, + "precipitation": 0 + }, + { + "dt": 1713796800, + "precipitation": 0 + }, + { + "dt": 1713796860, + "precipitation": 0 + }, + { + "dt": 1713796920, + "precipitation": 0 + }, + { + "dt": 1713796980, + "precipitation": 0 + }, + { + "dt": 1713797040, + "precipitation": 0 + }, + { + "dt": 1713797100, + "precipitation": 0 + }, + { + "dt": 1713797160, + "precipitation": 0 + }, + { + "dt": 1713797220, + "precipitation": 0 + }, + { + "dt": 1713797280, + "precipitation": 0 + }, + { + "dt": 1713797340, + "precipitation": 0 + }, + { + "dt": 1713797400, + "precipitation": 0 + }, + { + "dt": 1713797460, + "precipitation": 0 + }, + { + "dt": 1713797520, + "precipitation": 0 + }, + { + "dt": 1713797580, + "precipitation": 0 + }, + { + "dt": 1713797640, + "precipitation": 0 + }, + { + "dt": 1713797700, + "precipitation": 0 + }, + { + "dt": 1713797760, + "precipitation": 0 + }, + { + "dt": 1713797820, + "precipitation": 0 + }, + { + "dt": 1713797880, + "precipitation": 0 + }, + { + "dt": 1713797940, + "precipitation": 0 + }, + { + "dt": 1713798000, + "precipitation": 0 + }, + { + "dt": 1713798060, + "precipitation": 0 + }, + { + "dt": 1713798120, + "precipitation": 0 + }, + { + "dt": 1713798180, + "precipitation": 0 + }, + { + "dt": 1713798240, + "precipitation": 0 + }, + { + "dt": 1713798300, + "precipitation": 0 + }, + { + "dt": 1713798360, + "precipitation": 0 + }, + { + "dt": 1713798420, + "precipitation": 0 + }, + { + "dt": 1713798480, + "precipitation": 0 + }, + { + "dt": 1713798540, + "precipitation": 0 + }, + { + "dt": 1713798600, + "precipitation": 0 + }, + { + "dt": 1713798660, + "precipitation": 0 + }, + { + "dt": 1713798720, + "precipitation": 0 + } + ], + "hourly": [ + { + "dt": 1713794400, + "temp": 6.31, + "feels_like": 4.45, + "pressure": 1006, + "humidity": 69, + "dew_point": 1.05, + "uvi": 1.6, + "clouds": 0, + "visibility": 10000, + "wind_speed": 2.5, + "wind_deg": 19, + "wind_gust": 3.01, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.24, + "rain": { + "1h": 0.13 + } + }, + { + "dt": 1713798000, + "temp": 6.59, + "feels_like": 4.88, + "pressure": 1009, + "humidity": 64, + "dew_point": 0.27, + "uvi": 0.97, + "clouds": 20, + "visibility": 10000, + "wind_speed": 2.38, + "wind_deg": 38, + "wind_gust": 3.26, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "pop": 0.03 + }, + { + "dt": 1713801600, + "temp": 6.86, + "feels_like": 5.68, + "pressure": 1012, + "humidity": 58, + "dew_point": -0.72, + "uvi": 0.56, + "clouds": 40, + "visibility": 10000, + "wind_speed": 1.85, + "wind_deg": 40, + "wind_gust": 2.53, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713805200, + "temp": 6.83, + "feels_like": 5.95, + "pressure": 1016, + "humidity": 55, + "dew_point": -1.39, + "uvi": 0.22, + "clouds": 59, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 41, + "wind_gust": 1.73, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713808800, + "temp": 6.33, + "feels_like": 6.33, + "pressure": 1019, + "humidity": 54, + "dew_point": -2.02, + "uvi": 0, + "clouds": 79, + "visibility": 10000, + "wind_speed": 1.33, + "wind_deg": 66, + "wind_gust": 1.27, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713812400, + "temp": 5.66, + "feels_like": 5.66, + "pressure": 1022, + "humidity": 56, + "dew_point": -2.51, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.29, + "wind_deg": 115, + "wind_gust": 1.74, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713816000, + "temp": 5.07, + "feels_like": 3.78, + "pressure": 1022, + "humidity": 59, + "dew_point": -2.36, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.7, + "wind_deg": 138, + "wind_gust": 2.56, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713819600, + "temp": 4.54, + "feels_like": 2.62, + "pressure": 1022, + "humidity": 60, + "dew_point": -2.43, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.21, + "wind_deg": 159, + "wind_gust": 3.88, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713823200, + "temp": 3.72, + "feels_like": 2.08, + "pressure": 1022, + "humidity": 62, + "dew_point": -2.87, + "uvi": 0, + "clouds": 98, + "visibility": 10000, + "wind_speed": 1.82, + "wind_deg": 166, + "wind_gust": 3.2, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713826800, + "temp": 3.18, + "feels_like": 1.57, + "pressure": 1022, + "humidity": 64, + "dew_point": -2.99, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 1.73, + "wind_deg": 193, + "wind_gust": 3.06, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713830400, + "temp": 2.91, + "feels_like": 1.47, + "pressure": 1022, + "humidity": 66, + "dew_point": -2.97, + "uvi": 0, + "clouds": 97, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 211, + "wind_gust": 2.9, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713834000, + "temp": 2.7, + "feels_like": 1.05, + "pressure": 1021, + "humidity": 67, + "dew_point": -2.85, + "uvi": 0, + "clouds": 98, + "visibility": 10000, + "wind_speed": 1.7, + "wind_deg": 212, + "wind_gust": 2.91, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713837600, + "temp": 2.19, + "feels_like": 0.45, + "pressure": 1021, + "humidity": 70, + "dew_point": -2.89, + "uvi": 0, + "clouds": 72, + "visibility": 10000, + "wind_speed": 1.71, + "wind_deg": 217, + "wind_gust": 3.06, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713841200, + "temp": 1.63, + "feels_like": -0.58, + "pressure": 1021, + "humidity": 72, + "dew_point": -2.98, + "uvi": 0, + "clouds": 72, + "visibility": 10000, + "wind_speed": 2.02, + "wind_deg": 227, + "wind_gust": 3.86, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713844800, + "temp": 1.2, + "feels_like": -1.47, + "pressure": 1020, + "humidity": 74, + "dew_point": -2.99, + "uvi": 0, + "clouds": 78, + "visibility": 10000, + "wind_speed": 2.37, + "wind_deg": 226, + "wind_gust": 4.72, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713848400, + "temp": 1.61, + "feels_like": -1.23, + "pressure": 1020, + "humidity": 73, + "dew_point": -2.7, + "uvi": 0.13, + "clouds": 83, + "visibility": 10000, + "wind_speed": 2.62, + "wind_deg": 241, + "wind_gust": 4.82, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713852000, + "temp": 2.83, + "feels_like": 0.07, + "pressure": 1020, + "humidity": 75, + "dew_point": -1.3, + "uvi": 0.4, + "clouds": 84, + "visibility": 10000, + "wind_speed": 2.79, + "wind_deg": 270, + "wind_gust": 4.72, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713855600, + "temp": 4.41, + "feels_like": 2.05, + "pressure": 1020, + "humidity": 71, + "dew_point": -0.34, + "uvi": 0.92, + "clouds": 65, + "visibility": 10000, + "wind_speed": 2.68, + "wind_deg": 265, + "wind_gust": 4.19, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713859200, + "temp": 6.08, + "feels_like": 3.83, + "pressure": 1020, + "humidity": 61, + "dew_point": -0.88, + "uvi": 1.63, + "clouds": 62, + "visibility": 10000, + "wind_speed": 2.96, + "wind_deg": 266, + "wind_gust": 4.32, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713862800, + "temp": 7.2, + "feels_like": 4.99, + "pressure": 1019, + "humidity": 52, + "dew_point": -2.03, + "uvi": 2.56, + "clouds": 75, + "visibility": 10000, + "wind_speed": 3.25, + "wind_deg": 267, + "wind_gust": 4.81, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713866400, + "temp": 8.24, + "feels_like": 5.97, + "pressure": 1018, + "humidity": 44, + "dew_point": -3.43, + "uvi": 3.47, + "clouds": 81, + "visibility": 10000, + "wind_speed": 3.75, + "wind_deg": 276, + "wind_gust": 5.07, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713870000, + "temp": 8.81, + "feels_like": 6.58, + "pressure": 1018, + "humidity": 38, + "dew_point": -4.6, + "uvi": 3.45, + "clouds": 84, + "visibility": 10000, + "wind_speed": 3.91, + "wind_deg": 287, + "wind_gust": 4.91, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713873600, + "temp": 9, + "feels_like": 6.9, + "pressure": 1017, + "humidity": 36, + "dew_point": -5.17, + "uvi": 2.82, + "clouds": 87, + "visibility": 10000, + "wind_speed": 3.75, + "wind_deg": 294, + "wind_gust": 4.62, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713877200, + "temp": 9.49, + "feels_like": 7.58, + "pressure": 1017, + "humidity": 35, + "dew_point": -5.28, + "uvi": 1.83, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.58, + "wind_deg": 299, + "wind_gust": 4.39, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713880800, + "temp": 9.65, + "feels_like": 7.99, + "pressure": 1016, + "humidity": 34, + "dew_point": -5.36, + "uvi": 1.69, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.17, + "wind_deg": 301, + "wind_gust": 4.04, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713884400, + "temp": 9.59, + "feels_like": 7.98, + "pressure": 1015, + "humidity": 35, + "dew_point": -5.25, + "uvi": 0.95, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.06, + "wind_deg": 302, + "wind_gust": 3.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713888000, + "temp": 9.26, + "feels_like": 7.72, + "pressure": 1015, + "humidity": 36, + "dew_point": -4.87, + "uvi": 0.52, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.85, + "wind_deg": 304, + "wind_gust": 3.24, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713891600, + "temp": 8.57, + "feels_like": 7.27, + "pressure": 1014, + "humidity": 40, + "dew_point": -4.26, + "uvi": 0.2, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.31, + "wind_deg": 309, + "wind_gust": 2.45, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713895200, + "temp": 7.68, + "feels_like": 6.98, + "pressure": 1014, + "humidity": 43, + "dew_point": -3.95, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.52, + "wind_deg": 318, + "wind_gust": 1.75, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713898800, + "temp": 6.98, + "feels_like": 6.98, + "pressure": 1014, + "humidity": 47, + "dew_point": -3.68, + "uvi": 0, + "clouds": 99, + "visibility": 10000, + "wind_speed": 1.02, + "wind_deg": 353, + "wind_gust": 1.17, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713902400, + "temp": 6.46, + "feels_like": 6.46, + "pressure": 1014, + "humidity": 49, + "dew_point": -3.61, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.25, + "wind_deg": 50, + "wind_gust": 1.31, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713906000, + "temp": 5.97, + "feels_like": 4.77, + "pressure": 1014, + "humidity": 51, + "dew_point": -3.55, + "uvi": 0, + "clouds": 99, + "visibility": 10000, + "wind_speed": 1.74, + "wind_deg": 83, + "wind_gust": 2.47, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713909600, + "temp": 5.49, + "feels_like": 3.88, + "pressure": 1013, + "humidity": 53, + "dew_point": -3.48, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.06, + "wind_deg": 104, + "wind_gust": 3.53, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713913200, + "temp": 4.85, + "feels_like": 3.17, + "pressure": 1012, + "humidity": 55, + "dew_point": -3.62, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.02, + "wind_deg": 110, + "wind_gust": 3.63, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713916800, + "temp": 4.27, + "feels_like": 2.76, + "pressure": 1012, + "humidity": 56, + "dew_point": -3.71, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.78, + "wind_deg": 119, + "wind_gust": 3.27, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713920400, + "temp": 3.99, + "feels_like": 2.57, + "pressure": 1012, + "humidity": 57, + "dew_point": -3.83, + "uvi": 0, + "clouds": 97, + "visibility": 10000, + "wind_speed": 1.67, + "wind_deg": 118, + "wind_gust": 3.02, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713924000, + "temp": 3.47, + "feels_like": 1.61, + "pressure": 1011, + "humidity": 58, + "dew_point": -3.95, + "uvi": 0, + "clouds": 75, + "visibility": 10000, + "wind_speed": 1.98, + "wind_deg": 116, + "wind_gust": 3.7, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713927600, + "temp": 3.01, + "feels_like": 1.06, + "pressure": 1011, + "humidity": 61, + "dew_point": -3.88, + "uvi": 0, + "clouds": 66, + "visibility": 10000, + "wind_speed": 1.99, + "wind_deg": 117, + "wind_gust": 3.83, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1713931200, + "temp": 2.65, + "feels_like": 0.77, + "pressure": 1010, + "humidity": 63, + "dew_point": -3.64, + "uvi": 0, + "clouds": 59, + "visibility": 10000, + "wind_speed": 1.88, + "wind_deg": 112, + "wind_gust": 3.49, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713934800, + "temp": 3.44, + "feels_like": 1.66, + "pressure": 1010, + "humidity": 63, + "dew_point": -2.99, + "uvi": 0.17, + "clouds": 55, + "visibility": 10000, + "wind_speed": 1.9, + "wind_deg": 111, + "wind_gust": 3.4, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713938400, + "temp": 5.04, + "feels_like": 3.74, + "pressure": 1010, + "humidity": 59, + "dew_point": -2.23, + "uvi": 0.53, + "clouds": 51, + "visibility": 10000, + "wind_speed": 1.7, + "wind_deg": 108, + "wind_gust": 2.68, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713942000, + "temp": 6.82, + "feels_like": 5.79, + "pressure": 1010, + "humidity": 54, + "dew_point": -1.79, + "uvi": 1.12, + "clouds": 55, + "visibility": 10000, + "wind_speed": 1.7, + "wind_deg": 101, + "wind_gust": 2.19, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713945600, + "temp": 8.48, + "feels_like": 7.38, + "pressure": 1010, + "humidity": 49, + "dew_point": -1.73, + "uvi": 1.94, + "clouds": 38, + "visibility": 10000, + "wind_speed": 2.05, + "wind_deg": 96, + "wind_gust": 2.56, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713949200, + "temp": 9.97, + "feels_like": 9.05, + "pressure": 1009, + "humidity": 43, + "dew_point": -2.11, + "uvi": 2.79, + "clouds": 32, + "visibility": 10000, + "wind_speed": 2.14, + "wind_deg": 99, + "wind_gust": 2.47, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713952800, + "temp": 11.14, + "feels_like": 9.3, + "pressure": 1008, + "humidity": 38, + "dew_point": -2.66, + "uvi": 3.36, + "clouds": 45, + "visibility": 10000, + "wind_speed": 1.96, + "wind_deg": 105, + "wind_gust": 2.22, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + }, + { + "dt": 1713956400, + "temp": 12.05, + "feels_like": 10.2, + "pressure": 1008, + "humidity": 34, + "dew_point": -3.09, + "uvi": 3.4, + "clouds": 56, + "visibility": 10000, + "wind_speed": 1.66, + "wind_deg": 106, + "wind_gust": 1.97, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713960000, + "temp": 11.73, + "feels_like": 9.87, + "pressure": 1007, + "humidity": 35, + "dew_point": -3.17, + "uvi": 2.58, + "clouds": 63, + "visibility": 10000, + "wind_speed": 2.06, + "wind_deg": 96, + "wind_gust": 1.83, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1713963600, + "temp": 12.09, + "feels_like": 10.29, + "pressure": 1007, + "humidity": 36, + "dew_point": -2.53, + "uvi": 2.13, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.1, + "wind_deg": 96, + "wind_gust": 2.17, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1713783600, + "sunrise": 1713757952, + "sunset": 1713809805, + "moonrise": 1713804660, + "moonset": 1713756240, + "moon_phase": 0.45, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 6.46, + "min": -0.14, + "max": 6.86, + "night": 4.54, + "eve": 6.83, + "morn": 0.55 + }, + "feels_like": { + "day": 4.47, + "night": 2.62, + "eve": 5.95, + "morn": -2.18 + }, + "pressure": 1016, + "humidity": 55, + "dew_point": -1.7, + "wind_speed": 4.28, + "wind_deg": 36, + "wind_gust": 9.62, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 60, + "pop": 0.42, + "rain": 0.13, + "uvi": 3.11 + }, + { + "dt": 1713870000, + "sunrise": 1713844225, + "sunset": 1713896310, + "moonrise": 1713895500, + "moonset": 1713843180, + "moon_phase": 0.48, + "summary": "There will be partly cloudy today", + "temp": { + "day": 8.81, + "min": 1.2, + "max": 9.65, + "night": 5.97, + "eve": 8.57, + "morn": 1.61 + }, + "feels_like": { + "day": 6.58, + "night": 4.77, + "eve": 7.27, + "morn": -1.23 + }, + "pressure": 1018, + "humidity": 38, + "dew_point": -4.6, + "wind_speed": 3.91, + "wind_deg": 287, + "wind_gust": 5.07, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 84, + "pop": 0, + "uvi": 3.47 + }, + { + "dt": 1713956400, + "sunrise": 1713930500, + "sunset": 1713982815, + "moonrise": 1713986520, + "moonset": 1713930240, + "moon_phase": 0.5, + "summary": "There will be partly cloudy today", + "temp": { + "day": 12.05, + "min": 2.65, + "max": 12.09, + "night": 7.11, + "eve": 11.14, + "morn": 3.44 + }, + "feels_like": { + "day": 10.2, + "night": 4.96, + "eve": 9.33, + "morn": 1.66 + }, + "pressure": 1008, + "humidity": 34, + "dew_point": -3.09, + "wind_speed": 3.44, + "wind_deg": 45, + "wind_gust": 7.56, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 56, + "pop": 0, + "uvi": 3.4 + }, + { + "dt": 1714042800, + "sunrise": 1714016776, + "sunset": 1714069320, + "moonrise": 1714077660, + "moonset": 1714017480, + "moon_phase": 0.55, + "summary": "There will be partly cloudy today", + "temp": { + "day": 10.8, + "min": 4.47, + "max": 12.46, + "night": 6.99, + "eve": 9.86, + "morn": 4.85 + }, + "feels_like": { + "day": 8.88, + "night": 4.96, + "eve": 8.3, + "morn": 3.15 + }, + "pressure": 1005, + "humidity": 36, + "dew_point": -3.67, + "wind_speed": 3.09, + "wind_deg": 354, + "wind_gust": 5.38, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 2.66 + }, + { + "dt": 1714129200, + "sunrise": 1714103053, + "sunset": 1714155825, + "moonrise": 0, + "moonset": 1714105020, + "moon_phase": 0.58, + "summary": "There will be partly cloudy today", + "temp": { + "day": 14.89, + "min": 4.75, + "max": 14.89, + "night": 9.67, + "eve": 12.47, + "morn": 7.12 + }, + "feels_like": { + "day": 13.32, + "night": 9.67, + "eve": 10.92, + "morn": 6.36 + }, + "pressure": 1010, + "humidity": 34, + "dew_point": -0.95, + "wind_speed": 3.29, + "wind_deg": 219, + "wind_gust": 4.33, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": 73, + "pop": 0, + "uvi": 3.19 + }, + { + "dt": 1714215600, + "sunrise": 1714189331, + "sunset": 1714242329, + "moonrise": 1714168860, + "moonset": 1714193160, + "moon_phase": 0.61, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 18.41, + "min": 6.58, + "max": 18.81, + "night": 12.2, + "eve": 15.59, + "morn": 9.08 + }, + "feels_like": { + "day": 17.14, + "night": 11.17, + "eve": 14.48, + "morn": 7.32 + }, + "pressure": 1012, + "humidity": 32, + "dew_point": 1.18, + "wind_speed": 4.6, + "wind_deg": 132, + "wind_gust": 9.27, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": 7, + "pop": 0, + "uvi": 4 + }, + { + "dt": 1714302000, + "sunrise": 1714275610, + "sunset": 1714328834, + "moonrise": 1714259640, + "moonset": 1714282140, + "moon_phase": 0.64, + "summary": "There will be partly cloudy today", + "temp": { + "day": 21.09, + "min": 9.57, + "max": 21.71, + "night": 14.94, + "eve": 18.3, + "morn": 11.65 + }, + "feels_like": { + "day": 20.25, + "night": 14.27, + "eve": 17.54, + "morn": 10.86 + }, + "pressure": 1017, + "humidity": 38, + "dew_point": 6.37, + "wind_speed": 2.63, + "wind_deg": 108, + "wind_gust": 5.85, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 4 + }, + { + "dt": 1714388400, + "sunrise": 1714361891, + "sunset": 1714415338, + "moonrise": 1714349580, + "moonset": 1714372140, + "moon_phase": 0.68, + "summary": "There will be partly cloudy today", + "temp": { + "day": 22.15, + "min": 11.98, + "max": 23.78, + "night": 18.74, + "eve": 20.61, + "morn": 13.9 + }, + "feels_like": { + "day": 21.49, + "night": 18.37, + "eve": 20.19, + "morn": 13.3 + }, + "pressure": 1021, + "humidity": 41, + "dew_point": 8.34, + "wind_speed": 4.45, + "wind_deg": 91, + "wind_gust": 10.64, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": 100, + "pop": 0, + "uvi": 4 + } + ], + "alerts": [ + { + "sender_name": "Deutscher Wetterdienst", + "event": "frost", + "start": 1713816000, + "end": 1713855600, + "description": "There is a risk of frost (level 1 of 2).\nMinimum temperature: -1 - -4 °C; near surface: -4 - -7 °C", + "tags": ["Extreme low temperature"] + } + ] +} diff --git a/Tests/HPOpenWeatherTests/TestSecret.swift b/Tests/HPOpenWeatherTests/TestSecret.swift index 712e092..034351e 100644 --- a/Tests/HPOpenWeatherTests/TestSecret.swift +++ b/Tests/HPOpenWeatherTests/TestSecret.swift @@ -1,7 +1,7 @@ import Foundation -struct TestSecret { +enum TestSecret { - static let apiKey: String! = ProcessInfo.processInfo.environment["API_KEY"] + static let apiKey: String = ProcessInfo.processInfo.environment["API_KEY"] ?? "" } From db354e3b33c7a0fd462705a064e7fe1264896750 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 16:44:04 +0200 Subject: [PATCH 02/17] Add some scripts and config files Signed-off-by: Henrik Panhans --- Scripts/build-docc-archive | 29 ++++++++++++++ Scripts/configure-hooks | 10 +++++ Scripts/convert-coverage-report | 55 +++++++++++++++++++++++++++ Scripts/format-swift-code | 10 +++++ Scripts/lint-swift-code | 10 +++++ config/.prettierignore | 2 + config/periphery.yml | 1 + config/prettier.config.js | 17 +++++++++ config/swift-format.json | 67 +++++++++++++++++++++++++++++++++ 9 files changed, 201 insertions(+) create mode 100755 Scripts/build-docc-archive create mode 100755 Scripts/configure-hooks create mode 100755 Scripts/convert-coverage-report create mode 100755 Scripts/format-swift-code create mode 100755 Scripts/lint-swift-code create mode 100644 config/.prettierignore create mode 100644 config/periphery.yml create mode 100644 config/prettier.config.js create mode 100644 config/swift-format.json diff --git a/Scripts/build-docc-archive b/Scripts/build-docc-archive new file mode 100755 index 0000000..6842dc0 --- /dev/null +++ b/Scripts/build-docc-archive @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +if [[ -z $RUNNER_TEMP ]]; then + echo "RUNNER_TEMP is not set. Setting to root of repository." + RUNNER_TEMP=$(git rev-parse --show-toplevel) +fi + +# First, insert docc-plugin dependency. This is very hacky, but it avoids everyone having to pull in the docc-plugin when they use this library. + +sed '/swift-http-types.git/a\ +.package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"),\ +' "Package.swift" > "Package.tmp" + +mv "Package.tmp" "Package.swift" + +swift package \ + --allow-writing-to-directory "$RUNNER_TEMP/docs" \ + generate-documentation \ + --target HPOpenWeather \ + --transform-for-static-hosting \ + --hosting-base-path HPOpenWeather \ + --output-path "$RUNNER_TEMP/docs" + +echo "" > "$RUNNER_TEMP/docs/index.html" + +if [[ -z $GITHUB_ACTIONS ]]; then + echo "Restoring Package.swift to original state." + sed -i '' '/swift-docc-plugin/d' "Package.swift" +fi \ No newline at end of file diff --git a/Scripts/configure-hooks b/Scripts/configure-hooks new file mode 100755 index 0000000..a83bfc4 --- /dev/null +++ b/Scripts/configure-hooks @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +GIT_ROOT=$(git rev-parse --show-toplevel) + +# Check if the pre-commit hook already exists +if [ -f "$GIT_ROOT/.git/hooks/pre-commit" ]; then + rm "$GIT_ROOT/.git/hooks/pre-commit" +fi + +ln -s "$GIT_ROOT/Scripts/lint-swift-code" "$GIT_ROOT/.git/hooks/pre-commit" \ No newline at end of file diff --git a/Scripts/convert-coverage-report b/Scripts/convert-coverage-report new file mode 100755 index 0000000..26963df --- /dev/null +++ b/Scripts/convert-coverage-report @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Adapted from https://github.com/michaelhenry/swifty-code-coverage/blob/main/lcov.sh + +OUTPUT_FILE="coverage/lcov.info" +IGNORE_FILENAME_REGEX=".build|Tests|Pods|Carthage|DerivedData" +BUILD_PATH=".build" + +while :; do + case $1 in + --target) TARGET=$2 + shift + ;; + --output) OUTPUT_FILE=$2 + shift + ;; + *) break + esac + shift +done + +if [ -z "$BUILD_PATH" ]; then + echo "Missing --build-path. Either DerivedData or .build (for spm)" + exit 1 +fi + +if [ -z "$TARGET" ]; then + echo "Missing --target. Either an .app or an .xctest (for spm)" + exit 1 +fi + +INSTR_PROFILE=$(find $BUILD_PATH -name "*.profdata") +TARGET_PATH=$(find $BUILD_PATH -name "$TARGET" | tail -n1) +if [ -f $TARGET_PATH ]; then + OBJECT_FILE="$TARGET_PATH" +else + TARGET=$(echo $TARGET | sed 's/\.[^.]*$//') + OBJECT_FILE=$(find $BUILD_PATH -name "$TARGET" | tail -n1) +fi + +mkdir -p $(dirname "$OUTPUT_FILE") + +# print to stdout +xcrun llvm-cov report \ + "$OBJECT_FILE" \ + --instr-profile=$INSTR_PROFILE \ + --ignore-filename-regex=$IGNORE_FILENAME_REGEX \ + --use-color + +# Export to code coverage file +xcrun llvm-cov export \ + "$OBJECT_FILE" \ + --instr-profile=$INSTR_PROFILE \ + --ignore-filename-regex=$IGNORE_FILENAME_REGEX \ + --format="lcov" > $OUTPUT_FILE \ No newline at end of file diff --git a/Scripts/format-swift-code b/Scripts/format-swift-code new file mode 100755 index 0000000..4e66902 --- /dev/null +++ b/Scripts/format-swift-code @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +swift-format format \ + --recursive \ + --parallel \ + --in-place \ + --configuration config/swift-format.json \ + Sources/ \ + Tests/ \ + Package.swift \ No newline at end of file diff --git a/Scripts/lint-swift-code b/Scripts/lint-swift-code new file mode 100755 index 0000000..9af98be --- /dev/null +++ b/Scripts/lint-swift-code @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +swift-format lint \ + --recursive \ + --parallel \ + --strict \ + --configuration config/swift-format.json \ + Sources/ \ + Tests/ \ + Package.swift \ No newline at end of file diff --git a/config/.prettierignore b/config/.prettierignore new file mode 100644 index 0000000..f4aa5e4 --- /dev/null +++ b/config/.prettierignore @@ -0,0 +1,2 @@ +.swiftpm +.build \ No newline at end of file diff --git a/config/periphery.yml b/config/periphery.yml new file mode 100644 index 0000000..85b884a --- /dev/null +++ b/config/periphery.yml @@ -0,0 +1 @@ +retain_public: true diff --git a/config/prettier.config.js b/config/prettier.config.js new file mode 100644 index 0000000..1876910 --- /dev/null +++ b/config/prettier.config.js @@ -0,0 +1,17 @@ +/** @type {import('prettier').Config} */ +module.exports = { + endOfLine: "lf", + semi: false, + singleQuote: false, + tabWidth: 2, + trailingComma: "es5", + printWidth: 140, + overrides: [ + { + files: ["**/*.md"], + options: { + tabWidth: 4, + }, + }, + ], +} diff --git a/config/swift-format.json b/config/swift-format.json new file mode 100644 index 0000000..e7bcf68 --- /dev/null +++ b/config/swift-format.json @@ -0,0 +1,67 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 140, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": ["XCTAssertNoThrow"] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": true, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + }, + "spacesAroundRangeFormationOperators": true, + "tabWidth": 4, + "version": 1 +} From 96ee935778dfc6f219e72e4a6d888c6bd79714b9 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 16:55:06 +0200 Subject: [PATCH 03/17] Reformat all code --- Package.swift | 14 +-- .../CLLocationCoordinate+Extensions.swift | 2 +- .../Models/Forecasts/CurrentWeather.swift | 8 +- .../Models/Forecasts/DailyForecast.swift | 1 - .../Models/Forecasts/ForecastBase.swift | 22 ++--- .../Models/Forecasts/HourlyForecast.swift | 8 +- Sources/HPOpenWeather/Models/Moon.swift | 6 +- .../HPOpenWeather/Models/Precipitation.swift | 14 ++- Sources/HPOpenWeather/Models/Sun.swift | 10 +- .../HPOpenWeather/Models/Temperature.swift | 10 +- .../Models/Weather+Language.swift | 6 +- .../HPOpenWeather/Models/Weather+Units.swift | 12 +-- Sources/HPOpenWeather/Models/Weather.swift | 8 +- .../HPOpenWeather/Models/WeatherAlert.swift | 12 +-- .../Models/WeatherCondition.swift | 10 +- .../HPOpenWeather/Models/WeatherIcon.swift | 15 +-- Sources/HPOpenWeather/OpenWeather.swift | 35 +++---- .../Requests/WeatherRequest.swift | 2 +- .../HPOpenWeatherTests.swift | 91 ++++++++++--------- .../HPOpenWeatherTests/WeatherIconTests.swift | 17 ++-- 20 files changed, 157 insertions(+), 146 deletions(-) diff --git a/Package.swift b/Package.swift index fe63d8f..4b8ffc2 100644 --- a/Package.swift +++ b/Package.swift @@ -6,19 +6,19 @@ import PackageDescription let package = Package( name: "HPOpenWeather", platforms: [ - .iOS(.v15), .tvOS(.v15), .watchOS(.v7), .macOS(.v12) + .iOS(.v15), .tvOS(.v15), .watchOS(.v7), .macOS(.v12), ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "HPOpenWeather", targets: ["HPOpenWeather"] - ) + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "4.0.0"), - .package(url: "https://github.com/henrik-dmg/HPURLBuilder", from: "1.0.0"), + .package(url: "https://github.com/henrik-dmg/HPURLBuilder", from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -27,16 +27,16 @@ let package = Package( name: "HPOpenWeather", dependencies: [ .product(name: "HPNetworkMock", package: "HPNetwork"), - "HPURLBuilder" + "HPURLBuilder", ] - ), + ), .testTarget( name: "HPOpenWeatherTests", dependencies: [ "HPOpenWeather", - .product(name: "HPNetworkMock", package: "HPNetwork") + .product(name: "HPNetworkMock", package: "HPNetwork"), ], resources: [.process("Resources")] - ) + ), ] ) diff --git a/Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift b/Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift index 1b4da93..b71f8d2 100644 --- a/Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift +++ b/Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift @@ -1,5 +1,5 @@ -import Foundation import CoreLocation +import Foundation extension CLLocationCoordinate2D: Codable { diff --git a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift index be98c49..b400bcb 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift @@ -1,9 +1,9 @@ import Foundation public struct CurrentWeather: ForecastBase, SunForecast { - + // MARK: - Coding Keys - + enum CodingKeys: String, CodingKey { case feelsLikeTemperature = "feels_like" case snow @@ -23,9 +23,9 @@ public struct CurrentWeather: ForecastBase, SunForecast { case sunrise case sunset } - + // MARK: - Properties - + public let timestamp: Date public let pressure: Double? public let humidity: Double? diff --git a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift index cf25958..75a0d0d 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift @@ -83,4 +83,3 @@ public struct DailyForecast: ForecastBase, SunForecast, MoonForecast { } } - diff --git a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift index f058c29..6418cd0 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift @@ -2,36 +2,36 @@ import Foundation public protocol ForecastBase: Decodable, Hashable { - /// The timestamp when the data was collected + /// The timestamp when the data was collected. var timestamp: Date { get } - /// Atmospheric pressure on the sea level, hPa + /// Atmospheric pressure on the sea level, hPa. var pressure: Double? { get } - /// Humidity in percent + /// Humidity in percent. var humidity: Double? { get } - /// Atmospheric temperature (varying according to pressure and humidity) below which - /// water droplets begin to condense and dew can form. Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit. + /// Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. + /// Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit. var dewPoint: Double? { get } - /// UV index + /// UV index. var uvIndex: Double? { get } - /// Average visibility + /// Average visibility. var visibility: Double? { get } - /// Cloudiness in percent + /// Cloudiness in percent. var cloudCoverage: Double? { get } - /// Basic information about observed wind + /// Basic information about observed wind. var wind: Wind { get } } public protocol SunForecast: Decodable, Hashable { - /// A container that holds information about sunset and sunrise timestamps + /// A container that holds information about sunset and sunrise timestamps. var sun: Sun { get } } public protocol MoonForecast: Decodable, Hashable { - /// A container that holds information about moonrise and moonset timestamps + /// A container that holds information about moonrise and moonset timestamps. var moon: Moon { get } } diff --git a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift index 95ff0bf..80dbca2 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift @@ -1,9 +1,9 @@ import Foundation public struct HourlyForecast: ForecastBase { - + // MARK: - Coding Keys - + enum CodingKeys: String, CodingKey { case actualTemperature = "temp" case feelsLikeTemperature = "feels_like" @@ -21,9 +21,9 @@ public struct HourlyForecast: ForecastBase { case windDirection = "wind_deg" case weather } - + // MARK: - Properties - + public let timestamp: Date public let pressure: Double? public let humidity: Double? diff --git a/Sources/HPOpenWeather/Models/Moon.swift b/Sources/HPOpenWeather/Models/Moon.swift index c378e8b..0986e02 100644 --- a/Sources/HPOpenWeather/Models/Moon.swift +++ b/Sources/HPOpenWeather/Models/Moon.swift @@ -1,11 +1,11 @@ import Foundation -/// Type that holds information about moonset and moonrise times in UTC time +/// Type that holds information about moonset and moonrise times in UTC time. public struct Moon: Codable, Equatable, Hashable { - /// Moonset time + /// Moonset time. public let moonset: Date - /// Moonrise time + /// Moonrise time. public let moonrise: Date } diff --git a/Sources/HPOpenWeather/Models/Precipitation.swift b/Sources/HPOpenWeather/Models/Precipitation.swift index a0f1c7a..3e9472a 100644 --- a/Sources/HPOpenWeather/Models/Precipitation.swift +++ b/Sources/HPOpenWeather/Models/Precipitation.swift @@ -1,19 +1,23 @@ import Foundation -/// Type that holds information about recent precipitation +/// Type that holds information about recent precipitation. public struct Precipitation: Codable, Equatable, Hashable { + // MARK: - Nested Types + enum CodingKeys: String, CodingKey { case lastHour = "1h" case lastThreeHours = "3h" } - /// Precipitation volume for the last 1 hour, measured in mm + // MARK: - Properties + + /// Precipitation volume for the last 1 hour, measured in mm. public var lastHour: Double? - /// Precipitation volume for the last 3 hours, measured in mm + /// Precipitation volume for the last 3 hours, measured in mm. public var lastThreeHours: Double? - /// A convertible measurement of how much precipitation occured in the last hour if any + /// A convertible measurement of how much precipitation occured in the last hour if any. public var lastHourMeasurement: Measurement? { guard let lastHour else { return nil @@ -21,7 +25,7 @@ public struct Precipitation: Codable, Equatable, Hashable { return Measurement(value: lastHour, unit: .millimeters) } - /// A convertible measurement of how much precipitation occured in the last three hours if any + /// A convertible measurement of how much precipitation occured in the last three hours if any. public var lastThreeHoursMeasurement: Measurement? { guard let lastThreeHours else { return nil diff --git a/Sources/HPOpenWeather/Models/Sun.swift b/Sources/HPOpenWeather/Models/Sun.swift index 778dbfa..139dc7a 100644 --- a/Sources/HPOpenWeather/Models/Sun.swift +++ b/Sources/HPOpenWeather/Models/Sun.swift @@ -1,11 +1,11 @@ import Foundation -/// Type that holds information about sunrise and sunset times in UTC time +/// Type that holds information about sunrise and sunset times in UTC time. public struct Sun: Codable, Equatable, Hashable { - - /// Sunset time + + /// Sunset time. public let sunset: Date - /// Sunrise time + /// Sunrise time. public let sunrise: Date - + } diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift index fed9b14..90d57d0 100644 --- a/Sources/HPOpenWeather/Models/Temperature.swift +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -1,20 +1,20 @@ import Foundation -/// Type that holds information about daily temperature changes +/// Type that holds information about daily temperature changes. public struct Temperature: Equatable, Hashable { - /// The actually measured temperature + /// The actually measured temperature. public let actual: Double - /// The feels-like temperature + /// The feels-like temperature. public let feelsLike: Double - /// A convertible measurement of the actually measured temperature + /// A convertible measurement of the actually measured temperature. /// - Parameter units: The units to use when formatting the `actual` property public func actualMeasurement(unit: Weather.Units) -> Measurement { Measurement(value: actual, unit: unit.temperatureUnit) } - /// A convertible measurement of how the actually measured temperature feels like + /// A convertible measurement of how the actually measured temperature feels like. /// - Parameter units: The units to use when formatting the `feelsLike` property public func feelsLikeMeasurement(unit: Weather.Units) -> Measurement { Measurement(value: feelsLike, unit: unit.temperatureUnit) diff --git a/Sources/HPOpenWeather/Models/Weather+Language.swift b/Sources/HPOpenWeather/Models/Weather+Language.swift index af2aca8..38b8df5 100644 --- a/Sources/HPOpenWeather/Models/Weather+Language.swift +++ b/Sources/HPOpenWeather/Models/Weather+Language.swift @@ -1,9 +1,9 @@ import Foundation -public extension Weather { +extension Weather { - /// The language that should be used in API responses for example for weather condition descriptions - enum Language: String, Codable { + /// The language that should be used in API responses for example for weather condition descriptions. + public enum Language: String, Codable { case afrikaans = "af" case arabic = "ar" case azerbaijani = "az" diff --git a/Sources/HPOpenWeather/Models/Weather+Units.swift b/Sources/HPOpenWeather/Models/Weather+Units.swift index 2f16e86..717e4fd 100644 --- a/Sources/HPOpenWeather/Models/Weather+Units.swift +++ b/Sources/HPOpenWeather/Models/Weather+Units.swift @@ -1,15 +1,15 @@ import Foundation -public extension Weather { +extension Weather { - /// The units that should the data in the API responses should be formatted in - enum Units: String, Codable { + /// The units that should the data in the API responses should be formatted in. + public enum Units: String, Codable { - /// Temperature in Kelvin and wind speed in meter/sec + /// Temperature in Kelvin and wind speed in meter/sec. case standard - /// Temperature in Celsius and wind speed in meter/sec + /// Temperature in Celsius and wind speed in meter/sec. case metric - /// Temperature in Fahrenheit and wind speed in miles/hour + /// Temperature in Fahrenheit and wind speed in miles/hour. case imperial @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) diff --git a/Sources/HPOpenWeather/Models/Weather.swift b/Sources/HPOpenWeather/Models/Weather.swift index 8c49025..621ef06 100644 --- a/Sources/HPOpenWeather/Models/Weather.swift +++ b/Sources/HPOpenWeather/Models/Weather.swift @@ -20,17 +20,17 @@ public struct Weather: Decodable, Equatable, Hashable { public let minutelyForecasts: [MinutelyForecast]? public let hourlyForecasts: [HourlyForecast]? public let dailyForecasts: [DailyForecast]? - /// Government weather alerts data from major national weather warning systems + /// Government weather alerts data from major national weather warning systems. public let alerts: [WeatherAlert]? - + public internal(set) var language: Weather.Language! public internal(set) var units: Weather.Units! - + // MARK: - Init public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + let timezoneIdentifier = try container.decode(String.self, forKey: .timezoneIdentifier) guard let timezone = TimeZone(identifier: timezoneIdentifier) else { throw OpenWeatherError.invalidTimeZoneIdentifier(timezoneIdentifier) diff --git a/Sources/HPOpenWeather/Models/WeatherAlert.swift b/Sources/HPOpenWeather/Models/WeatherAlert.swift index 93a68fc..9f0a9af 100644 --- a/Sources/HPOpenWeather/Models/WeatherAlert.swift +++ b/Sources/HPOpenWeather/Models/WeatherAlert.swift @@ -1,6 +1,6 @@ import Foundation -/// Type that holds information about weather alerts +/// Type that holds information about weather alerts. public struct WeatherAlert: Codable, Hashable, Equatable { // MARK: - Nested Types @@ -15,15 +15,15 @@ public struct WeatherAlert: Codable, Hashable, Equatable { // MARK: - Properties - /// Name of the alert source. Please read here the full list of alert sources + /// Name of the alert source. Please read here the full list of alert sources. public let senderName: String - /// Alert event name + /// Alert event name. public let eventName: String - //// Date and time of the start of the alert + //// Date and time of the start of the alert. public let startDate: Date - //// Date and time of the end of the alert + //// Date and time of the end of the alert. public let endDate: Date - /// Description of the alert + /// Description of the alert. public let description: String } diff --git a/Sources/HPOpenWeather/Models/WeatherCondition.swift b/Sources/HPOpenWeather/Models/WeatherCondition.swift index 7f95d66..7d714ee 100644 --- a/Sources/HPOpenWeather/Models/WeatherCondition.swift +++ b/Sources/HPOpenWeather/Models/WeatherCondition.swift @@ -1,15 +1,15 @@ import Foundation -/// Type that holds information about weather conditions +/// Type that holds information about weather conditions. public struct WeatherCondition: Codable, Equatable, Hashable { - /// The weather condition ID + /// The weather condition ID. public let id: Int - /// Group of weather parameters + /// Group of weather parameters. public let main: String - /// The weather condition within the group + /// The weather condition within the group. public let description: String - /// The ID of the corresponding weather icon + /// The ID of the corresponding weather icon. public let icon: WeatherIcon } diff --git a/Sources/HPOpenWeather/Models/WeatherIcon.swift b/Sources/HPOpenWeather/Models/WeatherIcon.swift index bbb1a14..f46b2bf 100644 --- a/Sources/HPOpenWeather/Models/WeatherIcon.swift +++ b/Sources/HPOpenWeather/Models/WeatherIcon.swift @@ -1,9 +1,10 @@ +import Foundation + #if canImport(UIKit) import UIKit #elseif canImport(AppKit) import AppKit #endif -import Foundation public enum WeatherIcon: String, Codable, CaseIterable { @@ -29,17 +30,17 @@ public enum WeatherIcon: String, Codable, CaseIterable { } @available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) -public extension WeatherIcon { +extension WeatherIcon { - var systemImageName: String { + public var systemImageName: String { makeIconName(filled: false) } - var systemImageNameFilled: String { + public var systemImageNameFilled: String { makeIconName(filled: true) } -#if canImport(UIKit) + #if canImport(UIKit) func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { UIImage(systemName: systemImageNameFilled, withConfiguration: configuration) @@ -49,7 +50,7 @@ public extension WeatherIcon { UIImage(systemName: systemImageName, withConfiguration: configuration) } -#elseif canImport(AppKit) + #elseif canImport(AppKit) func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { NSImage(systemSymbolName: systemImageNameFilled, accessibilityDescription: accessibilityDescription) @@ -59,7 +60,7 @@ public extension WeatherIcon { NSImage(systemSymbolName: systemImageName, accessibilityDescription: accessibilityDescription) } -#endif + #endif private func makeIconName(filled: Bool) -> String { let iconName: String diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index c6580b1..43ccee8 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -1,21 +1,21 @@ import CoreLocation import Foundation -/// A type to request current weather conditions and forecasts +/// A type to request current weather conditions and forecasts. public final class OpenWeather { // MARK: - Nested Types - /// Type that can be used to configure all settings at once + /// Type that can be used to configure all settings at once. public struct Settings { - /// The API key to use for weather requests - let apiKey : String - /// The language that will be used in weather responses + /// The API key to use for weather requests. + let apiKey: String + /// The language that will be used in weather responses. let language: Weather.Language - /// The units that will be used in weather responses + /// The units that will be used in weather responses. let units: Weather.Units - /// Initialises a new settings instance + /// Initialises a new settings instance. /// - Parameters: /// - apiKey: The API key to use for weather requests /// - language: The language that will be used in weather responses @@ -29,22 +29,22 @@ public final class OpenWeather { // MARK: - Properties - /// The OpenWeatherMap API key to authorize requests - public var apiKey : String? - /// The language that should be used in API responses + /// The OpenWeatherMap API key to authorize requests. + public var apiKey: String? + /// The language that should be used in API responses. public var language: Weather.Language = .english - /// The units that should be used to format the API responses + /// The units that should be used to format the API responses. public var units: Weather.Units = .metric // MARK: - Init - /// Initialised a new instance of `OpenWeather` and applies the specified API key + /// Initialised a new instance of `OpenWeather` and applies the specified API key. /// - Parameter apiKey: the API key to authenticate with the OpenWeatherMap API public init(apiKey: String? = nil) { self.apiKey = apiKey } - /// Initialised a new instance of `OpenWeather` and applies the specified settimgs + /// Initialised a new instance of `OpenWeather` and applies the specified settings. /// - Parameter settings: the settings to apply, including API key, language and units public init(settings: Settings) { self.apiKey = settings.apiKey @@ -54,25 +54,26 @@ public final class OpenWeather { // MARK: - Sending Requests - /// Sends the specified request to the OpenWeather API + /// Sends the specified request to the OpenWeather API. /// - Parameters: /// - coordinate: The coordinate for which the weather will be requested /// - excludedFields: An array specifying the fields that will be excluded from the response /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved /// - urlSession: The `URLSession` that will be used schedule requests /// - Returns: A weather response object + /// - Throws: If no API key was provided, the request was misconfigured, the networking failed or the response failed to decode public func weather( for coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil, urlSession: URLSession = .shared ) async throws -> Weather { - guard let apiKey else { + guard let apiKey, !apiKey.isEmpty else { throw OpenWeatherError.invalidAPIKey } let settings = Settings(apiKey: apiKey, language: language, units: units) - let request = WeatherRequest(coordinate: coordinate, excludedFields: excludedFields, date: date , settings: settings, version: .new) + let request = WeatherRequest(coordinate: coordinate, excludedFields: excludedFields, date: date, settings: settings, version: .new) return try await request.response(urlSession: urlSession).output } @@ -97,7 +98,7 @@ public final class OpenWeather { // MARK: - Applying Settings - /// Applies new settings to the weather client + /// Applies new settings to the weather client. /// - Parameter settings: The weather client settings, including an API key, language and units public func apply(_ settings: Settings) { apiKey = settings.apiKey diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index 0b3c952..2880619 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -1,5 +1,5 @@ -import Foundation import CoreLocation +import Foundation import HPNetwork import HPURLBuilder diff --git a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift index 9de6f90..9c82eb3 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift @@ -1,6 +1,6 @@ -import XCTest import HPNetwork import HPNetworkMock +import XCTest @testable import HPOpenWeather @@ -51,14 +51,19 @@ final class HPOpenWeatherTests: XCTestCase { let weather = try await request.response(urlSession: mockedURLSession).output let currentWeather = try XCTUnwrap(weather.currentWeather) - XCTAssertEqual(currentWeather.timestamp, Date(timeIntervalSince1970: 1713795125)) + XCTAssertEqual(currentWeather.timestamp, Date(timeIntervalSince1970: 1_713_795_125)) } // MARK: - Helpers private func make25WeatherResponse(version: WeatherRequest.Version) throws { let url = try XCTUnwrap(URL(string: "https://api.openweathermap.org/data/\(version.rawValue)/onecall")) - let jsonDataURL = try XCTUnwrap(Bundle.module.url(forResource: "\(version.rawValue.replacingOccurrences(of: ".", with: "-"))-test-response", withExtension: "json")) + let jsonDataURL = try XCTUnwrap( + Bundle.module.url( + forResource: "\(version.rawValue.replacingOccurrences(of: ".", with: "-"))-test-response", + withExtension: "json" + ) + ) let jsonData = try Data(contentsOf: jsonDataURL) _ = URLSessionMock.mockRequest(to: url, ignoresQuery: true) { _ in @@ -72,46 +77,46 @@ final class HPOpenWeatherTests: XCTestCase { } } -// func testTimeMachineRequestFailing() async throws { -// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) -// -// await HPAssertThrowsError { -// try await OpenWeather.shared.weatherResponse(request) -// } -// } -// -// func testTimeMachineRequest() async { -// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) -// -// await HPAssertThrowsNoError { -// try await OpenWeather.shared.weatherResponse(request) -// } -// } -// -// func testPublisher() { -// let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) -// -// let expectationFinished = expectation(description: "finished") -// let expectationReceive = expectation(description: "receiveValue") -// //let expectationFailure = expectation(description: "failure") -// -// let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( -// receiveCompletion: { result in -// switch result { -// case .failure(let error): -// XCTFail(error.localizedDescription) -// case .finished: -// expectationFinished.fulfill() -// } -// }, receiveValue: { response in -// expectationReceive.fulfill() -// } -// ) -// -// waitForExpectations(timeout: 10) { error in -// cancellable.cancel() -// } -// } + // func testTimeMachineRequestFailing() async throws { + // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) + // + // await HPAssertThrowsError { + // try await OpenWeather.shared.weatherResponse(request) + // } + // } + // + // func testTimeMachineRequest() async { + // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) + // + // await HPAssertThrowsNoError { + // try await OpenWeather.shared.weatherResponse(request) + // } + // } + // + // func testPublisher() { + // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) + // + // let expectationFinished = expectation(description: "finished") + // let expectationReceive = expectation(description: "receiveValue") + // //let expectationFailure = expectation(description: "failure") + // + // let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( + // receiveCompletion: { result in + // switch result { + // case .failure(let error): + // XCTFail(error.localizedDescription) + // case .finished: + // expectationFinished.fulfill() + // } + // }, receiveValue: { response in + // expectationReceive.fulfill() + // } + // ) + // + // waitForExpectations(timeout: 10) { error in + // cancellable.cancel() + // } + // } } diff --git a/Tests/HPOpenWeatherTests/WeatherIconTests.swift b/Tests/HPOpenWeatherTests/WeatherIconTests.swift index b72eb65..c279ded 100644 --- a/Tests/HPOpenWeatherTests/WeatherIconTests.swift +++ b/Tests/HPOpenWeatherTests/WeatherIconTests.swift @@ -1,15 +1,16 @@ import XCTest + @testable import HPOpenWeather final class WeatherIconTests: XCTestCase { - #if canImport(UIKit) - @available(iOS 13.0, tvOS 13.0, *) - func testAllSystemImages() { - WeatherIcon.allCases.forEach { - XCTAssertNotNil($0.filledUIImage()) - } - } - #endif + #if canImport(UIKit) + @available(iOS 13.0, tvOS 13.0, *) + func testAllSystemImages() { + WeatherIcon.allCases.forEach { + XCTAssertNotNil($0.filledUIImage()) + } + } + #endif } From 674c453b8b8ac2f83091837efab24b7551412a76 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 16:55:17 +0200 Subject: [PATCH 04/17] Clean up documentation Signed-off-by: Henrik Panhans --- Sources/HPOpenWeather/Models/DailyTemperature.swift | 2 +- .../HPOpenWeather/Models/Forecasts/ForecastBase.swift | 1 + Sources/HPOpenWeather/Models/Temperature.swift | 10 ++++++---- Sources/HPOpenWeather/Models/WeatherAlert.swift | 4 +++- Sources/HPOpenWeather/Models/Wind.swift | 11 ++++++----- Tests/HPOpenWeatherTests/WeatherIconTests.swift | 7 ++----- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Sources/HPOpenWeather/Models/DailyTemperature.swift b/Sources/HPOpenWeather/Models/DailyTemperature.swift index 143b453..893e3e2 100644 --- a/Sources/HPOpenWeather/Models/DailyTemperature.swift +++ b/Sources/HPOpenWeather/Models/DailyTemperature.swift @@ -1,6 +1,6 @@ import Foundation -/// Type that holds information about daily temperature changes +/// Type that holds information about daily temperature changes. public struct DailyTemperature: Codable, Equatable, Hashable { // MARK: - Nested Types diff --git a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift index 6418cd0..68d1eb9 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift @@ -9,6 +9,7 @@ public protocol ForecastBase: Decodable, Hashable { /// Humidity in percent. var humidity: Double? { get } /// Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. + /// /// Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit. var dewPoint: Double? { get } /// UV index. diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift index 90d57d0..662e7bb 100644 --- a/Sources/HPOpenWeather/Models/Temperature.swift +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -10,14 +10,16 @@ public struct Temperature: Equatable, Hashable { /// A convertible measurement of the actually measured temperature. /// - Parameter units: The units to use when formatting the `actual` property - public func actualMeasurement(unit: Weather.Units) -> Measurement { - Measurement(value: actual, unit: unit.temperatureUnit) + /// - Returns: a measurement in the provided unit + public func actualMeasurement(units: Weather.Units) -> Measurement { + Measurement(value: actual, unit: units.temperatureUnit) } /// A convertible measurement of how the actually measured temperature feels like. /// - Parameter units: The units to use when formatting the `feelsLike` property - public func feelsLikeMeasurement(unit: Weather.Units) -> Measurement { - Measurement(value: feelsLike, unit: unit.temperatureUnit) + /// - Returns: a measurement in the provided unit + public func feelsLikeMeasurement(units: Weather.Units) -> Measurement { + Measurement(value: feelsLike, unit: units.temperatureUnit) } } diff --git a/Sources/HPOpenWeather/Models/WeatherAlert.swift b/Sources/HPOpenWeather/Models/WeatherAlert.swift index 9f0a9af..987d3b2 100644 --- a/Sources/HPOpenWeather/Models/WeatherAlert.swift +++ b/Sources/HPOpenWeather/Models/WeatherAlert.swift @@ -15,7 +15,9 @@ public struct WeatherAlert: Codable, Hashable, Equatable { // MARK: - Properties - /// Name of the alert source. Please read here the full list of alert sources. + /// Name of the alert source. + /// + /// A full list of possible sources can be found [here](https://openweathermap.org/api/one-call-3#listsource) public let senderName: String /// Alert event name. public let eventName: String diff --git a/Sources/HPOpenWeather/Models/Wind.swift b/Sources/HPOpenWeather/Models/Wind.swift index c40e7f8..11148b9 100644 --- a/Sources/HPOpenWeather/Models/Wind.swift +++ b/Sources/HPOpenWeather/Models/Wind.swift @@ -1,17 +1,18 @@ import Foundation -/// Type that holds information about wind speed and direction measured in degrees +/// Type that holds information about wind speed and direction measured in degrees. public struct Wind: Codable, Equatable, Hashable { - /// The current wind speed depending on the request's unit (metric: meter/second, imperial: miles/hour) + /// The current wind speed depending on the request's unit (metric: meter/second, imperial: miles/hour). public let speed: Double? - /// Wind gust speed (metric: meter/sec, imperial: miles/hour) + /// Wind gust speed (metric: meter/sec, imperial: miles/hour). public let gust: Double? - /// The wind direction measured in degrees from North + /// The wind direction measured in degrees from North. public let degrees: Double? - /// A measurement of the `speed` property if existing, measured in the passed in units + /// A measurement of the `speed` property if existing, measured in the passed in units. /// - Parameter units: The units to use when formatting the `speed` property + /// - Returns: a measurement in the provided unit public func speedMeasurement(units: Weather.Units) -> Measurement? { guard let speed else { return nil diff --git a/Tests/HPOpenWeatherTests/WeatherIconTests.swift b/Tests/HPOpenWeatherTests/WeatherIconTests.swift index c279ded..caa350c 100644 --- a/Tests/HPOpenWeatherTests/WeatherIconTests.swift +++ b/Tests/HPOpenWeatherTests/WeatherIconTests.swift @@ -4,13 +4,10 @@ import XCTest final class WeatherIconTests: XCTestCase { - #if canImport(UIKit) - @available(iOS 13.0, tvOS 13.0, *) func testAllSystemImages() { - WeatherIcon.allCases.forEach { - XCTAssertNotNil($0.filledUIImage()) + for icon in WeatherIcon.allCases { + XCTAssertNotNil(icon.filledNSImage()) } } - #endif } From 948a107b9f762307fe4476287b35973293f78c5a Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 17:09:38 +0200 Subject: [PATCH 05/17] Decode API errors Signed-off-by: Henrik Panhans --- .../HPOpenWeather/OpenWeatherAPIError.swift | 23 +++++++++++++++++ .../Requests/WeatherRequest.swift | 25 +++++++++++++++---- .../HPOpenWeatherTests.swift | 18 +++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 Sources/HPOpenWeather/OpenWeatherAPIError.swift diff --git a/Sources/HPOpenWeather/OpenWeatherAPIError.swift b/Sources/HPOpenWeather/OpenWeatherAPIError.swift new file mode 100644 index 0000000..0acbea5 --- /dev/null +++ b/Sources/HPOpenWeather/OpenWeatherAPIError.swift @@ -0,0 +1,23 @@ +import Foundation + +/// An error that is thrown when the API returns an error. +public struct OpenWeatherAPIError: Error, Decodable { + + // MARK: - Nested Types + + enum CodingKeys: String, CodingKey { + case code = "cod" + case message + case parameters + } + + // MARK: - Properties + + /// The error code, such as 400, 404 or 5xx. + let code: Int + /// The error message or description. + let message: String + /// List of request parameters names that are related to this particular error. + let parameters: [String]? + +} diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index 2880619..f1f2297 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -30,7 +30,7 @@ struct WeatherRequest: DecodableRequest { return decoder } - // MARK: - OpenWeatherRequest + // MARK: - DecodableRequest func makeURL() throws -> URL { if let date = date, date < Date(), abs(date.timeIntervalSinceNow) <= 6 * .hour { @@ -53,10 +53,25 @@ struct WeatherRequest: DecodableRequest { } func convertResponse(data: Data, response: HTTPResponse) throws -> Weather { - var weather = try decoder.decode(Weather.self, from: data) - weather.units = settings.units - weather.language = settings.language - return weather + switch response.status.kind { + case .informational, .successful: + var weather = try decoder.decode(Weather.self, from: data) + weather.units = settings.units + weather.language = settings.language + return weather + case .clientError, .invalid, .redirection, .serverError: + var errorToThrow: any Error + do { + errorToThrow = try decoder.decode(OpenWeatherAPIError.self, from: data) + } catch { + errorToThrow = URLError(URLError.Code(rawValue: response.status.code)) + } + throw errorToThrow + } + } + + func validateResponse(_ response: HTTPResponse) throws { + // do nothing, validation will be handled by convertResponse instead } } diff --git a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift index 9c82eb3..84a2867 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift @@ -54,6 +54,24 @@ final class HPOpenWeatherTests: XCTestCase { XCTAssertEqual(currentWeather.timestamp, Date(timeIntervalSince1970: 1_713_795_125)) } + func testInvalidApiKey() async throws { + let settings = OpenWeather.Settings(apiKey: "debug") + let request = WeatherRequest( + coordinate: .init(latitude: 52.5200, longitude: 13.4050), + excludedFields: nil, + date: nil, + settings: settings, + version: .new + ) + + do { + _ = try await request.response(urlSession: .shared).output + } catch { + let apiError = try XCTUnwrap(error as? OpenWeatherAPIError) + XCTAssertEqual(apiError.code, 401) + } + } + // MARK: - Helpers private func make25WeatherResponse(version: WeatherRequest.Version) throws { From 2fcd95145e6c1e9811a5c2371b33ed4137499848 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 17:35:45 +0200 Subject: [PATCH 06/17] Lots of cleanup Signed-off-by: Henrik Panhans --- .../Models/Forecasts/CurrentWeather.swift | 1 + .../HPOpenWeather/Models/WeatherIcon.swift | 8 +- Sources/HPOpenWeather/OpenWeather.swift | 68 ++++++++++----- .../HPOpenWeather/OpenWeatherAPIError.swift | 6 +- Sources/HPOpenWeather/OpenWeatherError.swift | 4 +- .../Requests/WeatherRequest.swift | 17 ++-- ...therTests.swift => OpenWeatherTests.swift} | 87 ++++++------------- Tests/HPOpenWeatherTests/TestSecret.swift | 7 -- 8 files changed, 86 insertions(+), 112 deletions(-) rename Tests/HPOpenWeatherTests/{HPOpenWeatherTests.swift => OpenWeatherTests.swift} (54%) delete mode 100644 Tests/HPOpenWeatherTests/TestSecret.swift diff --git a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift index b400bcb..de68815 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift @@ -1,5 +1,6 @@ import Foundation +/// A type containing information about the current weather. public struct CurrentWeather: ForecastBase, SunForecast { // MARK: - Coding Keys diff --git a/Sources/HPOpenWeather/Models/WeatherIcon.swift b/Sources/HPOpenWeather/Models/WeatherIcon.swift index f46b2bf..39d8e0a 100644 --- a/Sources/HPOpenWeather/Models/WeatherIcon.swift +++ b/Sources/HPOpenWeather/Models/WeatherIcon.swift @@ -42,21 +42,21 @@ extension WeatherIcon { #if canImport(UIKit) - func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + public func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { UIImage(systemName: systemImageNameFilled, withConfiguration: configuration) } - func outlineUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + public func outlineUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { UIImage(systemName: systemImageName, withConfiguration: configuration) } #elseif canImport(AppKit) - func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { + public func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { NSImage(systemSymbolName: systemImageNameFilled, accessibilityDescription: accessibilityDescription) } - func outlineNSImage(accessibilityDescription: String? = nil) -> NSImage? { + public func outlineNSImage(accessibilityDescription: String? = nil) -> NSImage? { NSImage(systemSymbolName: systemImageName, accessibilityDescription: accessibilityDescription) } diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index 43ccee8..4fe4d87 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -27,6 +27,12 @@ public final class OpenWeather { } } + public enum APIVersion: String { + @available(*, deprecated, message: "The OpenWeather One Call API 2.5 will be deprecated in June 2024") + case twoPointFive = "2.5" + case threePointZero = "3.0" + } + // MARK: - Properties /// The OpenWeatherMap API key to authorize requests. @@ -54,11 +60,12 @@ public final class OpenWeather { // MARK: - Sending Requests - /// Sends the specified request to the OpenWeather API. + /// Requests a weather forecast for the specified location. /// - Parameters: /// - coordinate: The coordinate for which the weather will be requested /// - excludedFields: An array specifying the fields that will be excluded from the response /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved + /// - version: The API version to use. Default is 3.0. /// - urlSession: The `URLSession` that will be used schedule requests /// - Returns: A weather response object /// - Throws: If no API key was provided, the request was misconfigured, the networking failed or the response failed to decode @@ -66,6 +73,7 @@ public final class OpenWeather { for coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil, + version: APIVersion = .threePointZero, urlSession: URLSession = .shared ) async throws -> Weather { guard let apiKey, !apiKey.isEmpty else { @@ -73,37 +81,49 @@ public final class OpenWeather { } let settings = Settings(apiKey: apiKey, language: language, units: units) - let request = WeatherRequest(coordinate: coordinate, excludedFields: excludedFields, date: date, settings: settings, version: .new) + let request = WeatherRequest( + coordinate: coordinate, + excludedFields: excludedFields, + date: date, + settings: settings, + version: version + ) return try await request.response(urlSession: urlSession).output } - /// Sends the specified request to the OpenWeather API + /// Requests a weather forecast for the specified location. /// - Parameters: - /// - request: The request object that holds information about request location, date, etc. + /// - coordinate: The coordinate for which the weather will be requested + /// - excludedFields: An array specifying the fields that will be excluded from the response + /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved + /// - version: The API version to use. Default is 3.0. /// - urlSession: The `URLSession` that will be used schedule requests /// - completion: A completion that will be called with the result of the network request /// - Returns: A network task that can be used to cancel the request - // @discardableResult - // public func schedule(_ request: WeatherRequest, urlSession: URLSession = .shared, completion: @escaping (Result) -> Void) -> Task { - // Task { - // do { - // let response = try await weatherResponse(request, urlSession: urlSession) - // completion(.success(response)) - // } catch { - // completion(.failure(error)) - // } - // } - // } - - // MARK: - Applying Settings - - /// Applies new settings to the weather client. - /// - Parameter settings: The weather client settings, including an API key, language and units - public func apply(_ settings: Settings) { - apiKey = settings.apiKey - language = settings.language - units = settings.units + @discardableResult + public func requestWeather( + for coordinate: CLLocationCoordinate2D, + excludedFields: [ExcludableField]? = nil, + date: Date? = nil, + version: APIVersion = .threePointZero, + urlSession: URLSession = .shared, + completion: @escaping (Result) -> Void + ) -> Task { + Task { + do { + let response = try await weather( + for: coordinate, + excludedFields: excludedFields, + date: date, + version: version, + urlSession: urlSession + ) + completion(.success(response)) + } catch { + completion(.failure(error)) + } + } } } diff --git a/Sources/HPOpenWeather/OpenWeatherAPIError.swift b/Sources/HPOpenWeather/OpenWeatherAPIError.swift index 0acbea5..ac40104 100644 --- a/Sources/HPOpenWeather/OpenWeatherAPIError.swift +++ b/Sources/HPOpenWeather/OpenWeatherAPIError.swift @@ -14,10 +14,10 @@ public struct OpenWeatherAPIError: Error, Decodable { // MARK: - Properties /// The error code, such as 400, 404 or 5xx. - let code: Int + public let code: Int /// The error message or description. - let message: String + public let message: String /// List of request parameters names that are related to this particular error. - let parameters: [String]? + public let parameters: [String]? } diff --git a/Sources/HPOpenWeather/OpenWeatherError.swift b/Sources/HPOpenWeather/OpenWeatherError.swift index efc69a3..53964e8 100644 --- a/Sources/HPOpenWeather/OpenWeatherError.swift +++ b/Sources/HPOpenWeather/OpenWeatherError.swift @@ -1,7 +1,7 @@ import Foundation -public enum OpenWeatherError: Error { - case invalidTimeMachineDate +public enum OpenWeatherError: Error, Equatable { + case invalidRequestTimestamp case invalidAPIKey case noCurrentConditionReturned case invalidTimeZoneIdentifier(_ identifier: String) diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index f1f2297..ed026c1 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -9,18 +9,13 @@ struct WeatherRequest: DecodableRequest { typealias Output = Weather - enum Version: String { - case old = "2.5" - case new = "3.0" - } - // MARK: - Properties let coordinate: CLLocationCoordinate2D let excludedFields: [ExcludableField]? let date: Date? let settings: OpenWeather.Settings - let version: Version + let version: OpenWeather.APIVersion let requestMethod: HTTPRequest.Method = .get @@ -33,8 +28,8 @@ struct WeatherRequest: DecodableRequest { // MARK: - DecodableRequest func makeURL() throws -> URL { - if let date = date, date < Date(), abs(date.timeIntervalSinceNow) <= 6 * .hour { - throw OpenWeatherError.invalidTimeMachineDate + if let date, Date.now.addingTimeInterval(.day * 4) < date { + throw OpenWeatherError.invalidRequestTimestamp } return try URL.buildThrowing { Host("api.openweathermap.org") @@ -47,7 +42,7 @@ struct WeatherRequest: DecodableRequest { QueryItem(name: "appid", value: settings.apiKey) QueryItem(name: "dt", value: date.flatMap({ Int($0.timeIntervalSince1970) })) QueryItem(name: "exclude", value: excludedFields?.compactMap({ $0.rawValue })) - QueryItem(name: "units", value: "metric") + QueryItem(name: "units", value: settings.units.rawValue) QueryItem(name: "lang", value: settings.language.rawValue) } } @@ -78,6 +73,8 @@ struct WeatherRequest: DecodableRequest { extension TimeInterval { - static let hour = 3600.00 + static let minute = 60.0 + static let hour = minute * 60 + static let day = hour * 24 } diff --git a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift b/Tests/HPOpenWeatherTests/OpenWeatherTests.swift similarity index 54% rename from Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift rename to Tests/HPOpenWeatherTests/OpenWeatherTests.swift index 84a2867..4c7a3bc 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/OpenWeatherTests.swift @@ -4,7 +4,7 @@ import XCTest @testable import HPOpenWeather -final class HPOpenWeatherTests: XCTestCase { +final class OpenWeatherTests: XCTestCase { // MARK: - Properties @@ -24,7 +24,7 @@ final class HPOpenWeatherTests: XCTestCase { // MARK: - Tests func testOldApiRequest() async throws { - try make25WeatherResponse(version: .old) + try make25WeatherResponse(version: .twoPointFive) let settings = OpenWeather.Settings(apiKey: "debug") let request = WeatherRequest( @@ -32,13 +32,13 @@ final class HPOpenWeatherTests: XCTestCase { excludedFields: nil, date: nil, settings: settings, - version: .old + version: .twoPointFive ) _ = try await request.response(urlSession: mockedURLSession) } func testNewApiRequest() async throws { - try make25WeatherResponse(version: .new) + try make25WeatherResponse(version: .threePointZero) let settings = OpenWeather.Settings(apiKey: "debug") let request = WeatherRequest( @@ -46,7 +46,7 @@ final class HPOpenWeatherTests: XCTestCase { excludedFields: nil, date: nil, settings: settings, - version: .new + version: .threePointZero ) let weather = try await request.response(urlSession: mockedURLSession).output @@ -61,7 +61,7 @@ final class HPOpenWeatherTests: XCTestCase { excludedFields: nil, date: nil, settings: settings, - version: .new + version: .threePointZero ) do { @@ -72,9 +72,27 @@ final class HPOpenWeatherTests: XCTestCase { } } + func testInvalidTimeMachineTimestamp() async throws { + let settings = OpenWeather.Settings(apiKey: "debug") + let request = WeatherRequest( + coordinate: .init(latitude: 52.5200, longitude: 13.4050), + excludedFields: nil, + date: Date.distantFuture, + settings: settings, + version: .threePointZero + ) + + do { + _ = try await request.response(urlSession: .shared).output + } catch { + let apiError = try XCTUnwrap(error as? OpenWeatherError) + XCTAssertEqual(apiError, .invalidRequestTimestamp) + } + } + // MARK: - Helpers - private func make25WeatherResponse(version: WeatherRequest.Version) throws { + private func make25WeatherResponse(version: OpenWeather.APIVersion) throws { let url = try XCTUnwrap(URL(string: "https://api.openweathermap.org/data/\(version.rawValue)/onecall")) let jsonDataURL = try XCTUnwrap( Bundle.module.url( @@ -95,59 +113,4 @@ final class HPOpenWeatherTests: XCTestCase { } } - // func testTimeMachineRequestFailing() async throws { - // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) - // - // await HPAssertThrowsError { - // try await OpenWeather.shared.weatherResponse(request) - // } - // } - // - // func testTimeMachineRequest() async { - // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) - // - // await HPAssertThrowsNoError { - // try await OpenWeather.shared.weatherResponse(request) - // } - // } - // - // func testPublisher() { - // let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) - // - // let expectationFinished = expectation(description: "finished") - // let expectationReceive = expectation(description: "receiveValue") - // //let expectationFailure = expectation(description: "failure") - // - // let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( - // receiveCompletion: { result in - // switch result { - // case .failure(let error): - // XCTFail(error.localizedDescription) - // case .finished: - // expectationFinished.fulfill() - // } - // }, receiveValue: { response in - // expectationReceive.fulfill() - // } - // ) - // - // waitForExpectations(timeout: 10) { error in - // cancellable.cancel() - // } - // } - -} - -extension Encodable { - - func encodeAndDecode(type: T.Type) throws -> T { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = .secondsSince1970 - let encodedData = try jsonEncoder.encode(self) - - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .secondsSince1970 - return try jsonDecoder.decode(type.self, from: encodedData) - } - } diff --git a/Tests/HPOpenWeatherTests/TestSecret.swift b/Tests/HPOpenWeatherTests/TestSecret.swift deleted file mode 100644 index 034351e..0000000 --- a/Tests/HPOpenWeatherTests/TestSecret.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -enum TestSecret { - - static let apiKey: String = ProcessInfo.processInfo.environment["API_KEY"] ?? "" - -} From 1661f9639d68a09c5c9047bb99f9ba2e5a3366ec Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 22:17:58 +0200 Subject: [PATCH 07/17] A bunch of fixes and more CI stuff Signed-off-by: Henrik Panhans --- .github/workflows/documentation.yml | 54 +++++++++++--------- .github/workflows/prettier.yml | 35 +++++++++++++ .github/workflows/swift.yml | 79 ++++++++++++++++++++++------- .gitignore | 2 + HPOpenWeather.podspec | 32 ------------ Package.resolved | 60 +++++++++++----------- Scripts/build-docc-archive | 4 +- 7 files changed, 160 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/prettier.yml delete mode 100644 HPOpenWeather.podspec diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 2cb9a5c..d20dd7c 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,14 +1,12 @@ -name: Pages Deploy +name: Documentation on: push: - branches: [ main ] - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write + branches: ["main"] + paths: + - "Sources/**/*.swift" + - "Tests/**/*.swift" + - "**/*.md" # Allow one concurrent deployment concurrency: @@ -16,29 +14,35 @@ concurrency: cancel-in-progress: true jobs: - # Single deploy job since we're just deploying - deploy: + deploy-pages: + name: Deploy Documentation to GitHub Pages + runs-on: macos-14 environment: - # Must be set to this for deploying to GitHub Pages name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: macos-12 + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source steps: - - name: Checkout 🛎️ - uses: actions/checkout@v3 + - name: Configure Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Checkout Code + uses: actions/checkout@v4 + - name: Cache SPM dependencies + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- - name: Build DocC - run: | - swift package --allow-writing-to-directory ./docs \ - generate-documentation --target HPOpenWeather \ - --transform-for-static-hosting \ - --hosting-base-path HPOpenWeather \ - --output-path ./docs - echo "" > docs/index.html + run: Scripts/build-docc-archive - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: - # Upload only docs directory - path: 'docs' + path: ${{ runner.temp }}/docs - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 0000000..f529368 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,35 @@ +name: Prettier + +on: + push: + branches: ["main"] + paths: + - "**/*.js" + - "**/*.yml" + - "**/*.md" + pull_request: + branches: [main] + paths: + - "**/*.js" + - "**/*.yml" + - "**/*.md" + +jobs: + prettier: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Make sure the actual branch is checked out when running on pull requests + ref: ${{ github.head_ref }} + # This is important to fetch the changes to the previous commit + fetch-depth: 0 + + - name: Prettify code + uses: creyD/prettier_action@v4.3 + with: + # This part is also where you can pass other options, for example: + prettier_options: --write **/*.{js,md,yml} --config config/prettier.config.js --ignore-path config/.prettierignore + only_changed: True diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 123af42..5df0065 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -2,25 +2,70 @@ name: Swift on: push: - branches: [ main ] + branches: [main] + paths: + - "Sources/**/*.swift" + - "Tests/**/*.swift" pull_request: - branches: [ main ] + branches: [main] + paths: + - "Sources/**/*.swift" + - "Tests/**/*.swift" jobs: - build: - - runs-on: macos-latest - + test-swift: + name: Test Swift Code + runs-on: macos-14 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - uses: actions/checkout@v2 - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v - env: + - name: Configure Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Checkout Code + uses: actions/checkout@v4 + - name: Cache SPM dependencies + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + - name: Build + run: swift build -v + - name: Run tests + run: swift test --enable-code-coverage -v + env: API_KEY: ${{ secrets.API_KEY }} - - name: Codecov - uses: codecov/codecov-action@v2 \ No newline at end of file + - name: Convert coverage report + continue-on-error: true + run: Scripts/convert-coverage-report --target HPOpenWeatherPackageTests + - name: Upload coverage reports to Codecov + continue-on-error: true + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: henrik-dmg/HPOpenWeather + + lint-code: + name: Lint Swift Code + runs-on: macos-14 + steps: + - name: Configure Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Checkout Code + uses: actions/checkout@v4 + - name: Cache SPM dependencies + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + - name: Install SwiftLint + run: brew install swift-format peripheryapp/periphery/periphery + - name: Lint code + run: Scripts/lint-swift-code + - name: Scan for dead code + run: periphery scan --strict --config config/periphery.yml diff --git a/.gitignore b/.gitignore index c4c0743..1e17051 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,5 @@ fastlane/test_output iOSInjectionProject/ .swiftpm settings.json +/docs +/coverage diff --git a/HPOpenWeather.podspec b/HPOpenWeather.podspec deleted file mode 100644 index e1044ce..0000000 --- a/HPOpenWeather.podspec +++ /dev/null @@ -1,32 +0,0 @@ -Pod::Spec.new do |s| - - s.name = "HPOpenWeather" - s.version = "5.0.0" - s.summary = "Cross-platform framework to communicate with the OpenWeatherMap JSON API" - - s.license = { :type => "MIT", :file => "LICENSE.md" } - s.homepage = "https://panhans.dev/opensource/hpopenweather" - - s.author = { "henrik-dmg" => "henrik@panhans.dev" } - s.social_media_url = "https://twitter.com/henrik_dmg" - - s.ios.deployment_target = "13.0" - s.watchos.deployment_target = "7.0" - s.tvos.deployment_target = "13.0" - s.osx.deployment_target = "10.15" - - s.source = { :git => 'https://github.com/henrik-dmg/HPOpenWeather.git', :tag => s.version } - - s.source_files = "Sources/**/*.swift" - - s.framework = "Foundation" - s.ios.framework = "UIKit" - s.watchos.framework = "UIKit" - s.tvos.framework = "UIKit" - - s.swift_version = "5.5" - s.requires_arc = true - s.dependency "HPNetwork", "~> 3.1.1" - s.dependency "HPURLBuilder", "~> 1.0.0" - -end diff --git a/Package.resolved b/Package.resolved index b4e3224..cf9bead 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,34 +1,32 @@ { - "object": { - "pins": [ - { - "package": "HPNetwork", - "repositoryURL": "https://github.com/henrik-dmg/HPNetwork", - "state": { - "branch": null, - "revision": "6e1266a06692dab7df6d57faffaee59f2b46c4f5", - "version": "4.0.0" - } - }, - { - "package": "HPURLBuilder", - "repositoryURL": "https://github.com/henrik-dmg/HPURLBuilder", - "state": { - "branch": null, - "revision": "49ad1fb6f10914e7134dc9f8e5c21f48a52e3d37", - "version": "1.1.0" - } - }, - { - "package": "swift-http-types", - "repositoryURL": "https://github.com/apple/swift-http-types.git", - "state": { - "branch": null, - "revision": "12358d55a3824bd5fed310b999ea8cf83a9a1a65", - "version": "1.0.3" - } + "pins" : [ + { + "identity" : "hpnetwork", + "kind" : "remoteSourceControl", + "location" : "https://github.com/henrik-dmg/HPNetwork", + "state" : { + "revision" : "6e1266a06692dab7df6d57faffaee59f2b46c4f5", + "version" : "4.0.0" } - ] - }, - "version": 1 + }, + { + "identity" : "hpurlbuilder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/henrik-dmg/HPURLBuilder", + "state" : { + "revision" : "49ad1fb6f10914e7134dc9f8e5c21f48a52e3d37", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" + } + } + ], + "version" : 2 } diff --git a/Scripts/build-docc-archive b/Scripts/build-docc-archive index 6842dc0..6d4216b 100755 --- a/Scripts/build-docc-archive +++ b/Scripts/build-docc-archive @@ -7,12 +7,14 @@ fi # First, insert docc-plugin dependency. This is very hacky, but it avoids everyone having to pull in the docc-plugin when they use this library. -sed '/swift-http-types.git/a\ +sed '/Dependencies declare/a\ .package(url: "https://github.com/apple/swift-docc-plugin", branch: "main"),\ ' "Package.swift" > "Package.tmp" mv "Package.tmp" "Package.swift" +swift package resolve + swift package \ --allow-writing-to-directory "$RUNNER_TEMP/docs" \ generate-documentation \ From 3f5460b99f6ba4d83b1c55912a12e3188bd32d67 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 23:47:40 +0200 Subject: [PATCH 08/17] Some cleanup Signed-off-by: Henrik Panhans --- CODE_OF_CONDUCT.md | 26 ++--- Package.swift | 2 +- README.md | 45 ++++----- .../HPOpenWeather/Models/Temperature.swift | 4 +- .../Models/Weather+Language.swift | 98 +++++++++---------- .../HPOpenWeather/Models/Weather+Units.swift | 60 ++++++------ Sources/HPOpenWeather/OpenWeather.swift | 10 +- Sources/HPOpenWeather/OpenWeatherError.swift | 17 +++- 8 files changed, 129 insertions(+), 133 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 77b84c7..e24371c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/Package.swift b/Package.swift index 4b8ffc2..65a130e 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "HPOpenWeather", platforms: [ - .iOS(.v15), .tvOS(.v15), .watchOS(.v7), .macOS(.v12), + .iOS(.v15), .tvOS(.v15), .watchOS(.v6), .macOS(.v12), ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. diff --git a/README.md b/README.md index 4dd798e..b40c7a7 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,17 @@ HPOpenWeather is a cross-platform Swift framework to communicate with the OpenWe ## Installation -HPOpenWeather supports iOS 13.0+, watchOS 7.0+, tvOS 13.0+ and macOS 10.15+. +HPOpenWeather supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. ### SPM -Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "5.0.0")` to your `Package.swift` file - -### CocoaPods - -Add `pod 'HPOpenWeather'` to your `Podfile` and run `pod install` +Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "6.0.0")` to your `Package.swift` file ## Usage -To get started, you need an API key from [OpenWeather](https://openweathermap.org). Put this API key in the initialiser, additionally you can also specify a custom temperature format and/or language used in the responses (see list for available languages and units below). +### Configuration + +To get started, you need an API key from [OpenWeather](https://openweathermap.org). Configure the `OpenWeather.shared` singleton or create your own instance and configure it with your key and other settings. ```swift import HPOpenWeather @@ -33,42 +31,33 @@ OpenWeather.shared.units = .metric // Or use options let settings = OpenWeather.Settings(apiKey: "yourAPIKey", language: .german, units: .metric) -OpenWeather.shared.apply(settings) +let openWeather = OpenWeather(settings: settings) ``` You can also customise the response data units and language by accessing the `language` and `units` propertis. -### Making a request - -To make a request, initialize a new request object like this +### Retrieving Weather Information -```swift -let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30)) -``` +To fetch the weather, there are two options: async/await or callback. Both expect a `CLLocationCoordinate2D` for which to fetch the weather. +Additionally, you can specify which fields should be excluded from the response to save bandwidth, or specify a historic date or a date up to 4 days in the future. -Or to request weather data from the past: +#### Async ```swift -let timemachineRequest = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30), date: someDate) +let weather = try await OpenWeather.shared.weather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) ``` -**Note:** the date has to be at least 6 hours in the past - -To post a request, call `sendWeatherRequest` on `OpenWeather`: +#### Callback ```swift -// Classic completion handler approach -OpenWeather.shared.schedule(request) { result in - switch result { - case .success(let response): - // do something with weather data here +OpenWeather.shared.requestWeather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) { result in + switch result { + case .success(let weather): + print(weather) case .failure(let error): - // handle error + print(error) } } - -// Or using the new concurrency features -let response = try await OpenWeather.shared.weatherResponse(request) ``` ### Available languages (default in bold) diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift index 662e7bb..9a1b670 100644 --- a/Sources/HPOpenWeather/Models/Temperature.swift +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -11,14 +11,14 @@ public struct Temperature: Equatable, Hashable { /// A convertible measurement of the actually measured temperature. /// - Parameter units: The units to use when formatting the `actual` property /// - Returns: a measurement in the provided unit - public func actualMeasurement(units: Weather.Units) -> Measurement { + public func actualMeasurement(units: WeatherUnits) -> Measurement { Measurement(value: actual, unit: units.temperatureUnit) } /// A convertible measurement of how the actually measured temperature feels like. /// - Parameter units: The units to use when formatting the `feelsLike` property /// - Returns: a measurement in the provided unit - public func feelsLikeMeasurement(units: Weather.Units) -> Measurement { + public func feelsLikeMeasurement(units: WeatherUnits) -> Measurement { Measurement(value: feelsLike, unit: units.temperatureUnit) } diff --git a/Sources/HPOpenWeather/Models/Weather+Language.swift b/Sources/HPOpenWeather/Models/Weather+Language.swift index 38b8df5..f509186 100644 --- a/Sources/HPOpenWeather/Models/Weather+Language.swift +++ b/Sources/HPOpenWeather/Models/Weather+Language.swift @@ -1,54 +1,50 @@ import Foundation -extension Weather { - - /// The language that should be used in API responses for example for weather condition descriptions. - public enum Language: String, Codable { - case afrikaans = "af" - case arabic = "ar" - case azerbaijani = "az" - case bulgarian = "bg" - case catalan = "ca" - case czech = "cz" - case danish = "da" - case german = "de" - case greek = "el" - case english = "en" - case basque = "eu" - case persian = "fa" - case finnish = "fi" - case french = "fr" - case galician = "gl" - case hebrew = "he" - case hindi = "hi" - case croatian = "hr" - case hungarian = "hu" - case indonesian = "id" - case italian = "it" - case japanese = "ja" - case korean = "kr" - case latvian = "la" - case lithuanian = "lt" - case macedonian = "mk" - case norwegian = "no" - case dutch = "nl" - case polish = "pl" - case portuguese = "pt" - case portugueseBrasil = "pt_br" - case romanian = "ro" - case russian = "ru" - case swedish = "sv" - case slovak = "sk" - case slovenian = "sl" - case spanish = "es" - case serbian = "sr" - case thai = "th" - case turkish = "tr" - case ukrainian = "ua" - case vietnamese = "vi" - case chineseSimplified = "zh_cn" - case chineseTraditional = "zh_tw" - case zulu = "zu" - } - +/// The language that should be used in API responses for example for weather condition descriptions. +public enum WeatherLanguage: String, Codable { + case afrikaans = "af" + case arabic = "ar" + case azerbaijani = "az" + case bulgarian = "bg" + case catalan = "ca" + case czech = "cz" + case danish = "da" + case german = "de" + case greek = "el" + case english = "en" + case basque = "eu" + case persian = "fa" + case finnish = "fi" + case french = "fr" + case galician = "gl" + case hebrew = "he" + case hindi = "hi" + case croatian = "hr" + case hungarian = "hu" + case indonesian = "id" + case italian = "it" + case japanese = "ja" + case korean = "kr" + case latvian = "la" + case lithuanian = "lt" + case macedonian = "mk" + case norwegian = "no" + case dutch = "nl" + case polish = "pl" + case portuguese = "pt" + case portugueseBrasil = "pt_br" + case romanian = "ro" + case russian = "ru" + case swedish = "sv" + case slovak = "sk" + case slovenian = "sl" + case spanish = "es" + case serbian = "sr" + case thai = "th" + case turkish = "tr" + case ukrainian = "ua" + case vietnamese = "vi" + case chineseSimplified = "zh_cn" + case chineseTraditional = "zh_tw" + case zulu = "zu" } diff --git a/Sources/HPOpenWeather/Models/Weather+Units.swift b/Sources/HPOpenWeather/Models/Weather+Units.swift index 717e4fd..c2cf069 100644 --- a/Sources/HPOpenWeather/Models/Weather+Units.swift +++ b/Sources/HPOpenWeather/Models/Weather+Units.swift @@ -1,41 +1,37 @@ import Foundation -extension Weather { +/// The units that should the data in the API responses should be formatted in. +public enum WeatherUnits: String, Codable { - /// The units that should the data in the API responses should be formatted in. - public enum Units: String, Codable { + /// Temperature in Kelvin and wind speed in meter/sec. + case standard + /// Temperature in Celsius and wind speed in meter/sec. + case metric + /// Temperature in Fahrenheit and wind speed in miles/hour. + case imperial - /// Temperature in Kelvin and wind speed in meter/sec. - case standard - /// Temperature in Celsius and wind speed in meter/sec. - case metric - /// Temperature in Fahrenheit and wind speed in miles/hour. - case imperial - - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - var temperatureUnit: UnitTemperature { - switch self { - case .standard: - return .kelvin - case .metric: - return .celsius - case .imperial: - return .fahrenheit - } + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + var temperatureUnit: UnitTemperature { + switch self { + case .standard: + return .kelvin + case .metric: + return .celsius + case .imperial: + return .fahrenheit } + } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - var windSpeedUnit: UnitSpeed { - switch self { - case .standard: - return .metersPerSecond - case .metric: - return .metersPerSecond - case .imperial: - return .milesPerHour - } + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + var windSpeedUnit: UnitSpeed { + switch self { + case .standard: + return .metersPerSecond + case .metric: + return .metersPerSecond + case .imperial: + return .milesPerHour } - } -} + } diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index 4fe4d87..e94f375 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -11,16 +11,16 @@ public final class OpenWeather { /// The API key to use for weather requests. let apiKey: String /// The language that will be used in weather responses. - let language: Weather.Language + let language: WeatherLanguage /// The units that will be used in weather responses. - let units: Weather.Units + let units: WeatherUnits /// Initialises a new settings instance. /// - Parameters: /// - apiKey: The API key to use for weather requests /// - language: The language that will be used in weather responses /// - units: The units that will be used in weather responses - public init(apiKey: String, language: Weather.Language = .english, units: Weather.Units = .metric) { + public init(apiKey: String, language: WeatherLanguage = .english, units: WeatherUnits = .metric) { self.language = language self.units = units self.apiKey = apiKey @@ -38,9 +38,9 @@ public final class OpenWeather { /// The OpenWeatherMap API key to authorize requests. public var apiKey: String? /// The language that should be used in API responses. - public var language: Weather.Language = .english + public var language: WeatherLanguage = .english /// The units that should be used to format the API responses. - public var units: Weather.Units = .metric + public var units: WeatherUnits = .metric // MARK: - Init diff --git a/Sources/HPOpenWeather/OpenWeatherError.swift b/Sources/HPOpenWeather/OpenWeatherError.swift index 53964e8..4a49b78 100644 --- a/Sources/HPOpenWeather/OpenWeatherError.swift +++ b/Sources/HPOpenWeather/OpenWeatherError.swift @@ -1,8 +1,23 @@ import Foundation -public enum OpenWeatherError: Error, Equatable { +public enum OpenWeatherError: LocalizedError, Equatable { + case invalidRequestTimestamp case invalidAPIKey case noCurrentConditionReturned case invalidTimeZoneIdentifier(_ identifier: String) + + public var errorDescription: String? { + switch self { + case .invalidRequestTimestamp: + return "The request timestamp is invalid" + case .invalidAPIKey: + return "The API key is missing or empty" + case .noCurrentConditionReturned: + return "No current condition was returned" + case .invalidTimeZoneIdentifier(let identifier): + return "The timezone identifier '\(identifier)' is invalid" + } + } + } From eaa64951e4c40889f39b577d38aa873ee2a41d36 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 22 Apr 2024 23:48:51 +0200 Subject: [PATCH 09/17] fix compilation Signed-off-by: Henrik Panhans --- Sources/HPOpenWeather/Models/Weather+Units.swift | 2 +- Sources/HPOpenWeather/Models/Weather.swift | 4 ++-- Sources/HPOpenWeather/Models/Wind.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/HPOpenWeather/Models/Weather+Units.swift b/Sources/HPOpenWeather/Models/Weather+Units.swift index c2cf069..3ba94b3 100644 --- a/Sources/HPOpenWeather/Models/Weather+Units.swift +++ b/Sources/HPOpenWeather/Models/Weather+Units.swift @@ -34,4 +34,4 @@ public enum WeatherUnits: String, Codable { } } - } +} diff --git a/Sources/HPOpenWeather/Models/Weather.swift b/Sources/HPOpenWeather/Models/Weather.swift index 621ef06..a746978 100644 --- a/Sources/HPOpenWeather/Models/Weather.swift +++ b/Sources/HPOpenWeather/Models/Weather.swift @@ -23,8 +23,8 @@ public struct Weather: Decodable, Equatable, Hashable { /// Government weather alerts data from major national weather warning systems. public let alerts: [WeatherAlert]? - public internal(set) var language: Weather.Language! - public internal(set) var units: Weather.Units! + public internal(set) var language: WeatherLanguage! + public internal(set) var units: WeatherUnits! // MARK: - Init diff --git a/Sources/HPOpenWeather/Models/Wind.swift b/Sources/HPOpenWeather/Models/Wind.swift index 11148b9..799a053 100644 --- a/Sources/HPOpenWeather/Models/Wind.swift +++ b/Sources/HPOpenWeather/Models/Wind.swift @@ -13,7 +13,7 @@ public struct Wind: Codable, Equatable, Hashable { /// A measurement of the `speed` property if existing, measured in the passed in units. /// - Parameter units: The units to use when formatting the `speed` property /// - Returns: a measurement in the provided unit - public func speedMeasurement(units: Weather.Units) -> Measurement? { + public func speedMeasurement(units: WeatherUnits) -> Measurement? { guard let speed else { return nil } From dcd0ca4aa36cb591526b68c4b1eadc6c0f1712ed Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 23 Apr 2024 00:12:47 +0200 Subject: [PATCH 10/17] Fix coverage reporting Signed-off-by: Henrik Panhans --- Scripts/convert-coverage-report | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Scripts/convert-coverage-report b/Scripts/convert-coverage-report index 26963df..676c41c 100755 --- a/Scripts/convert-coverage-report +++ b/Scripts/convert-coverage-report @@ -30,12 +30,12 @@ if [ -z "$TARGET" ]; then fi INSTR_PROFILE=$(find $BUILD_PATH -name "*.profdata") -TARGET_PATH=$(find $BUILD_PATH -name "$TARGET" | tail -n1) +TARGET_PATH=$(find $BUILD_PATH -name "$TARGET" | head -1) if [ -f $TARGET_PATH ]; then OBJECT_FILE="$TARGET_PATH" else TARGET=$(echo $TARGET | sed 's/\.[^.]*$//') - OBJECT_FILE=$(find $BUILD_PATH -name "$TARGET" | tail -n1) + OBJECT_FILE=$(find $BUILD_PATH -name "$TARGET" | head -1) fi mkdir -p $(dirname "$OUTPUT_FILE") @@ -45,7 +45,6 @@ xcrun llvm-cov report \ "$OBJECT_FILE" \ --instr-profile=$INSTR_PROFILE \ --ignore-filename-regex=$IGNORE_FILENAME_REGEX \ - --use-color # Export to code coverage file xcrun llvm-cov export \ From 54b7d482f6b59083e97d9e3786320e140c932b97 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 23 Apr 2024 09:57:23 +0200 Subject: [PATCH 11/17] Ignore DS_Store files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1e17051..ed8d367 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,5 @@ iOSInjectionProject/ settings.json /docs /coverage + +**/.DS_Store \ No newline at end of file From 39bed250c0c087b6a7f1dc652817531c044c3314 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Thu, 2 May 2024 10:49:57 +0200 Subject: [PATCH 12/17] Update dependencies and scripts Signed-off-by: Henrik Panhans --- .github/workflows/documentation.yml | 2 +- Package.resolved | 4 ++-- Scripts/build-docc-archive | 14 +++++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index d20dd7c..cfc9bac 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -38,7 +38,7 @@ jobs: restore-keys: | ${{ runner.os }}-spm- - name: Build DocC - run: Scripts/build-docc-archive + run: Scripts/build-docc-archive HPOpenWeather - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/Package.resolved b/Package.resolved index cf9bead..3ff4768 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/henrik-dmg/HPNetwork", "state" : { - "revision" : "6e1266a06692dab7df6d57faffaee59f2b46c4f5", - "version" : "4.0.0" + "revision" : "a4629039573f695f97fc044032c4e654b40671b0", + "version" : "4.0.1" } }, { diff --git a/Scripts/build-docc-archive b/Scripts/build-docc-archive index 6d4216b..1ef153a 100755 --- a/Scripts/build-docc-archive +++ b/Scripts/build-docc-archive @@ -5,6 +5,13 @@ if [[ -z $RUNNER_TEMP ]]; then RUNNER_TEMP=$(git rev-parse --show-toplevel) fi +TARGET=$1 +HOSTING_BASE_PATH=$2 + +if [[ -z $HOSTING_BASE_PATH ]]; then + HOSTING_BASE_PATH=$TARGET +fi + # First, insert docc-plugin dependency. This is very hacky, but it avoids everyone having to pull in the docc-plugin when they use this library. sed '/Dependencies declare/a\ @@ -18,12 +25,13 @@ swift package resolve swift package \ --allow-writing-to-directory "$RUNNER_TEMP/docs" \ generate-documentation \ - --target HPOpenWeather \ + --target "$TARGET" \ --transform-for-static-hosting \ - --hosting-base-path HPOpenWeather \ + --hosting-base-path "$HOSTING_BASE_PATH" \ --output-path "$RUNNER_TEMP/docs" -echo "" > "$RUNNER_TEMP/docs/index.html" +CUSTOM_PATH=$(echo $HOSTING_BASE_PATH | tr '[:upper:]' '[:lower:]') +echo "" > "$RUNNER_TEMP/docs/index.html" if [[ -z $GITHUB_ACTIONS ]]; then echo "Restoring Package.swift to original state." From 1632d8a6065eecd8fc10f25fd0b4957a5ee3bebe Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 30 Sep 2024 18:26:52 +0200 Subject: [PATCH 13/17] Remove old API version and make models Codable Signed-off-by: Henrik Panhans --- Package.resolved | 4 +- .../Models/Forecasts/CurrentWeather.swift | 27 + .../Models/Forecasts/DailyForecast.swift | 29 + .../Models/Forecasts/ForecastBase.swift | 6 +- .../Models/Forecasts/HourlyForecast.swift | 24 + .../Models/Forecasts/MinutelyForecast.swift | 2 +- .../HPOpenWeather/Models/Temperature.swift | 4 +- .../Models/Weather+Language.swift | 2 + .../HPOpenWeather/Models/Weather+Units.swift | 2 - Sources/HPOpenWeather/Models/Weather.swift | 31 +- Sources/HPOpenWeather/Models/Wind.swift | 2 +- Sources/HPOpenWeather/OpenWeather.swift | 16 +- .../Requests/WeatherRequest.swift | 3 +- .../HPOpenWeatherTests/OpenWeatherTests.swift | 32 +- .../Resources/2-5-test-response.json | 1487 ----------------- Tests/HPOpenWeatherTests/WeatherTests.swift | 33 + 16 files changed, 163 insertions(+), 1541 deletions(-) delete mode 100644 Tests/HPOpenWeatherTests/Resources/2-5-test-response.json create mode 100644 Tests/HPOpenWeatherTests/WeatherTests.swift diff --git a/Package.resolved b/Package.resolved index 3ff4768..fa54949 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types.git", "state" : { - "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", - "version" : "1.0.3" + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" } } ], diff --git a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift index de68815..5cec335 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift @@ -76,4 +76,31 @@ public struct CurrentWeather: ForecastBase, SunForecast { self.sun = Sun(sunset: sunset, sunrise: sunrise) } + // MARK: - Encoding + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(timestamp, forKey: .timestamp) + try container.encodeIfPresent(pressure, forKey: .pressure) + try container.encodeIfPresent(humidity, forKey: .humidity) + try container.encodeIfPresent(dewPoint, forKey: .dewPoint) + try container.encodeIfPresent(uvIndex, forKey: .uvIndex) + try container.encodeIfPresent(visibility, forKey: .visibility) + try container.encodeIfPresent(cloudCoverage, forKey: .cloudCoverage) + try container.encodeIfPresent(rain, forKey: .rain) + try container.encodeIfPresent(snow, forKey: .snow) + try container.encodeIfPresent([currentCondition], forKey: .weather) + + try container.encodeIfPresent(temperature.actual, forKey: .actualTemperature) + try container.encodeIfPresent(temperature.feelsLike, forKey: .feelsLikeTemperature) + + try container.encodeIfPresent(sun.sunrise, forKey: .sunrise) + try container.encodeIfPresent(sun.sunset, forKey: .sunset) + + try container.encodeIfPresent(wind.gust, forKey: .windGust) + try container.encodeIfPresent(wind.speed, forKey: .windSpeed) + try container.encodeIfPresent(wind.degrees, forKey: .windDirection) + } + } diff --git a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift index 75a0d0d..7e77290 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift @@ -82,4 +82,33 @@ public struct DailyForecast: ForecastBase, SunForecast, MoonForecast { self.moon = Moon(moonset: moonset, moonrise: moonrise) } + // MARK: - Encoding + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(temperature, forKey: .temperature) + try container.encodeIfPresent(feelsLikeTemperature, forKey: .feelsLikeTemperature) + try container.encodeIfPresent(totalRain, forKey: .totalRain) + try container.encodeIfPresent(totalSnow, forKey: .totalSnow) + try container.encodeIfPresent(timestamp, forKey: .timestamp) + try container.encodeIfPresent(pressure, forKey: .pressure) + try container.encodeIfPresent(humidity, forKey: .humidity) + try container.encodeIfPresent(dewPoint, forKey: .dewPoint) + try container.encodeIfPresent(uvIndex, forKey: .uvIndex) + try container.encodeIfPresent(visibility, forKey: .visibility) + try container.encodeIfPresent(cloudCoverage, forKey: .cloudCoverage) + try container.encodeIfPresent([condition], forKey: .weather) + + try container.encodeIfPresent(sun.sunrise, forKey: .sunrise) + try container.encodeIfPresent(sun.sunset, forKey: .sunset) + + try container.encodeIfPresent(moon.moonrise, forKey: .moonrise) + try container.encodeIfPresent(moon.moonset, forKey: .moonset) + + try container.encodeIfPresent(wind.gust, forKey: .windGust) + try container.encodeIfPresent(wind.speed, forKey: .windSpeed) + try container.encodeIfPresent(wind.degrees, forKey: .windDirection) + } + } diff --git a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift index 68d1eb9..099b2e2 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/ForecastBase.swift @@ -1,6 +1,6 @@ import Foundation -public protocol ForecastBase: Decodable, Hashable { +public protocol ForecastBase: Codable, Hashable { /// The timestamp when the data was collected. var timestamp: Date { get } @@ -23,14 +23,14 @@ public protocol ForecastBase: Decodable, Hashable { } -public protocol SunForecast: Decodable, Hashable { +public protocol SunForecast: Codable, Hashable { /// A container that holds information about sunset and sunrise timestamps. var sun: Sun { get } } -public protocol MoonForecast: Decodable, Hashable { +public protocol MoonForecast: Codable, Hashable { /// A container that holds information about moonrise and moonset timestamps. var moon: Moon { get } diff --git a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift index 80dbca2..47b87f1 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift @@ -68,4 +68,28 @@ public struct HourlyForecast: ForecastBase { self.wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) } + // MARK: - Encoding + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(timestamp, forKey: .timestamp) + try container.encodeIfPresent(pressure, forKey: .pressure) + try container.encodeIfPresent(humidity, forKey: .humidity) + try container.encodeIfPresent(dewPoint, forKey: .dewPoint) + try container.encodeIfPresent(uvIndex, forKey: .uvIndex) + try container.encodeIfPresent(visibility, forKey: .visibility) + try container.encodeIfPresent(cloudCoverage, forKey: .cloudCoverage) + try container.encodeIfPresent(rain, forKey: .rain) + try container.encodeIfPresent(snow, forKey: .snow) + try container.encodeIfPresent([condition], forKey: .weather) + + try container.encodeIfPresent(temperature.actual, forKey: .actualTemperature) + try container.encodeIfPresent(temperature.feelsLike, forKey: .feelsLikeTemperature) + + try container.encodeIfPresent(wind.gust, forKey: .windGust) + try container.encodeIfPresent(wind.speed, forKey: .windSpeed) + try container.encodeIfPresent(wind.degrees, forKey: .windDirection) + } + } diff --git a/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift index 9f3216b..f5bd859 100644 --- a/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift +++ b/Sources/HPOpenWeather/Models/Forecasts/MinutelyForecast.swift @@ -1,6 +1,6 @@ import Foundation -public struct MinutelyForecast: Decodable, Equatable, Hashable { +public struct MinutelyForecast: Codable, Equatable, Hashable { // MARK: - Nested Types diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift index 9a1b670..d663a3a 100644 --- a/Sources/HPOpenWeather/Models/Temperature.swift +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -9,14 +9,14 @@ public struct Temperature: Equatable, Hashable { public let feelsLike: Double /// A convertible measurement of the actually measured temperature. - /// - Parameter units: The units to use when formatting the `actual` property + /// - Parameter units: The units to use when formatting the `actual` property. This should be the same as what you used when making the request. /// - Returns: a measurement in the provided unit public func actualMeasurement(units: WeatherUnits) -> Measurement { Measurement(value: actual, unit: units.temperatureUnit) } /// A convertible measurement of how the actually measured temperature feels like. - /// - Parameter units: The units to use when formatting the `feelsLike` property + /// - Parameter units: The units to use when formatting the `feelsLike` property. This should be the same as what you used when making the request. /// - Returns: a measurement in the provided unit public func feelsLikeMeasurement(units: WeatherUnits) -> Measurement { Measurement(value: feelsLike, unit: units.temperatureUnit) diff --git a/Sources/HPOpenWeather/Models/Weather+Language.swift b/Sources/HPOpenWeather/Models/Weather+Language.swift index f509186..ce207b0 100644 --- a/Sources/HPOpenWeather/Models/Weather+Language.swift +++ b/Sources/HPOpenWeather/Models/Weather+Language.swift @@ -2,6 +2,7 @@ import Foundation /// The language that should be used in API responses for example for weather condition descriptions. public enum WeatherLanguage: String, Codable { + case afrikaans = "af" case arabic = "ar" case azerbaijani = "az" @@ -47,4 +48,5 @@ public enum WeatherLanguage: String, Codable { case chineseSimplified = "zh_cn" case chineseTraditional = "zh_tw" case zulu = "zu" + } diff --git a/Sources/HPOpenWeather/Models/Weather+Units.swift b/Sources/HPOpenWeather/Models/Weather+Units.swift index 3ba94b3..7415fde 100644 --- a/Sources/HPOpenWeather/Models/Weather+Units.swift +++ b/Sources/HPOpenWeather/Models/Weather+Units.swift @@ -10,7 +10,6 @@ public enum WeatherUnits: String, Codable { /// Temperature in Fahrenheit and wind speed in miles/hour. case imperial - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) var temperatureUnit: UnitTemperature { switch self { case .standard: @@ -22,7 +21,6 @@ public enum WeatherUnits: String, Codable { } } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) var windSpeedUnit: UnitSpeed { switch self { case .standard: diff --git a/Sources/HPOpenWeather/Models/Weather.swift b/Sources/HPOpenWeather/Models/Weather.swift index a746978..2ac48bc 100644 --- a/Sources/HPOpenWeather/Models/Weather.swift +++ b/Sources/HPOpenWeather/Models/Weather.swift @@ -1,6 +1,6 @@ import Foundation -public struct Weather: Decodable, Equatable, Hashable { +public struct Weather: Codable, Equatable, Hashable { // MARK: - Nested Types @@ -11,6 +11,12 @@ public struct Weather: Decodable, Equatable, Hashable { case hourlyForecasts = "hourly" case dailyForecasts = "daily" case alerts + + // These keys are not actually present in the response from the OpenWeather API. + // We inject them manually after decoding the response in order to persist these settings + // if you want to cache the response for example. + case language + case units } // MARK: - Properties @@ -23,8 +29,8 @@ public struct Weather: Decodable, Equatable, Hashable { /// Government weather alerts data from major national weather warning systems. public let alerts: [WeatherAlert]? - public internal(set) var language: WeatherLanguage! - public internal(set) var units: WeatherUnits! + public internal(set) var language: WeatherLanguage? + public internal(set) var units: WeatherUnits? // MARK: - Init @@ -42,6 +48,25 @@ public struct Weather: Decodable, Equatable, Hashable { self.hourlyForecasts = try container.decodeIfPresent([HourlyForecast].self, forKey: .hourlyForecasts) self.dailyForecasts = try container.decodeIfPresent([DailyForecast].self, forKey: .dailyForecasts) self.alerts = try container.decodeIfPresent([WeatherAlert].self, forKey: .alerts) + + self.language = try container.decodeIfPresent(WeatherLanguage.self, forKey: .language) + self.units = try container.decodeIfPresent(WeatherUnits.self, forKey: .units) + } + + // MARK: - Encoding + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(timezone.identifier, forKey: .timezoneIdentifier) + try container.encodeIfPresent(currentWeather, forKey: .currentWeather) + try container.encodeIfPresent(minutelyForecasts, forKey: .minutelyForecasts) + try container.encodeIfPresent(hourlyForecasts, forKey: .hourlyForecasts) + try container.encodeIfPresent(dailyForecasts, forKey: .dailyForecasts) + try container.encodeIfPresent(alerts, forKey: .alerts) + + try container.encodeIfPresent(language, forKey: .language) + try container.encodeIfPresent(units, forKey: .units) } } diff --git a/Sources/HPOpenWeather/Models/Wind.swift b/Sources/HPOpenWeather/Models/Wind.swift index 799a053..cc5b7ac 100644 --- a/Sources/HPOpenWeather/Models/Wind.swift +++ b/Sources/HPOpenWeather/Models/Wind.swift @@ -11,7 +11,7 @@ public struct Wind: Codable, Equatable, Hashable { public let degrees: Double? /// A measurement of the `speed` property if existing, measured in the passed in units. - /// - Parameter units: The units to use when formatting the `speed` property + /// - Parameter units: The units to use when formatting the `speed` property. This should be the same as what you used when making the request. /// - Returns: a measurement in the provided unit public func speedMeasurement(units: WeatherUnits) -> Measurement? { guard let speed else { diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index e94f375..5ec0971 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -27,15 +27,9 @@ public final class OpenWeather { } } - public enum APIVersion: String { - @available(*, deprecated, message: "The OpenWeather One Call API 2.5 will be deprecated in June 2024") - case twoPointFive = "2.5" - case threePointZero = "3.0" - } - // MARK: - Properties - /// The OpenWeatherMap API key to authorize requests. + /// The OpenWeather API key to authorize requests. public var apiKey: String? /// The language that should be used in API responses. public var language: WeatherLanguage = .english @@ -65,7 +59,6 @@ public final class OpenWeather { /// - coordinate: The coordinate for which the weather will be requested /// - excludedFields: An array specifying the fields that will be excluded from the response /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved - /// - version: The API version to use. Default is 3.0. /// - urlSession: The `URLSession` that will be used schedule requests /// - Returns: A weather response object /// - Throws: If no API key was provided, the request was misconfigured, the networking failed or the response failed to decode @@ -73,7 +66,6 @@ public final class OpenWeather { for coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil, - version: APIVersion = .threePointZero, urlSession: URLSession = .shared ) async throws -> Weather { guard let apiKey, !apiKey.isEmpty else { @@ -85,8 +77,7 @@ public final class OpenWeather { coordinate: coordinate, excludedFields: excludedFields, date: date, - settings: settings, - version: version + settings: settings ) return try await request.response(urlSession: urlSession).output @@ -97,7 +88,6 @@ public final class OpenWeather { /// - coordinate: The coordinate for which the weather will be requested /// - excludedFields: An array specifying the fields that will be excluded from the response /// - date: The date for which you want to request the weather. If no date is provided, the current weather will be retrieved - /// - version: The API version to use. Default is 3.0. /// - urlSession: The `URLSession` that will be used schedule requests /// - completion: A completion that will be called with the result of the network request /// - Returns: A network task that can be used to cancel the request @@ -106,7 +96,6 @@ public final class OpenWeather { for coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil, - version: APIVersion = .threePointZero, urlSession: URLSession = .shared, completion: @escaping (Result) -> Void ) -> Task { @@ -116,7 +105,6 @@ public final class OpenWeather { for: coordinate, excludedFields: excludedFields, date: date, - version: version, urlSession: urlSession ) completion(.success(response)) diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index ed026c1..9435af2 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -15,7 +15,6 @@ struct WeatherRequest: DecodableRequest { let excludedFields: [ExcludableField]? let date: Date? let settings: OpenWeather.Settings - let version: OpenWeather.APIVersion let requestMethod: HTTPRequest.Method = .get @@ -34,7 +33,7 @@ struct WeatherRequest: DecodableRequest { return try URL.buildThrowing { Host("api.openweathermap.org") PathComponent("data") - PathComponent(version.rawValue) + PathComponent("3.0") PathComponent("onecall") PathComponent(date != nil ? "timemachine" : nil) QueryItem(name: "lat", value: coordinate.latitude, digits: 5) diff --git a/Tests/HPOpenWeatherTests/OpenWeatherTests.swift b/Tests/HPOpenWeatherTests/OpenWeatherTests.swift index 4c7a3bc..72ad07d 100644 --- a/Tests/HPOpenWeatherTests/OpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/OpenWeatherTests.swift @@ -23,35 +23,21 @@ final class OpenWeatherTests: XCTestCase { // MARK: - Tests - func testOldApiRequest() async throws { - try make25WeatherResponse(version: .twoPointFive) - - let settings = OpenWeather.Settings(apiKey: "debug") - let request = WeatherRequest( - coordinate: .init(latitude: 52.5200, longitude: 13.4050), - excludedFields: nil, - date: nil, - settings: settings, - version: .twoPointFive - ) - _ = try await request.response(urlSession: mockedURLSession) - } - func testNewApiRequest() async throws { - try make25WeatherResponse(version: .threePointZero) + try makeWeatherResponse() let settings = OpenWeather.Settings(apiKey: "debug") let request = WeatherRequest( coordinate: .init(latitude: 52.5200, longitude: 13.4050), excludedFields: nil, date: nil, - settings: settings, - version: .threePointZero + settings: settings ) let weather = try await request.response(urlSession: mockedURLSession).output let currentWeather = try XCTUnwrap(weather.currentWeather) XCTAssertEqual(currentWeather.timestamp, Date(timeIntervalSince1970: 1_713_795_125)) + XCTAssertEqual(weather.units, .metric) } func testInvalidApiKey() async throws { @@ -60,8 +46,7 @@ final class OpenWeatherTests: XCTestCase { coordinate: .init(latitude: 52.5200, longitude: 13.4050), excludedFields: nil, date: nil, - settings: settings, - version: .threePointZero + settings: settings ) do { @@ -78,8 +63,7 @@ final class OpenWeatherTests: XCTestCase { coordinate: .init(latitude: 52.5200, longitude: 13.4050), excludedFields: nil, date: Date.distantFuture, - settings: settings, - version: .threePointZero + settings: settings ) do { @@ -92,11 +76,11 @@ final class OpenWeatherTests: XCTestCase { // MARK: - Helpers - private func make25WeatherResponse(version: OpenWeather.APIVersion) throws { - let url = try XCTUnwrap(URL(string: "https://api.openweathermap.org/data/\(version.rawValue)/onecall")) + private func makeWeatherResponse() throws { + let url = try XCTUnwrap(URL(string: "https://api.openweathermap.org/data/3.0/onecall")) let jsonDataURL = try XCTUnwrap( Bundle.module.url( - forResource: "\(version.rawValue.replacingOccurrences(of: ".", with: "-"))-test-response", + forResource: "3-0-test-response", withExtension: "json" ) ) diff --git a/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json b/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json deleted file mode 100644 index 56f0670..0000000 --- a/Tests/HPOpenWeatherTests/Resources/2-5-test-response.json +++ /dev/null @@ -1,1487 +0,0 @@ -{ - "lat": 52.52, - "lon": 13.405, - "timezone": "Europe/Berlin", - "timezone_offset": 7200, - "current": { - "dt": 1713733621, - "sunrise": 1713671679, - "sunset": 1713723300, - "temp": 3.32, - "feels_like": -1.08, - "pressure": 1012, - "humidity": 71, - "dew_point": -1.26, - "uvi": 0, - "clouds": 0, - "visibility": 10000, - "wind_speed": 5.66, - "wind_deg": 20, - "weather": [ - { "id": 800, "main": "Clear", "description": "clear sky", "icon": "01n" } - ] - }, - "minutely": [ - { "dt": 1713733680, "precipitation": 0 }, - { "dt": 1713733740, "precipitation": 0 }, - { "dt": 1713733800, "precipitation": 0 }, - { "dt": 1713733860, "precipitation": 0 }, - { "dt": 1713733920, "precipitation": 0 }, - { "dt": 1713733980, "precipitation": 0 }, - { "dt": 1713734040, "precipitation": 0 }, - { "dt": 1713734100, "precipitation": 0 }, - { "dt": 1713734160, "precipitation": 0 }, - { "dt": 1713734220, "precipitation": 0 }, - { "dt": 1713734280, "precipitation": 0 }, - { "dt": 1713734340, "precipitation": 0 }, - { "dt": 1713734400, "precipitation": 0 }, - { "dt": 1713734460, "precipitation": 0 }, - { "dt": 1713734520, "precipitation": 0 }, - { "dt": 1713734580, "precipitation": 0 }, - { "dt": 1713734640, "precipitation": 0 }, - { "dt": 1713734700, "precipitation": 0 }, - { "dt": 1713734760, "precipitation": 0 }, - { "dt": 1713734820, "precipitation": 0 }, - { "dt": 1713734880, "precipitation": 0 }, - { "dt": 1713734940, "precipitation": 0 }, - { "dt": 1713735000, "precipitation": 0 }, - { "dt": 1713735060, "precipitation": 0 }, - { "dt": 1713735120, "precipitation": 0 }, - { "dt": 1713735180, "precipitation": 0 }, - { "dt": 1713735240, "precipitation": 0 }, - { "dt": 1713735300, "precipitation": 0 }, - { "dt": 1713735360, "precipitation": 0 }, - { "dt": 1713735420, "precipitation": 0 }, - { "dt": 1713735480, "precipitation": 0 }, - { "dt": 1713735540, "precipitation": 0 }, - { "dt": 1713735600, "precipitation": 0 }, - { "dt": 1713735660, "precipitation": 0 }, - { "dt": 1713735720, "precipitation": 0 }, - { "dt": 1713735780, "precipitation": 0 }, - { "dt": 1713735840, "precipitation": 0 }, - { "dt": 1713735900, "precipitation": 0 }, - { "dt": 1713735960, "precipitation": 0 }, - { "dt": 1713736020, "precipitation": 0 }, - { "dt": 1713736080, "precipitation": 0 }, - { "dt": 1713736140, "precipitation": 0 }, - { "dt": 1713736200, "precipitation": 0 }, - { "dt": 1713736260, "precipitation": 0 }, - { "dt": 1713736320, "precipitation": 0 }, - { "dt": 1713736380, "precipitation": 0 }, - { "dt": 1713736440, "precipitation": 0 }, - { "dt": 1713736500, "precipitation": 0 }, - { "dt": 1713736560, "precipitation": 0 }, - { "dt": 1713736620, "precipitation": 0 }, - { "dt": 1713736680, "precipitation": 0 }, - { "dt": 1713736740, "precipitation": 0 }, - { "dt": 1713736800, "precipitation": 0 }, - { "dt": 1713736860, "precipitation": 0 }, - { "dt": 1713736920, "precipitation": 0 }, - { "dt": 1713736980, "precipitation": 0 }, - { "dt": 1713737040, "precipitation": 0 }, - { "dt": 1713737100, "precipitation": 0 }, - { "dt": 1713737160, "precipitation": 0 }, - { "dt": 1713737220, "precipitation": 0 } - ], - "hourly": [ - { - "dt": 1713733200, - "temp": 3.32, - "feels_like": -0.5, - "pressure": 1012, - "humidity": 71, - "dew_point": -1.26, - "uvi": 0, - "clouds": 0, - "visibility": 10000, - "wind_speed": 4.5, - "wind_deg": 35, - "wind_gust": 8.91, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01n" - } - ], - "pop": 0 - }, - { - "dt": 1713736800, - "temp": 2.99, - "feels_like": -0.78, - "pressure": 1015, - "humidity": 69, - "dew_point": -1.88, - "uvi": 0, - "clouds": 9, - "visibility": 10000, - "wind_speed": 4.28, - "wind_deg": 36, - "wind_gust": 8.78, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01n" - } - ], - "pop": 0 - }, - { - "dt": 1713740400, - "temp": 2.4, - "feels_like": -1.5, - "pressure": 1017, - "humidity": 68, - "dew_point": -2.56, - "uvi": 0, - "clouds": 18, - "visibility": 10000, - "wind_speed": 4.26, - "wind_deg": 37, - "wind_gust": 9.23, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02n" - } - ], - "pop": 0 - }, - { - "dt": 1713744000, - "temp": 1.74, - "feels_like": -2.13, - "pressure": 1020, - "humidity": 68, - "dew_point": -3.12, - "uvi": 0, - "clouds": 23, - "visibility": 10000, - "wind_speed": 3.98, - "wind_deg": 40, - "wind_gust": 9.66, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02n" - } - ], - "pop": 0 - }, - { - "dt": 1713747600, - "temp": 1.02, - "feels_like": -2.61, - "pressure": 1022, - "humidity": 69, - "dew_point": -3.56, - "uvi": 0, - "clouds": 10, - "visibility": 10000, - "wind_speed": 3.42, - "wind_deg": 43, - "wind_gust": 9.41, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01n" - } - ], - "pop": 0 - }, - { - "dt": 1713751200, - "temp": 0.11, - "feels_like": -3.25, - "pressure": 1025, - "humidity": 71, - "dew_point": -4.6, - "uvi": 0, - "clouds": 17, - "visibility": 10000, - "wind_speed": 2.87, - "wind_deg": 43, - "wind_gust": 7.87, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02n" - } - ], - "pop": 0 - }, - { - "dt": 1713754800, - "temp": -0.11, - "feels_like": -3.24, - "pressure": 1025, - "humidity": 73, - "dew_point": -4.36, - "uvi": 0, - "clouds": 30, - "visibility": 10000, - "wind_speed": 2.59, - "wind_deg": 32, - "wind_gust": 6.69, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03n" - } - ], - "pop": 0 - }, - { - "dt": 1713758400, - "temp": -0.25, - "feels_like": -3.22, - "pressure": 1025, - "humidity": 74, - "dew_point": -4.26, - "uvi": 0, - "clouds": 39, - "visibility": 10000, - "wind_speed": 2.41, - "wind_deg": 22, - "wind_gust": 5.86, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0 - }, - { - "dt": 1713762000, - "temp": 0.39, - "feels_like": -2.39, - "pressure": 1025, - "humidity": 72, - "dew_point": -4.19, - "uvi": 0.15, - "clouds": 44, - "visibility": 10000, - "wind_speed": 2.34, - "wind_deg": 20, - "wind_gust": 4.91, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0 - }, - { - "dt": 1713765600, - "temp": 1.87, - "feels_like": -0.62, - "pressure": 1025, - "humidity": 64, - "dew_point": -4.15, - "uvi": 0.5, - "clouds": 42, - "visibility": 10000, - "wind_speed": 2.31, - "wind_deg": 17, - "wind_gust": 4.62, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0 - }, - { - "dt": 1713769200, - "temp": 3.37, - "feels_like": 0.76, - "pressure": 1025, - "humidity": 57, - "dew_point": -4.38, - "uvi": 1.09, - "clouds": 44, - "visibility": 10000, - "wind_speed": 2.74, - "wind_deg": 16, - "wind_gust": 4.61, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0 - }, - { - "dt": 1713772800, - "temp": 4.67, - "feels_like": 2.15, - "pressure": 1024, - "humidity": 50, - "dew_point": -4.83, - "uvi": 1.88, - "clouds": 56, - "visibility": 10000, - "wind_speed": 2.94, - "wind_deg": 12, - "wind_gust": 4.75, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713776400, - "temp": 5.36, - "feels_like": 2.66, - "pressure": 1024, - "humidity": 46, - "dew_point": -5.19, - "uvi": 1.84, - "clouds": 71, - "visibility": 10000, - "wind_speed": 3.41, - "wind_deg": 15, - "wind_gust": 5.1, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713780000, - "temp": 5.13, - "feels_like": 2.07, - "pressure": 1024, - "humidity": 48, - "dew_point": -5.1, - "uvi": 1.66, - "clouds": 78, - "visibility": 10000, - "wind_speed": 3.91, - "wind_deg": 23, - "wind_gust": 4.9, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713783600, - "temp": 6.32, - "feels_like": 3.97, - "pressure": 1024, - "humidity": 44, - "dew_point": -4.93, - "uvi": 3.11, - "clouds": 82, - "visibility": 10000, - "wind_speed": 3.18, - "wind_deg": 14, - "wind_gust": 4.72, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713787200, - "temp": 6.04, - "feels_like": 3.59, - "pressure": 1023, - "humidity": 45, - "dew_point": -5.08, - "uvi": 1.61, - "clouds": 85, - "visibility": 10000, - "wind_speed": 3.24, - "wind_deg": 6, - "wind_gust": 4.19, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713790800, - "temp": 7.1, - "feels_like": 4.96, - "pressure": 1023, - "humidity": 42, - "dew_point": -4.95, - "uvi": 2.33, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.11, - "wind_deg": 1, - "wind_gust": 4, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713794400, - "temp": 6.78, - "feels_like": 4.27, - "pressure": 1023, - "humidity": 42, - "dew_point": -5.19, - "uvi": 1.6, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.59, - "wind_deg": 3, - "wind_gust": 4.07, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713798000, - "temp": 7.25, - "feels_like": 4.71, - "pressure": 1023, - "humidity": 41, - "dew_point": -5.04, - "uvi": 0.97, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.84, - "wind_deg": 12, - "wind_gust": 4.21, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713801600, - "temp": 6.74, - "feels_like": 4.23, - "pressure": 1022, - "humidity": 43, - "dew_point": -5.11, - "uvi": 0.56, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.59, - "wind_deg": 20, - "wind_gust": 3.88, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713805200, - "temp": 6.3, - "feels_like": 4.25, - "pressure": 1022, - "humidity": 44, - "dew_point": -4.86, - "uvi": 0.22, - "clouds": 96, - "visibility": 10000, - "wind_speed": 2.74, - "wind_deg": 32, - "wind_gust": 2.77, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713808800, - "temp": 5.07, - "feels_like": 3.04, - "pressure": 1023, - "humidity": 50, - "dew_point": -4.57, - "uvi": 0, - "clouds": 81, - "visibility": 10000, - "wind_speed": 2.43, - "wind_deg": 44, - "wind_gust": 3.03, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713812400, - "temp": 4.22, - "feels_like": 2.49, - "pressure": 1023, - "humidity": 54, - "dew_point": -4.24, - "uvi": 0, - "clouds": 9, - "visibility": 10000, - "wind_speed": 1.97, - "wind_deg": 70, - "wind_gust": 3.53, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01n" - } - ], - "pop": 0 - }, - { - "dt": 1713816000, - "temp": 3.63, - "feels_like": 2, - "pressure": 1023, - "humidity": 57, - "dew_point": -4.02, - "uvi": 0, - "clouds": 12, - "visibility": 10000, - "wind_speed": 1.8, - "wind_deg": 95, - "wind_gust": 3.29, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02n" - } - ], - "pop": 0 - }, - { - "dt": 1713819600, - "temp": 3.18, - "feels_like": 1.71, - "pressure": 1023, - "humidity": 59, - "dew_point": -4.13, - "uvi": 0, - "clouds": 23, - "visibility": 10000, - "wind_speed": 1.62, - "wind_deg": 125, - "wind_gust": 2.62, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02n" - } - ], - "pop": 0 - }, - { - "dt": 1713823200, - "temp": 2.69, - "feels_like": 1.11, - "pressure": 1022, - "humidity": 61, - "dew_point": -4.2, - "uvi": 0, - "clouds": 34, - "visibility": 10000, - "wind_speed": 1.65, - "wind_deg": 149, - "wind_gust": 2.84, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03n" - } - ], - "pop": 0 - }, - { - "dt": 1713826800, - "temp": 2.34, - "feels_like": 0.7, - "pressure": 1022, - "humidity": 62, - "dew_point": -4.3, - "uvi": 0, - "clouds": 46, - "visibility": 10000, - "wind_speed": 1.65, - "wind_deg": 161, - "wind_gust": 2.82, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03n" - } - ], - "pop": 0 - }, - { - "dt": 1713830400, - "temp": 2.07, - "feels_like": 0.38, - "pressure": 1022, - "humidity": 62, - "dew_point": -4.44, - "uvi": 0, - "clouds": 55, - "visibility": 10000, - "wind_speed": 1.66, - "wind_deg": 169, - "wind_gust": 2.73, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1713834000, - "temp": 1.89, - "feels_like": 0.3, - "pressure": 1022, - "humidity": 63, - "dew_point": -4.44, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.57, - "wind_deg": 180, - "wind_gust": 2.67, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1713837600, - "temp": 1.66, - "feels_like": -0.17, - "pressure": 1022, - "humidity": 65, - "dew_point": -4.3, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.72, - "wind_deg": 194, - "wind_gust": 3.04, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1713841200, - "temp": 1.24, - "feels_like": -0.84, - "pressure": 1021, - "humidity": 67, - "dew_point": -4.12, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.86, - "wind_deg": 210, - "wind_gust": 3.35, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1713844800, - "temp": 1.1, - "feels_like": -1.43, - "pressure": 1021, - "humidity": 69, - "dew_point": -3.97, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.22, - "wind_deg": 217, - "wind_gust": 3.99, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713848400, - "temp": 1.47, - "feels_like": -1.12, - "pressure": 1021, - "humidity": 69, - "dew_point": -3.56, - "uvi": 0.13, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.34, - "wind_deg": 230, - "wind_gust": 4, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713852000, - "temp": 2.6, - "feels_like": 0.22, - "pressure": 1021, - "humidity": 66, - "dew_point": -3.23, - "uvi": 0.4, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.33, - "wind_deg": 253, - "wind_gust": 3.81, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713855600, - "temp": 4.13, - "feels_like": 1.81, - "pressure": 1020, - "humidity": 62, - "dew_point": -2.49, - "uvi": 0.92, - "clouds": 63, - "visibility": 10000, - "wind_speed": 2.57, - "wind_deg": 265, - "wind_gust": 3.94, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713859200, - "temp": 5.51, - "feels_like": 3.42, - "pressure": 1020, - "humidity": 60, - "dew_point": -1.66, - "uvi": 1.63, - "clouds": 57, - "visibility": 10000, - "wind_speed": 2.6, - "wind_deg": 267, - "wind_gust": 3.64, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713862800, - "temp": 6.7, - "feels_like": 4.78, - "pressure": 1020, - "humidity": 53, - "dew_point": -2.06, - "uvi": 2.56, - "clouds": 64, - "visibility": 10000, - "wind_speed": 2.67, - "wind_deg": 264, - "wind_gust": 3.96, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713866400, - "temp": 7.99, - "feels_like": 6.22, - "pressure": 1019, - "humidity": 42, - "dew_point": -4.09, - "uvi": 3.47, - "clouds": 59, - "visibility": 10000, - "wind_speed": 2.81, - "wind_deg": 272, - "wind_gust": 3.94, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713870000, - "temp": 8.81, - "feels_like": 7.26, - "pressure": 1018, - "humidity": 36, - "dew_point": -5.29, - "uvi": 3.45, - "clouds": 57, - "visibility": 10000, - "wind_speed": 2.72, - "wind_deg": 277, - "wind_gust": 3.79, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713873600, - "temp": 9.67, - "feels_like": 8.41, - "pressure": 1018, - "humidity": 34, - "dew_point": -5.52, - "uvi": 2.82, - "clouds": 61, - "visibility": 10000, - "wind_speed": 2.53, - "wind_deg": 271, - "wind_gust": 3.56, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713877200, - "temp": 9.91, - "feels_like": 8.59, - "pressure": 1017, - "humidity": 34, - "dew_point": -5.29, - "uvi": 1.83, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.69, - "wind_deg": 274, - "wind_gust": 3.44, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713880800, - "temp": 9.78, - "feels_like": 8.42, - "pressure": 1016, - "humidity": 35, - "dew_point": -5.05, - "uvi": 1.69, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.72, - "wind_deg": 293, - "wind_gust": 3.32, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713884400, - "temp": 9.98, - "feels_like": 8.86, - "pressure": 1015, - "humidity": 35, - "dew_point": -4.64, - "uvi": 0.95, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.41, - "wind_deg": 299, - "wind_gust": 2.93, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713888000, - "temp": 9.71, - "feels_like": 8.63, - "pressure": 1015, - "humidity": 37, - "dew_point": -4.34, - "uvi": 0.52, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.29, - "wind_deg": 298, - "wind_gust": 2.35, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713891600, - "temp": 9.06, - "feels_like": 8.13, - "pressure": 1015, - "humidity": 41, - "dew_point": -3.38, - "uvi": 0.2, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.96, - "wind_deg": 312, - "wind_gust": 1.77, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713895200, - "temp": 7.9, - "feels_like": 7.27, - "pressure": 1015, - "humidity": 46, - "dew_point": -2.97, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.49, - "wind_deg": 351, - "wind_gust": 1.66, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1713898800, - "temp": 6.85, - "feels_like": 6.16, - "pressure": 1015, - "humidity": 51, - "dew_point": -2.68, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.42, - "wind_deg": 35, - "wind_gust": 2.15, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1713902400, - "temp": 6.26, - "feels_like": 5.07, - "pressure": 1015, - "humidity": 53, - "dew_point": -2.62, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.77, - "wind_deg": 62, - "wind_gust": 3.01, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - } - ], - "daily": [ - { - "dt": 1713697200, - "sunrise": 1713671679, - "sunset": 1713723300, - "moonrise": 1713713940, - "moonset": 1713669360, - "moon_phase": 0.42, - "summary": "Expect a day of partly cloudy with rain", - "temp": { - "day": 8.02, - "min": 1.1, - "max": 8.86, - "night": 3.32, - "eve": 6.26, - "morn": 1.3 - }, - "feels_like": { "day": 4.88, "night": -0.5, "eve": 3.33, "morn": -2.82 }, - "pressure": 1023, - "humidity": 41, - "dew_point": -4.55, - "wind_speed": 5.58, - "wind_deg": 51, - "wind_gust": 9.18, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 71, - "pop": 1, - "rain": 0.8, - "uvi": 3.47 - }, - { - "dt": 1713783600, - "sunrise": 1713757952, - "sunset": 1713809805, - "moonrise": 1713804660, - "moonset": 1713756240, - "moon_phase": 0.45, - "summary": "Expect a day of partly cloudy with clear spells", - "temp": { - "day": 6.32, - "min": -0.25, - "max": 7.25, - "night": 3.18, - "eve": 6.3, - "morn": 0.39 - }, - "feels_like": { "day": 3.97, "night": 1.71, "eve": 4.25, "morn": -2.39 }, - "pressure": 1024, - "humidity": 44, - "dew_point": -4.93, - "wind_speed": 4.28, - "wind_deg": 36, - "wind_gust": 9.66, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "clouds": 82, - "pop": 0, - "uvi": 3.11 - }, - { - "dt": 1713870000, - "sunrise": 1713844225, - "sunset": 1713896310, - "moonrise": 1713895500, - "moonset": 1713843180, - "moon_phase": 0.48, - "summary": "There will be partly cloudy today", - "temp": { - "day": 8.81, - "min": 1.1, - "max": 9.98, - "night": 5.59, - "eve": 9.06, - "morn": 1.47 - }, - "feels_like": { "day": 7.26, "night": 3.91, "eve": 8.13, "morn": -1.12 }, - "pressure": 1018, - "humidity": 36, - "dew_point": -5.29, - "wind_speed": 2.81, - "wind_deg": 272, - "wind_gust": 4.08, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "clouds": 57, - "pop": 0, - "uvi": 3.47 - }, - { - "dt": 1713956400, - "sunrise": 1713930500, - "sunset": 1713982815, - "moonrise": 1713986520, - "moonset": 1713930240, - "moon_phase": 0.5, - "summary": "There will be partly cloudy today", - "temp": { - "day": 11, - "min": 2.71, - "max": 11.78, - "night": 6.66, - "eve": 9.64, - "morn": 3.21 - }, - "feels_like": { "day": 9.17, "night": 3.78, "eve": 7.24, "morn": 0.78 }, - "pressure": 1009, - "humidity": 39, - "dew_point": -2.38, - "wind_speed": 4.86, - "wind_deg": 19, - "wind_gust": 9.16, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "clouds": 97, - "pop": 0, - "uvi": 3.4 - }, - { - "dt": 1714042800, - "sunrise": 1714016776, - "sunset": 1714069320, - "moonrise": 1714077660, - "moonset": 1714017480, - "moon_phase": 0.55, - "summary": "There will be partly cloudy today", - "temp": { - "day": 9.53, - "min": 4.57, - "max": 10.36, - "night": 5.95, - "eve": 8.04, - "morn": 4.92 - }, - "feels_like": { "day": 7.32, "night": 4.71, "eve": 6, "morn": 1.66 }, - "pressure": 1006, - "humidity": 36, - "dew_point": -4.62, - "wind_speed": 4.99, - "wind_deg": 350, - "wind_gust": 10.09, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "clouds": 100, - "pop": 0, - "uvi": 2.66 - }, - { - "dt": 1714129200, - "sunrise": 1714103053, - "sunset": 1714155825, - "moonrise": 0, - "moonset": 1714105020, - "moon_phase": 0.58, - "summary": "There will be partly cloudy today", - "temp": { - "day": 11.55, - "min": 3.66, - "max": 12.67, - "night": 7.8, - "eve": 10.31, - "morn": 4.96 - }, - "feels_like": { "day": 9.83, "night": 7.8, "eve": 8.65, "morn": 2.49 }, - "pressure": 1011, - "humidity": 41, - "dew_point": -1.11, - "wind_speed": 2.96, - "wind_deg": 287, - "wind_gust": 5.89, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "clouds": 100, - "pop": 0, - "uvi": 3.19 - }, - { - "dt": 1714215600, - "sunrise": 1714189331, - "sunset": 1714242329, - "moonrise": 1714168860, - "moonset": 1714193160, - "moon_phase": 0.61, - "summary": "Expect a day of partly cloudy with rain", - "temp": { - "day": 15.79, - "min": 5.26, - "max": 15.79, - "night": 9.76, - "eve": 9.96, - "morn": 7.44 - }, - "feels_like": { "day": 14.34, "night": 7.6, "eve": 7.16, "morn": 5.53 }, - "pressure": 1010, - "humidity": 35, - "dew_point": 0.21, - "wind_speed": 6.09, - "wind_deg": 51, - "wind_gust": 9.68, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 94, - "pop": 1, - "rain": 18.54, - "uvi": 4 - }, - { - "dt": 1714302000, - "sunrise": 1714275610, - "sunset": 1714328834, - "moonrise": 1714259640, - "moonset": 1714282140, - "moon_phase": 0.64, - "summary": "There will be rain until morning, then partly cloudy", - "temp": { - "day": 9.68, - "min": 7.88, - "max": 10.51, - "night": 7.88, - "eve": 9.71, - "morn": 8.15 - }, - "feels_like": { "day": 7.73, "night": 7.88, "eve": 9.12, "morn": 5.52 }, - "pressure": 1010, - "humidity": 84, - "dew_point": 7.07, - "wind_speed": 4.43, - "wind_deg": 247, - "wind_gust": 8.15, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 0.18, - "uvi": 4 - } - ], - "alerts": [ - { - "sender_name": "Deutscher Wetterdienst", - "event": "frost", - "start": 1713729600, - "end": 1713769200, - "description": "There is a risk of frost (level 1 of 2).\nMinimum temperature: 0 - -4 °C; near surface: -3 - -7 °C", - "tags": ["Extreme low temperature"] - } - ] -} diff --git a/Tests/HPOpenWeatherTests/WeatherTests.swift b/Tests/HPOpenWeatherTests/WeatherTests.swift new file mode 100644 index 0000000..4cf04a8 --- /dev/null +++ b/Tests/HPOpenWeatherTests/WeatherTests.swift @@ -0,0 +1,33 @@ +import HPNetwork +import XCTest + +@testable import HPOpenWeather + +final class WeatherTests: XCTestCase { + + func testEncoding() throws { + let weather = try makeWeatherResponse() + XCTAssertNoThrow(try JSONEncoder().encode(weather)) + } + + func testEncodingAndDecoding() throws { + let weather = try makeWeatherResponse() + let encodedJSON = try JSONEncoder().encode(weather) + let decodedWeather = try JSONDecoder().decode(Weather.self, from: encodedJSON) + + XCTAssertEqual(decodedWeather, weather) + } + + private func makeWeatherResponse() throws -> Weather { + let jsonDataURL = try XCTUnwrap( + Bundle.module.url( + forResource: "3-0-test-response", + withExtension: "json" + ) + ) + let jsonData = try Data(contentsOf: jsonDataURL) + + return try JSONDecoder().decode(Weather.self, from: jsonData) + } + +} From 03269d080264105f85f64dc54ac71f75f27c28dc Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 1 Oct 2024 12:03:19 +0200 Subject: [PATCH 14/17] Move documentation into docc catalog Signed-off-by: Henrik Panhans --- README.md | 83 ++--------------- .../Documentation.docc/Documentation.md | 89 +++++++++++++++++++ Sources/HPOpenWeather/OpenWeather.swift | 2 +- 3 files changed, 97 insertions(+), 77 deletions(-) create mode 100644 Sources/HPOpenWeather/Documentation.docc/Documentation.md diff --git a/README.md b/README.md index b40c7a7..14d5f69 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,20 @@ # HPOpenWeather -CodeFactor - -Github Actions +[![codecov](https://codecov.io/gh/henrik-dmg/HPOpenWeather/graph/badge.svg?token=mX05cXr144)](https://codecov.io/gh/henrik-dmg/HPOpenWeather) +[![Swift](https://github.com/henrik-dmg/HPOpenWeather/actions/workflows/swift.yml/badge.svg)](https://github.com/henrik-dmg/HPOpenWeather/actions/workflows/swift.yml) [![GitHub license](https://img.shields.io/github/license/henrik-dmg/HPOpenWeather)](https://github.com/henrik-dmg/HPOpenWeather/blob/master/LICENSE.md) HPOpenWeather is a cross-platform Swift framework to communicate with the OpenWeather One-Call API. See their [documentation](https://openweathermap.org/api/one-call-api) for further details. ## Installation -HPOpenWeather supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. +`HPOpenWeather` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. +It can be installed via SPM: -### SPM - -Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "6.0.0")` to your `Package.swift` file - -## Usage - -### Configuration - -To get started, you need an API key from [OpenWeather](https://openweathermap.org). Configure the `OpenWeather.shared` singleton or create your own instance and configure it with your key and other settings. - -```swift -import HPOpenWeather - -// Assign API key -OpenWeather.shared.apiKey = "--- YOUR API KEY ---" -OpenWeather.shared.language = .german -OpenWeather.shared.units = .metric - -// Or use options -let settings = OpenWeather.Settings(apiKey: "yourAPIKey", language: .german, units: .metric) -let openWeather = OpenWeather(settings: settings) ``` - -You can also customise the response data units and language by accessing the `language` and `units` propertis. - -### Retrieving Weather Information - -To fetch the weather, there are two options: async/await or callback. Both expect a `CLLocationCoordinate2D` for which to fetch the weather. -Additionally, you can specify which fields should be excluded from the response to save bandwidth, or specify a historic date or a date up to 4 days in the future. - -#### Async - -```swift -let weather = try await OpenWeather.shared.weather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) -``` - -#### Callback - -```swift -OpenWeather.shared.requestWeather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) { result in - switch result { - case .success(let weather): - print(weather) - case .failure(let error): - print(error) - } -} +.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "6.0.0") ``` -### Available languages (default in bold) - -- **English** -- Russian -- Italian -- Spanish -- Ukrainian -- German -- Portuguese -- Romanian -- Polish -- Finnish -- Dutch -- French -- Bulgarian -- Swedish -- Chinese Traditional -- Chinese Simplified -- Turkish -- Croatian -- Catalan - -### Available units (default in bold) +## Documentation -- **Metric** (wind speed in m/s, temperature in Celsius) -- Imperial (wind speed in mph, temperature in Fahrenheit) -- Standard (wind speed in m/s, temperature in Kelvin) +The documentation has been moved to https://henrik-dmg.github.io/HPOpenWeather, which is deployed from the DocC catalog. diff --git a/Sources/HPOpenWeather/Documentation.docc/Documentation.md b/Sources/HPOpenWeather/Documentation.docc/Documentation.md new file mode 100644 index 0000000..9439cc2 --- /dev/null +++ b/Sources/HPOpenWeather/Documentation.docc/Documentation.md @@ -0,0 +1,89 @@ +# ``HPOpenWeather`` + +## Overview + +HPOpenWeather is a cross-platform Swift framework to communicate with the OpenWeather One-Call API. +See their [documentation](https://openweathermap.org/api/one-call-api) for further details. + +## Installation + +``HPOpenWeather`` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. +It can be installed via SPM: + +``` +.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "6.0.0") +``` + +## Usage + +### Configuration + +To get started, you need an API key from [OpenWeather](https://openweathermap.org). + +```swift +import HPOpenWeather + +// Create instance +let openWeatherClient = OpenWeather(apiKey: "") + +// Or use options +let settings = OpenWeather.Settings(apiKey: "", language: .german, units: .metric) +let openWeather = OpenWeather(settings: settings) + +// Change settings at any point +openWeatherClient.apiKey = "" +openWeatherClient.language = .german +openWeatherClient.units = .metric +``` + +### Retrieving Weather Information + +To fetch the weather, there are two options: async/await or callback. Both expect a ``CLLocationCoordinate2D`` for which to fetch the weather. +Additionally, you can specify which fields should be excluded from the response to save bandwidth, or specify a historic date or a date up to 4 days in the future. + +#### Async + +```swift +let weather = try await openWeatherClient.weather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) +``` + +#### Callback + +```swift +openWeatherClient.requestWeather(for: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)) { result in + switch result { + case .success(let weather): + print(weather) + case .failure(let error): + print(error) + } +} +``` + +### Available languages (default in bold) + +- **English** +- Russian +- Italian +- Spanish +- Ukrainian +- German +- Portuguese +- Romanian +- Polish +- Finnish +- Dutch +- French +- Bulgarian +- Swedish +- Chinese Traditional +- Chinese Simplified +- Turkish +- Croatian +- Catalan + +### Available units (default in bold) + +- **Metric** (wind speed in m/s, temperature in Celsius) +- Imperial (wind speed in mph, temperature in Fahrenheit) +- Standard (wind speed in m/s, temperature in Kelvin) diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index 5ec0971..ddd6bc2 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -97,7 +97,7 @@ public final class OpenWeather { excludedFields: [ExcludableField]? = nil, date: Date? = nil, urlSession: URLSession = .shared, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> Task { Task { do { From af25cfcf258120e4af6c1d350a71e9663439ba50 Mon Sep 17 00:00:00 2001 From: henrik-dmg Date: Tue, 1 Oct 2024 10:03:52 +0000 Subject: [PATCH 15/17] Prettified Code! --- Sources/HPOpenWeather/Documentation.docc/Documentation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/HPOpenWeather/Documentation.docc/Documentation.md b/Sources/HPOpenWeather/Documentation.docc/Documentation.md index 9439cc2..abb7fc7 100644 --- a/Sources/HPOpenWeather/Documentation.docc/Documentation.md +++ b/Sources/HPOpenWeather/Documentation.docc/Documentation.md @@ -1,4 +1,4 @@ -# ``HPOpenWeather`` +# `HPOpenWeather` ## Overview @@ -7,7 +7,7 @@ See their [documentation](https://openweathermap.org/api/one-call-api) for furth ## Installation -``HPOpenWeather`` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. +`HPOpenWeather` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. It can be installed via SPM: ``` @@ -38,7 +38,7 @@ openWeatherClient.units = .metric ### Retrieving Weather Information -To fetch the weather, there are two options: async/await or callback. Both expect a ``CLLocationCoordinate2D`` for which to fetch the weather. +To fetch the weather, there are two options: async/await or callback. Both expect a `CLLocationCoordinate2D` for which to fetch the weather. Additionally, you can specify which fields should be excluded from the response to save bandwidth, or specify a historic date or a date up to 4 days in the future. #### Async From c82dc0062b0d79e6b1d0ac87de3edeac86a2578a Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 1 Oct 2024 12:05:40 +0200 Subject: [PATCH 16/17] Remove prettier Signed-off-by: Henrik Panhans --- .github/workflows/prettier.yml | 35 ---------------------------------- config/.prettierignore | 2 -- config/prettier.config.js | 17 ----------------- 3 files changed, 54 deletions(-) delete mode 100644 .github/workflows/prettier.yml delete mode 100644 config/.prettierignore delete mode 100644 config/prettier.config.js diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml deleted file mode 100644 index f529368..0000000 --- a/.github/workflows/prettier.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Prettier - -on: - push: - branches: ["main"] - paths: - - "**/*.js" - - "**/*.yml" - - "**/*.md" - pull_request: - branches: [main] - paths: - - "**/*.js" - - "**/*.yml" - - "**/*.md" - -jobs: - prettier: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # Make sure the actual branch is checked out when running on pull requests - ref: ${{ github.head_ref }} - # This is important to fetch the changes to the previous commit - fetch-depth: 0 - - - name: Prettify code - uses: creyD/prettier_action@v4.3 - with: - # This part is also where you can pass other options, for example: - prettier_options: --write **/*.{js,md,yml} --config config/prettier.config.js --ignore-path config/.prettierignore - only_changed: True diff --git a/config/.prettierignore b/config/.prettierignore deleted file mode 100644 index f4aa5e4..0000000 --- a/config/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -.swiftpm -.build \ No newline at end of file diff --git a/config/prettier.config.js b/config/prettier.config.js deleted file mode 100644 index 1876910..0000000 --- a/config/prettier.config.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @type {import('prettier').Config} */ -module.exports = { - endOfLine: "lf", - semi: false, - singleQuote: false, - tabWidth: 2, - trailingComma: "es5", - printWidth: 140, - overrides: [ - { - files: ["**/*.md"], - options: { - tabWidth: 4, - }, - }, - ], -} From 6a1cefee757d73c31029c9bf09fb2edafa7ca6df Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 1 Oct 2024 12:07:21 +0200 Subject: [PATCH 17/17] Bring back proper docc formatting Signed-off-by: Henrik Panhans --- .../Documentation.docc/Documentation.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/HPOpenWeather/Documentation.docc/Documentation.md b/Sources/HPOpenWeather/Documentation.docc/Documentation.md index abb7fc7..891ca04 100644 --- a/Sources/HPOpenWeather/Documentation.docc/Documentation.md +++ b/Sources/HPOpenWeather/Documentation.docc/Documentation.md @@ -1,4 +1,4 @@ -# `HPOpenWeather` +# ``HPOpenWeather`` ## Overview @@ -7,7 +7,7 @@ See their [documentation](https://openweathermap.org/api/one-call-api) for furth ## Installation -`HPOpenWeather` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. +``HPOpenWeather`` supports iOS 15.0+, watchOS 6.0+, tvOS 15.0+ and macOS 12+. It can be installed via SPM: ``` @@ -18,7 +18,7 @@ It can be installed via SPM: ### Configuration -To get started, you need an API key from [OpenWeather](https://openweathermap.org). +To get started, you need an API key from [OpenWeather](https://openweathermap.org). Then you can create an instance of the ``OpenWeather`` class. ```swift import HPOpenWeather @@ -38,7 +38,7 @@ openWeatherClient.units = .metric ### Retrieving Weather Information -To fetch the weather, there are two options: async/await or callback. Both expect a `CLLocationCoordinate2D` for which to fetch the weather. +To fetch the weather, there are two options: async/await or callback. Both expect a ``CLLocationCoordinate2D`` for which to fetch the weather. Additionally, you can specify which fields should be excluded from the response to save bandwidth, or specify a historic date or a date up to 4 days in the future. #### Async @@ -82,8 +82,12 @@ openWeatherClient.requestWeather(for: CLLocationCoordinate2D(latitude: 37.7749, - Croatian - Catalan +See ``WeatherLanguage`` for details. + ### Available units (default in bold) - **Metric** (wind speed in m/s, temperature in Celsius) - Imperial (wind speed in mph, temperature in Fahrenheit) - Standard (wind speed in m/s, temperature in Kelvin) + +See ``WeatherUnits`` for details. \ No newline at end of file