From ceae8fe54d5f731b215b763452cc13c1ba08b797 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Sun, 27 Feb 2022 21:49:43 +0100 Subject: [PATCH] Measurement (#12) --- .github/workflows/swift.yml | 2 +- .vscode/launch.json | 15 ++ HPOpenWeather.podspec | 18 +-- Package.resolved | 13 +- Package.swift | 9 +- README.md | 37 ++--- Sources/HPOpenWeather/DataTypes/Alert.swift | 25 ---- .../DataTypes/Forecasts/CurrentWeather.swift | 75 ---------- .../DataTypes/Forecasts/DailyForecast.swift | 70 ---------- .../DataTypes/Forecasts/HourlyForecast.swift | 64 --------- .../DataTypes/Precipitation.swift | 16 --- .../HPOpenWeather/DataTypes/Temperature.swift | 11 -- .../HPOpenWeather/DataTypes/WeatherIcon.swift | 96 ------------- Sources/HPOpenWeather/DataTypes/Wind.swift | 13 -- .../URLQueryItemsBuilder+Extensions.swift | 12 -- .../{DataTypes => Models}/City.swift | 2 +- .../DailyTemperature.swift | 12 +- .../Forecasts/BasicWeather.swift | 0 .../Models/Forecasts/CurrentWeather.swift | 75 ++++++++++ .../Models/Forecasts/DailyForecast.swift | 70 ++++++++++ .../Models/Forecasts/HourlyForecast.swift | 64 +++++++++ .../HPOpenWeather/Models/Precipitation.swift | 32 +++++ .../{DataTypes => Models}/Sun.swift | 6 +- .../HPOpenWeather/Models/Temperature.swift | 33 +++++ .../HPOpenWeather/Models/WeatherAlert.swift | 25 ++++ .../WeatherCondition.swift | 2 +- .../HPOpenWeather/Models/WeatherIcon.swift | 95 +++++++++++++ Sources/HPOpenWeather/Models/Wind.swift | 27 ++++ Sources/HPOpenWeather/OpenWeather.swift | 130 ++++++++++-------- .../Requests/APINetworkRequest.swift | 39 +++--- .../Requests/ExcludableField.swift | 11 -- .../Requests/RequestLanguage.swift | 52 ------- .../HPOpenWeather/Requests/RequestUnits.swift | 13 -- .../Requests/WeatherRequest+Combine.swift | 28 ++-- .../WeatherRequest+ExcludableField.swift | 13 ++ .../Requests/WeatherRequest.swift | 111 +++++++-------- .../Response/WeatherResponse+Language.swift | 54 ++++++++ .../Response/WeatherResponse+Units.swift | 41 ++++++ .../Response/WeatherResponse.swift | 33 +++-- .../HPOpenWeatherTests.swift | 69 +++++----- 40 files changed, 828 insertions(+), 685 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 Sources/HPOpenWeather/DataTypes/Alert.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Precipitation.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Temperature.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/WeatherIcon.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/Wind.swift delete mode 100644 Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift rename Sources/HPOpenWeather/{DataTypes => Models}/City.swift (89%) rename Sources/HPOpenWeather/{DataTypes => Models}/DailyTemperature.swift (74%) rename Sources/HPOpenWeather/{DataTypes => Models}/Forecasts/BasicWeather.swift (100%) create mode 100644 Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift create mode 100644 Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift create mode 100644 Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift create mode 100644 Sources/HPOpenWeather/Models/Precipitation.swift rename Sources/HPOpenWeather/{DataTypes => Models}/Sun.swift (69%) create mode 100644 Sources/HPOpenWeather/Models/Temperature.swift create mode 100644 Sources/HPOpenWeather/Models/WeatherAlert.swift rename Sources/HPOpenWeather/{DataTypes => Models}/WeatherCondition.swift (92%) create mode 100644 Sources/HPOpenWeather/Models/WeatherIcon.swift create mode 100644 Sources/HPOpenWeather/Models/Wind.swift delete mode 100644 Sources/HPOpenWeather/Requests/ExcludableField.swift delete mode 100644 Sources/HPOpenWeather/Requests/RequestLanguage.swift delete mode 100644 Sources/HPOpenWeather/Requests/RequestUnits.swift create mode 100644 Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift create mode 100644 Sources/HPOpenWeather/Response/WeatherResponse+Language.swift create mode 100644 Sources/HPOpenWeather/Response/WeatherResponse+Units.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a8f4bf0..08e9469 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -23,4 +23,4 @@ jobs: env: API_KEY: ${{ secrets.API_KEY }} - name: Codecov - uses: codecov/codecov-action@v1.0.15 \ No newline at end of file + uses: codecov/codecov-action@v2 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b7e1bb4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "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/HPOpenWeather.podspec b/HPOpenWeather.podspec index a44214c..e1044ce 100644 --- a/HPOpenWeather.podspec +++ b/HPOpenWeather.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "HPOpenWeather" - s.version = "4.1.5" + s.version = "5.0.0" s.summary = "Cross-platform framework to communicate with the OpenWeatherMap JSON API" s.license = { :type => "MIT", :file => "LICENSE.md" } @@ -10,23 +10,23 @@ Pod::Spec.new do |s| s.author = { "henrik-dmg" => "henrik@panhans.dev" } s.social_media_url = "https://twitter.com/henrik_dmg" - s.ios.deployment_target = "9.0" - s.osx.deployment_target = "10.11" - s.watchos.deployment_target = "3.0" - s.tvos.deployment_target = "9.0" + 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.osx.framework = "AppKit" - s.swift_version = "5.1" + s.swift_version = "5.5" s.requires_arc = true - s.dependency "HPNetwork" + s.dependency "HPNetwork", "~> 3.1.1" + s.dependency "HPURLBuilder", "~> 1.0.0" end diff --git a/Package.resolved b/Package.resolved index 64216c5..e8836d1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,17 @@ "repositoryURL": "https://github.com/henrik-dmg/HPNetwork", "state": { "branch": null, - "revision": "7e888cdaabf7cb6e8c6640db57cb4325099f79b2", - "version": "2.0.2" + "revision": "2bdaaa3f1c9d52a30e5afb8f737c3b72f5afba0c", + "version": "3.1.1" + } + }, + { + "package": "HPURLBuilder", + "repositoryURL": "https://github.com/henrik-dmg/HPURLBuilder", + "state": { + "branch": null, + "revision": "49ad1fb6f10914e7134dc9f8e5c21f48a52e3d37", + "version": "1.1.0" } } ] diff --git a/Package.swift b/Package.swift index 6ff0492..2161b65 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.5 // 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(.v9), .macOS(.v10_11), .tvOS(.v9), .watchOS(.v3) + .iOS(.v13), .tvOS(.v13), .watchOS(.v7), .macOS(.v10_15) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. @@ -17,14 +17,15 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "2.0.0") + .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "3.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. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "HPOpenWeather", - dependencies: ["HPNetwork"] + dependencies: ["HPNetwork", "HPURLBuilder"] ), .testTarget( name: "HPOpenWeatherTests", diff --git a/README.md b/README.md index cd2714c..4dd798e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -

- Storage -

+# HPOpenWeather CodeFactor @@ -8,21 +6,23 @@ [![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 9.0+, watchOS 3.0+, tvOS 9.0+ and macOS 10.10+. +HPOpenWeather supports iOS 13.0+, watchOS 7.0+, tvOS 13.0+ and macOS 10.15+. -#### SPM +### SPM -Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "4.0.0")` to your `Package.swift` file +Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "5.0.0")` to your `Package.swift` file -#### CocoaPods +### CocoaPods Add `pod 'HPOpenWeather'` to your `Podfile` and run `pod install` ## 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). + ```swift import HPOpenWeather @@ -35,9 +35,10 @@ OpenWeather.shared.units = .metric let settings = OpenWeather.Settings(apiKey: "yourAPIKey", language: .german, units: .metric) OpenWeather.shared.apply(settings) ``` + You can also customise the response data units and language by accessing the `language` and `units` propertis. -## Making a request +### Making a request To make a request, initialize a new request object like this @@ -53,10 +54,11 @@ let timemachineRequest = WeatherRequest(coordinate: .init(latitude: 40, longitud **Note:** the date has to be at least 6 hours in the past -To post a request, call `sendWeatherRequest` on `OpenWeather`: +To post a request, call `sendWeatherRequest` on `OpenWeather`: ```swift -OpenWeather.shared.performWeatherRequest(request) { result in +// Classic completion handler approach +OpenWeather.shared.schedule(request) { result in switch result { case .success(let response): // do something with weather data here @@ -64,11 +66,14 @@ OpenWeather.shared.performWeatherRequest(request) { result in // handle error } } + +// Or using the new concurrency features +let response = try await OpenWeather.shared.weatherResponse(request) ``` -**The following response languages are available** +### Available languages (default in bold) -- English (default) +- **English** - Russian - Italian - Spanish @@ -88,8 +93,8 @@ OpenWeather.shared.performWeatherRequest(request) { result in - Croatian - Catalan -**The following temperature units are available** +### Available units (default in bold) -- Celsius (default) -- Kelvin -- Fahrenheit +- **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/DataTypes/Alert.swift b/Sources/HPOpenWeather/DataTypes/Alert.swift deleted file mode 100644 index 426a519..0000000 --- a/Sources/HPOpenWeather/DataTypes/Alert.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -/// Type that holds information about weather alerts -public struct Alert: Codable, Hashable, Equatable { - - /// Name of the alert source. Please read here the full list of alert sources - public let senderName: String - /// Alert event name - public let eventName: String - //// Date and time of the start of the alert - public let startDate: Date - //// Date and time of the end of the alert - public let endDate: Date - /// 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/DataTypes/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift deleted file mode 100644 index 34694ae..0000000 --- a/Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation - -public struct CurrentWeather: BasicWeatherResponse, SunResponse { - - // MARK: - Coding Keys - - enum CodingKeys: String, CodingKey { - case feelsLikeTemperature = "feels_like" - case snow - case rain - case timestamp = "dt" - case actualTemperature = "temp" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - case sunrise - case sunset - } - - // MARK: - Properties - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - public let uvIndex: Double? - public let visibility: Double? - 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) - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift deleted file mode 100644 index f466dab..0000000 --- a/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation - -public struct DailyForecast: BasicWeatherResponse, SunResponse { - - // MARK: - Coding Keys - - enum CodingKeys: String, CodingKey { - case feelsLikeTemperature = "feels_like" - case totalRain = "rain" - case totalSnow = "snow" - case timestamp = "dt" - case temperature = "temp" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - case sunrise - case sunset - } - - // MARK: - Properties - - public let temperature: DailyTemperature - public let feelsLikeTemperature: DailyTemperature - public let totalRain: Double? - public let totalSnow: Double? - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - 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) - } - -} - diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift deleted file mode 100644 index 62febc2..0000000 --- a/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation - -public struct HourlyForecast: BasicWeatherResponse { - - // MARK: - Coding Keys - - enum CodingKeys: String, CodingKey { - case actualTemperature = "temp" - case feelsLikeTemperature = "feels_like" - case snow - case rain - case timestamp = "dt" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - } - - // MARK: - Properties - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - public let uvIndex: Double? - public let visibility: Double? - 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 - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/Precipitation.swift b/Sources/HPOpenWeather/DataTypes/Precipitation.swift deleted file mode 100644 index d40ee0f..0000000 --- a/Sources/HPOpenWeather/DataTypes/Precipitation.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Type that holds information about recent precipitation -public struct Precipitation: Codable, Equatable, Hashable { - - /// Precipitation volume for the last 1 hour, measured in mm - public var lastHour: Double? - /// Precipitation volume for the last 3 hours, measured in mm - public var lastThreeHours: Double? - - enum CodingKeys: String, CodingKey { - case lastHour = "1h" - case lastThreeHours = "3h" - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/Temperature.swift b/Sources/HPOpenWeather/DataTypes/Temperature.swift deleted file mode 100644 index 63686bd..0000000 --- a/Sources/HPOpenWeather/DataTypes/Temperature.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// Type that holds information about daily temperature changes -public struct Temperature: Codable, Equatable, Hashable { - - /// The actually measured temperature - public let actual: Double - /// The feels-like temperature - public let feelsLike: Double - -} diff --git a/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift b/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift deleted file mode 100644 index c327a75..0000000 --- a/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift +++ /dev/null @@ -1,96 +0,0 @@ -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif -import Foundation - -public enum WeatherIcon: String, Codable, CaseIterable { - - case clearSky = "01d" - case clearSkyNight = "01n" - case fewClouds = "02d" - case fewCloudsNight = "02n" - case scatteredClouds = "03d" - case scatteredCloudsNight = "03n" - case brokenClouds = "04d" - case brokenCloudsNight = "04n" - case showerRain = "09d" - case showerRainNight = "09n" - case rain = "10d" - case rainNight = "10n" - case thunderstorm = "11d" - case thunderstormNight = "11n" - case snow = "13d" - case snowNight = "13n" - case mist = "50d" - case mistNight = "50n" - -} - -@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) -public extension WeatherIcon { - - var systemImageName: String { - makeIconName(filled: false) - } - - var systemImageNameFilled: String { - makeIconName(filled: true) - } - - #if canImport(UIKit) - - func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { - UIImage(systemName: systemImageNameFilled, withConfiguration: configuration) - } - - func outlineUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { - UIImage(systemName: systemImageName, withConfiguration: configuration) - } - - #elseif canImport(AppKit) - - func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { - NSImage(systemSymbolName: systemImageNameFilled, accessibilityDescription: accessibilityDescription) - } - - func outlineNSImage(accessibilityDescription: String? = nil) -> NSImage? { - NSImage(systemSymbolName: systemImageName, accessibilityDescription: accessibilityDescription) - } - - #endif - - private func makeIconName(filled: Bool) -> String { - let iconName: String - switch self { - case .clearSky: - iconName = "sun.max" - case .clearSkyNight: - iconName = "moon" - case .fewClouds: - iconName = "cloud.sun" - case .fewCloudsNight: - iconName = "cloud.moon" - case .scatteredClouds, .scatteredCloudsNight: - iconName = "cloud" - case .brokenClouds, .brokenCloudsNight: - iconName = "smoke" - case .showerRain, .showerRainNight: - iconName = "cloud.rain" - case .rain: - iconName = "cloud.sun.rain" - case .rainNight: - iconName = "cloud.moon.rain" - case .thunderstorm, .thunderstormNight: - iconName = "cloud.bolt.rain" - case .snow, .snowNight: - return "snow" - case .mist, .mistNight: - iconName = "cloud.fog" - } - - return iconName + (filled ? ".fill" : "") - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/Wind.swift b/Sources/HPOpenWeather/DataTypes/Wind.swift deleted file mode 100644 index e52ba0b..0000000 --- a/Sources/HPOpenWeather/DataTypes/Wind.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// 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) - public let speed: Double? - /// Wind gust speed (metric: meter/sec, imperial: miles/hour) - public let gust: Double? - /// The wind direction measured in degrees from North - public let degrees: Double? - -} diff --git a/Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift b/Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift deleted file mode 100644 index e86e6cf..0000000 --- a/Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift +++ /dev/null @@ -1,12 +0,0 @@ -import HPNetwork - -extension URLBuilder { - - static let weatherBase: URLBuilder = { - URLBuilder(host: "api.openweathermap.org") - .addingPathComponent("data") - .addingPathComponent("2.5") - .addingPathComponent("onecall") - }() - -} diff --git a/Sources/HPOpenWeather/DataTypes/City.swift b/Sources/HPOpenWeather/Models/City.swift similarity index 89% rename from Sources/HPOpenWeather/DataTypes/City.swift rename to Sources/HPOpenWeather/Models/City.swift index c41e982..09c2570 100644 --- a/Sources/HPOpenWeather/DataTypes/City.swift +++ b/Sources/HPOpenWeather/Models/City.swift @@ -2,7 +2,7 @@ import Foundation import CoreLocation /// Type that holds information about the reqeuest's nearest city -public struct City: Codable, Equatable, Hashable { +public struct City: Codable, Equatable, Hashable, Identifiable { /// The ID assigned to the city public let id: Int diff --git a/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift b/Sources/HPOpenWeather/Models/DailyTemperature.swift similarity index 74% rename from Sources/HPOpenWeather/DataTypes/DailyTemperature.swift rename to Sources/HPOpenWeather/Models/DailyTemperature.swift index d323bd6..fa9a51a 100644 --- a/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift +++ b/Sources/HPOpenWeather/Models/DailyTemperature.swift @@ -3,17 +3,17 @@ import Foundation /// Type that holds information about daily temperature changes public struct DailyTemperature: Codable, Equatable, Hashable { - /// Day temperature. + /// Day temperature. public let day: Double - /// Night temperature. + /// Night temperature. public let night: Double - /// Minimum daily temperature. + /// Minimum daily temperature. public let min: Double? - /// Max daily temperature. + /// Max daily temperature. public let max: Double? - /// Evening temperature. + /// Evening temperature. public let evening: Double - /// Morning temperature. + /// Morning temperature. public let morning: Double enum CodingKeys: String, CodingKey { diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/BasicWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/BasicWeather.swift similarity index 100% rename from Sources/HPOpenWeather/DataTypes/Forecasts/BasicWeather.swift rename to Sources/HPOpenWeather/Models/Forecasts/BasicWeather.swift diff --git a/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift new file mode 100644 index 0000000..3638d71 --- /dev/null +++ b/Sources/HPOpenWeather/Models/Forecasts/CurrentWeather.swift @@ -0,0 +1,75 @@ +import Foundation + +public struct CurrentWeather: BasicWeatherResponse, SunResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case feelsLikeTemperature = "feels_like" + case snow + case rain + case timestamp = "dt" + case actualTemperature = "temp" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + case sunrise + case sunset + } + + // MARK: - Properties + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + public let uvIndex: Double? + public let visibility: Double? + 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) + } + +} diff --git a/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift new file mode 100644 index 0000000..1b9f063 --- /dev/null +++ b/Sources/HPOpenWeather/Models/Forecasts/DailyForecast.swift @@ -0,0 +1,70 @@ +import Foundation + +public struct DailyForecast: BasicWeatherResponse, SunResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case feelsLikeTemperature = "feels_like" + case totalRain = "rain" + case totalSnow = "snow" + case timestamp = "dt" + case temperature = "temp" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + case sunrise + case sunset + } + + // MARK: - Properties + + public let temperature: DailyTemperature + public let feelsLikeTemperature: DailyTemperature + public let totalRain: Double? + public let totalSnow: Double? + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + 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) + } + +} + diff --git a/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift new file mode 100644 index 0000000..8a2ffbb --- /dev/null +++ b/Sources/HPOpenWeather/Models/Forecasts/HourlyForecast.swift @@ -0,0 +1,64 @@ +import Foundation + +public struct HourlyForecast: BasicWeatherResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case actualTemperature = "temp" + case feelsLikeTemperature = "feels_like" + case snow + case rain + case timestamp = "dt" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + } + + // MARK: - Properties + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + public let uvIndex: Double? + public let visibility: Double? + 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 + } + +} diff --git a/Sources/HPOpenWeather/Models/Precipitation.swift b/Sources/HPOpenWeather/Models/Precipitation.swift new file mode 100644 index 0000000..fcdccc7 --- /dev/null +++ b/Sources/HPOpenWeather/Models/Precipitation.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Type that holds information about recent precipitation +public struct Precipitation: Codable, Equatable, Hashable { + + enum CodingKeys: String, CodingKey { + case lastHour = "1h" + case lastThreeHours = "3h" + } + + /// Precipitation volume for the last 1 hour, measured in mm + public var lastHour: Double? + /// 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 + public var lastHourMeasurement: Measurement? { + guard let lastHour = lastHour else { + return nil + } + return Measurement(value: lastHour, unit: .millimeters) + } + + /// A convertible measurement of how much precipitation occured in the last three hours if any + public var lastThreeHoursMeasurement: Measurement? { + guard let lastThreeHours = lastThreeHours else { + return nil + } + return Measurement(value: lastThreeHours, unit: .millimeters) + } + +} diff --git a/Sources/HPOpenWeather/DataTypes/Sun.swift b/Sources/HPOpenWeather/Models/Sun.swift similarity index 69% rename from Sources/HPOpenWeather/DataTypes/Sun.swift rename to Sources/HPOpenWeather/Models/Sun.swift index 880baa1..778dbfa 100644 --- a/Sources/HPOpenWeather/DataTypes/Sun.swift +++ b/Sources/HPOpenWeather/Models/Sun.swift @@ -2,10 +2,10 @@ import Foundation /// Type that holds information about sunrise and sunset times in UTC time public struct Sun: Codable, Equatable, Hashable { - + /// Sunset time public let sunset: Date - /// Sunrise timeWind speed. Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour. + /// Sunrise time public let sunrise: Date - + } diff --git a/Sources/HPOpenWeather/Models/Temperature.swift b/Sources/HPOpenWeather/Models/Temperature.swift new file mode 100644 index 0000000..f2bc8fd --- /dev/null +++ b/Sources/HPOpenWeather/Models/Temperature.swift @@ -0,0 +1,33 @@ +import Foundation + +/// Type that holds information about daily temperature changes +public struct Temperature: Codable, 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) + } + + /// 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) + } + +} diff --git a/Sources/HPOpenWeather/Models/WeatherAlert.swift b/Sources/HPOpenWeather/Models/WeatherAlert.swift new file mode 100644 index 0000000..7659d38 --- /dev/null +++ b/Sources/HPOpenWeather/Models/WeatherAlert.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Type that holds information about weather alerts +public struct WeatherAlert: Codable, Hashable, Equatable { + + /// Name of the alert source. Please read here the full list of alert sources + public let senderName: String + /// Alert event name + public let eventName: String + //// Date and time of the start of the alert + public let startDate: Date + //// Date and time of the end of the alert + public let endDate: Date + /// 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/DataTypes/WeatherCondition.swift b/Sources/HPOpenWeather/Models/WeatherCondition.swift similarity index 92% rename from Sources/HPOpenWeather/DataTypes/WeatherCondition.swift rename to Sources/HPOpenWeather/Models/WeatherCondition.swift index 8478364..7f95d66 100644 --- a/Sources/HPOpenWeather/DataTypes/WeatherCondition.swift +++ b/Sources/HPOpenWeather/Models/WeatherCondition.swift @@ -10,6 +10,6 @@ public struct WeatherCondition: Codable, Equatable, Hashable { /// The weather condition within the group public let description: String /// The ID of the corresponding weather icon - public let icon: WeatherIcon + public let icon: WeatherIcon } diff --git a/Sources/HPOpenWeather/Models/WeatherIcon.swift b/Sources/HPOpenWeather/Models/WeatherIcon.swift new file mode 100644 index 0000000..bbb1a14 --- /dev/null +++ b/Sources/HPOpenWeather/Models/WeatherIcon.swift @@ -0,0 +1,95 @@ +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +import Foundation + +public enum WeatherIcon: String, Codable, CaseIterable { + + case clearSky = "01d" + case clearSkyNight = "01n" + case fewClouds = "02d" + case fewCloudsNight = "02n" + case scatteredClouds = "03d" + case scatteredCloudsNight = "03n" + case brokenClouds = "04d" + case brokenCloudsNight = "04n" + case showerRain = "09d" + case showerRainNight = "09n" + case rain = "10d" + case rainNight = "10n" + case thunderstorm = "11d" + case thunderstormNight = "11n" + case snow = "13d" + case snowNight = "13n" + case mist = "50d" + case mistNight = "50n" + +} + +@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) +public extension WeatherIcon { + + var systemImageName: String { + makeIconName(filled: false) + } + + var systemImageNameFilled: String { + makeIconName(filled: true) + } + +#if canImport(UIKit) + + func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + UIImage(systemName: systemImageNameFilled, withConfiguration: configuration) + } + + func outlineUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + UIImage(systemName: systemImageName, withConfiguration: configuration) + } + +#elseif canImport(AppKit) + + func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { + NSImage(systemSymbolName: systemImageNameFilled, accessibilityDescription: accessibilityDescription) + } + + func outlineNSImage(accessibilityDescription: String? = nil) -> NSImage? { + NSImage(systemSymbolName: systemImageName, accessibilityDescription: accessibilityDescription) + } + +#endif + + private func makeIconName(filled: Bool) -> String { + let iconName: String + switch self { + case .clearSky: + iconName = "sun.max" + case .clearSkyNight: + iconName = "moon" + case .fewClouds: + iconName = "cloud.sun" + case .fewCloudsNight: + iconName = "cloud.moon" + case .scatteredClouds, .scatteredCloudsNight: + iconName = "cloud" + case .brokenClouds, .brokenCloudsNight: + iconName = "smoke" + case .showerRain, .showerRainNight: + iconName = "cloud.rain" + case .rain: + iconName = "cloud.sun.rain" + case .rainNight: + iconName = "cloud.moon.rain" + case .thunderstorm, .thunderstormNight: + iconName = "cloud.bolt.rain" + case .snow, .snowNight: + return "snow" + case .mist, .mistNight: + iconName = "cloud.fog" + } + return iconName + (filled ? ".fill" : "") + } + +} diff --git a/Sources/HPOpenWeather/Models/Wind.swift b/Sources/HPOpenWeather/Models/Wind.swift new file mode 100644 index 0000000..414e32d --- /dev/null +++ b/Sources/HPOpenWeather/Models/Wind.swift @@ -0,0 +1,27 @@ +import Foundation + +/// 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) + public let speed: Double? + /// Wind gust speed (metric: meter/sec, imperial: miles/hour) + 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 { + return nil + } + return Measurement(value: speed, unit: units.windSpeedUnit) + } + +} diff --git a/Sources/HPOpenWeather/OpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift index 4d86e54..3ab6e95 100644 --- a/Sources/HPOpenWeather/OpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -1,18 +1,26 @@ import CoreLocation import Foundation -import HPNetwork +/// 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 public struct Settings { + /// The API key to use for weather requests let apiKey : String - let language: RequestLanguage - let units: RequestUnits - - public init(apiKey: String, language: RequestLanguage = .english, units: RequestUnits = .metric) { + /// The language that will be used in weather responses + let language: WeatherResponse.Language + /// The units that will be used in weather responses + let units: WeatherResponse.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) { self.language = language self.units = units self.apiKey = apiKey @@ -21,22 +29,26 @@ public final class OpenWeather { // MARK: - Properties - /// A shared instance of the weather client + /// 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: RequestLanguage = .english + public var language: WeatherResponse.Language = .english /// The units that should be used to format the API responses - public var units: RequestUnits = .metric + public var units: WeatherResponse.Units = .metric // MARK: - Init + /// 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 + /// - Parameter settings: the settings to apply, including API key, language and units public init(settings: Settings) { self.apiKey = settings.apiKey self.language = settings.language @@ -45,62 +57,72 @@ public final class OpenWeather { // MARK: - Sending Requests - public func requestWeather( - coordinate: CLLocationCoordinate2D, - excludedFields: [ExcludableField]? = nil, - date: Date? = nil, - urlSession: URLSession = .shared, - finishingQueue: DispatchQueue = .main, - progressHandler: ProgressHandler? = nil, - completion: @escaping (Result) -> Void) - { - let request = WeatherRequest( - coordinate: coordinate, - excludedFields: excludedFields, - date: date, - urlSession: urlSession, - finishingQueue: finishingQueue - ) - schedule(request, progressHandler: progressHandler, completion: completion) - } - - /// Sends the specified request to the OpenWeather API - /// - Parameters: - /// - request: The request object that holds information about request location, date, etc. - /// - completion: The completion block that will be called once the networking finishes - /// - Returns: A network task that can be used to cancel the request - public func schedule( - _ request: WeatherRequest, - progressHandler: ProgressHandler? = nil, - completion: @escaping (Result) -> Void) - { + /// 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 + public func weatherResponse( + coordinate: CLLocationCoordinate2D, + excludedFields: [WeatherRequest.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 { - request.finishingQueue.async { - completion(.failure(NSError.noApiKey)) - } - return + throw NSError.noApiKey } let settings = Settings(apiKey: apiKey, language: language, units: units) - do { - let networkRequest = try request.makeNetworkRequest(settings: settings) - Network.shared.schedule(request: networkRequest, progressHandler: progressHandler, completion: completion) - } catch let error { - request.finishingQueue.async { + 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 + } + + /// 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 + /// - 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 + // 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 - } + /// 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 + } } diff --git a/Sources/HPOpenWeather/Requests/APINetworkRequest.swift b/Sources/HPOpenWeather/Requests/APINetworkRequest.swift index 5b67b9f..9e799ec 100644 --- a/Sources/HPOpenWeather/Requests/APINetworkRequest.swift +++ b/Sources/HPOpenWeather/Requests/APINetworkRequest.swift @@ -1,25 +1,30 @@ import Foundation import HPNetwork -public struct APINetworkRequest: DecodableRequest { +struct APINetworkRequest: DecodableRequest { - public let url: URL? - public let urlSession: URLSession - public let finishingQueue: DispatchQueue - public let requestMethod: NetworkRequestMethod = .get - public let headerFields = [NetworkRequestHeaderField.contentTypeJSON] + typealias Output = WeatherResponse - public let decoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - return decoder - }() + static let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + return decoder + }() - public func makeURL() throws -> URL { - guard let url = url else { - throw NSError(code: 6, description: "Could not create URL") - } - return url - } + 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 deleted file mode 100644 index c2e5c43..0000000 --- a/Sources/HPOpenWeather/Requests/ExcludableField.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -public enum ExcludableField: String { - - case current - case minutely - case hourly - case daily - case alerts - -} diff --git a/Sources/HPOpenWeather/Requests/RequestLanguage.swift b/Sources/HPOpenWeather/Requests/RequestLanguage.swift deleted file mode 100644 index 4c25782..0000000 --- a/Sources/HPOpenWeather/Requests/RequestLanguage.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -/// The language that should be used in API responses for example for weather condition descriptions -public enum RequestLanguage: String { - - 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/Requests/RequestUnits.swift b/Sources/HPOpenWeather/Requests/RequestUnits.swift deleted file mode 100644 index 3308807..0000000 --- a/Sources/HPOpenWeather/Requests/RequestUnits.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// The units that should the data in the API responses should be formatted in -public enum RequestUnits: String { - - /// 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 - -} diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift b/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift index a539865..d32b1f5 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest+Combine.swift @@ -1,19 +1,25 @@ -#if canImport(Combine) import Combine import Foundation -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public extension WeatherRequest { - func publisher(apiKey: String, language: RequestLanguage = .english, units: RequestUnits = .metric) -> AnyPublisher { - publisher(settings: OpenWeather.Settings(apiKey: apiKey, language: language, units: units)) - } + 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) -> AnyPublisher { - let request = APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) - return request.dataTaskPublisher() - } + func publisher(settings: OpenWeather.Settings, urlSession: URLSession = .shared, finishingQueue: DispatchQueue = .main) -> AnyPublisher { + let request = APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession) + return request.dataTaskPublisher() + } } - -#endif diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift b/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift new file mode 100644 index 0000000..bf70b80 --- /dev/null +++ b/Sources/HPOpenWeather/Requests/WeatherRequest+ExcludableField.swift @@ -0,0 +1,13 @@ +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 4b7890e..8123b16 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -1,73 +1,68 @@ import Foundation import CoreLocation import HPNetwork - -public struct WeatherRequest { - - // MARK: - Associated Types - - public typealias Output = WeatherResponse - - // MARK: - Properties - - public let coordinate: CLLocationCoordinate2D - public let excludedFields: [ExcludableField]? - public let date: Date? - public let urlSession: URLSession - public let finishingQueue: DispatchQueue - - // MARK: - Init - - public init( - coordinate: CLLocationCoordinate2D, - excludedFields: [ExcludableField]? = nil, - date: Date? = nil, - urlSession: URLSession = .shared, - finishingQueue: DispatchQueue = .main - ) { - self.coordinate = coordinate - self.excludedFields = excludedFields?.hp_nilIfEmpty() - self.date = date - self.urlSession = urlSession - self.finishingQueue = finishingQueue - } - - // MARK: - OpenWeatherRequest - - func makeURL(settings: OpenWeather.Settings) -> URL? { - URLBuilder.weatherBase - .addingPathComponent(date != nil ? "timemachine" : nil) - .addingQueryItem(name: "lat", value: coordinate.latitude, digits: 5) - .addingQueryItem(name: "lon", value: coordinate.longitude, digits: 5) - .addingQueryItem(name: "dt", value: date.flatMap({ Int($0.timeIntervalSince1970) })) - .addingQueryItem(name: "exclude", value: excludedFields?.compactMap({ $0.rawValue })) - .addingQueryItem(name: "appid", value: settings.apiKey) - .addingQueryItem(name: "units", value: settings.units.rawValue) - .addingQueryItem(name: "lang", value: settings.language.rawValue) - .build() - } - - func makeNetworkRequest(settings: OpenWeather.Settings) throws -> APINetworkRequest { - if let date = date, date < Date(), abs(date.timeIntervalSinceNow) <= 6 * .hour { - throw NSError.timeMachineDate - } - return APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) - } +import HPURLBuilder + +public struct WeatherRequest: Codable { + + // MARK: - Associated Types + + public typealias Output = WeatherResponse + + // MARK: - Properties + + public let coordinate: CLLocationCoordinate2D + public let excludedFields: [ExcludableField]? + public let date: Date? + + // MARK: - Init + + public init(coordinate: CLLocationCoordinate2D, excludedFields: [ExcludableField]? = nil, date: Date? = nil) { + self.coordinate = coordinate + self.excludedFields = excludedFields?.hp_nilIfEmpty() + self.date = date + } + + // MARK: - OpenWeatherRequest + + func makeURL(settings: OpenWeather.Settings) -> URL? { + URL.build { + Host("api.openweathermap.org") + PathComponent("data") + PathComponent("2.5") + 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: "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: "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 hp_nilIfEmpty() -> Self? { + isEmpty ? nil : self + } } extension TimeInterval { - static let minute = 60.00 - static let hour = 3600.00 - static let day = 86400.00 + static let minute = 60.00 + static let hour = 3600.00 + static let day = 86400.00 } diff --git a/Sources/HPOpenWeather/Response/WeatherResponse+Language.swift b/Sources/HPOpenWeather/Response/WeatherResponse+Language.swift new file mode 100644 index 0000000..624b882 --- /dev/null +++ b/Sources/HPOpenWeather/Response/WeatherResponse+Language.swift @@ -0,0 +1,54 @@ +import Foundation + +public extension WeatherResponse { + + /// The language that should be used in API responses for example for weather condition descriptions + 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" + } + +} diff --git a/Sources/HPOpenWeather/Response/WeatherResponse+Units.swift b/Sources/HPOpenWeather/Response/WeatherResponse+Units.swift new file mode 100644 index 0000000..38c0fbf --- /dev/null +++ b/Sources/HPOpenWeather/Response/WeatherResponse+Units.swift @@ -0,0 +1,41 @@ +import Foundation + +public extension WeatherResponse { + + /// The units that should the data in the API responses should be formatted in + 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 + + @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 + } + } + + } + +} diff --git a/Sources/HPOpenWeather/Response/WeatherResponse.swift b/Sources/HPOpenWeather/Response/WeatherResponse.swift index 536061a..3e1804e 100644 --- a/Sources/HPOpenWeather/Response/WeatherResponse.swift +++ b/Sources/HPOpenWeather/Response/WeatherResponse.swift @@ -1,24 +1,35 @@ 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: [Alert]? - - public var timezone: TimeZone { - TimeZone(identifier: timezoneIdentifier)! - } - + 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 + 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 d4ef5ae..1414432 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift @@ -14,46 +14,33 @@ final class HPOpenWeatherTests: XCTestCase { OpenWeather.shared.apiKey = nil } - func testCurrentRequest() { - let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30)) - let exp = XCTestExpectation(description: "Fetched data") - - OpenWeather.shared.schedule(request) { result in - exp.fulfill() - XCTAssertResult(result) - } - - wait(for: [exp], timeout: 10) + 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 + } } - func testTimeMachineRequestFailing() { - let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30), date: Date().addingTimeInterval(-1 * .hour)) - let exp = XCTestExpectation(description: "Fetched data") + func testTimeMachineRequestFailing() async throws { + let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-1 * .hour)) - OpenWeather.shared.schedule(request) { result in - exp.fulfill() - XCTAssertResultError(result) - } - - wait(for: [exp], timeout: 10) + await HPAssertThrowsError { + try await OpenWeather.shared.weatherResponse(request) + } } - func testTimeMachineRequest() { - let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30), date: Date().addingTimeInterval(-7 * .hour)) - let exp = XCTestExpectation(description: "Fetched data") - - OpenWeather.shared.schedule(request) { result in - exp.fulfill() - XCTAssertResult(result) - } + func testTimeMachineRequest() async { + let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050), date: Date().addingTimeInterval(-7 * .hour)) - wait(for: [exp], timeout: 10) + await HPAssertThrowsNoError { + try await OpenWeather.shared.weatherResponse(request) + } } - - @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testPublisher() { - let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30)) + let request = WeatherRequest(coordinate: .init(latitude: 52.5200, longitude: 13.4050)) let expectationFinished = expectation(description: "finished") let expectationReceive = expectation(description: "receiveValue") @@ -93,11 +80,27 @@ 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 { - print(error) - XCTFail(error.localizedDescription) + XCTFail(error.localizedDescription) } }