Last active
August 4, 2021 16:52
-
-
Save ajjames/8f48f3869ae07698546cfc778024a965 to your computer and use it in GitHub Desktop.
Earthquake Service Demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import Foundation | |
// MARK: - Supporting Models: Once per codebase | |
enum HTTPMethod: String { | |
case delete | |
case get | |
case post | |
case put | |
} | |
enum HTTPHeaderKey: String { | |
case accept = "Accept" | |
case acceptEncoding = "Accept-Encoding" | |
case authorization = "Authorization" | |
case connection = "Connection" | |
case contentType = "Content-Type" | |
case username = "username" | |
case host = "Host" | |
case ocpApimSubscriptionKey = "Ocp-Apim-Subscription-Key" | |
case userAgent = "User-Agent" | |
case xApiToken = "X-API-Token" | |
} | |
enum HTTPHeaderValue: String { | |
case any = "*/*" | |
case gzip = "gzip" | |
case hostName = "com.ah4r.primo" | |
case keepAlive = "keep-alive" | |
case vndAPIandJSON = "application/vnd.api+json" | |
case JSON = "application/json" | |
case octetStream = "application/octet-stream" | |
case xWWWFormURLEncoded = "application/x-www-form-urlencoded" | |
} | |
enum HTTPMIMEType: String { | |
case imageJpg = "image/jpeg" | |
} | |
enum ServiceError: Error, Equatable { | |
case unauthorized | |
case sessionError(innerError: Error) | |
case badResponse(message: String) | |
case decodeError(dataString: String) | |
case unexpectedResponse(statusCode: Int) | |
init(_ error: Error) { | |
if let serviceError = error as? ServiceError { | |
self = serviceError | |
} else { | |
self = .sessionError(innerError: error) | |
} | |
} | |
init(statusCode: Int) { | |
switch statusCode { | |
case 401: | |
self = .unauthorized | |
default: | |
self = .unexpectedResponse(statusCode: statusCode) | |
} | |
} | |
static func == (lhs: ServiceError, rhs: ServiceError) -> Bool { | |
switch (lhs, rhs) { | |
case (Self.unauthorized, Self.unauthorized): | |
return true | |
case (Self.sessionError(let lError), Self.sessionError(let rError)): | |
return lError.localizedDescription == rError.localizedDescription | |
case (Self.badResponse(let lString), Self.badResponse(let rString)), | |
(Self.decodeError(let lString), Self.decodeError(let rString)): | |
return lString == rString | |
case (Self.unexpectedResponse(let lStatusCode), Self.unexpectedResponse(let rStatusCode)): | |
return lStatusCode == rStatusCode | |
default: | |
return false | |
} | |
} | |
} | |
// MARK: - Protocol | |
protocol SomeService: AnyObject { | |
var publisherForNewData: AnyPublisher<GeoJSON, ServiceError> { get } | |
} | |
// MARK: - Model | |
struct SomeServiceResponse: Codable { | |
let id: String | |
} | |
// MARK: - Default | |
class SomeServiceDefault: SomeService { | |
private var baseURL = URL(string: "https://earthquake.usgs.gov")! | |
private var path = "/earthquakes/feed/v1.0/summary/all_day.geojson" | |
private let httpMethod = HTTPMethod.get | |
private var allHTTPHeaderFields: [String: String] { | |
return [ | |
HTTPHeaderKey.contentType.rawValue: HTTPHeaderValue.JSON.rawValue | |
] | |
} | |
private var httpBody: Data? { | |
let bodyString = "" | |
return bodyString.data(using: .utf8) | |
} | |
private var queryItems: [URLQueryItem] { | |
return [] | |
} | |
private var request: URLRequest { | |
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60) | |
request.httpMethod = httpMethod.rawValue | |
request.allHTTPHeaderFields = allHTTPHeaderFields | |
request.httpBody = httpBody | |
return request | |
} | |
private var url: URL { | |
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)! | |
components.path = path | |
components.queryItems = queryItems | |
return components.url! | |
} | |
var publisherForNewData: AnyPublisher<GeoJSON, ServiceError> { | |
return URLSession.shared.dataTaskPublisher(for: request) | |
.retry(1) | |
.map { $0.data } | |
.decode(type: GeoJSON.self, decoder: JSONDecoder()) | |
.mapError({ ServiceError($0) }) | |
.eraseToAnyPublisher() | |
} | |
} | |
// MARK: - Demo model | |
struct GeoJSON: Decodable { | |
private enum RootCodingKeys: String, CodingKey { case features } | |
private enum FeatureCodingKeys: String, CodingKey { case properties, geometry } | |
// Pretend that the data source is an array of dictionaries. | |
// The keys must have the same name as the attributes of the Quake entity. | |
private(set) var quakePropertiesList = [[String: Any]]() | |
init(from decoder: Decoder) throws { | |
let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self) | |
var featuresContainer = try rootContainer.nestedUnkeyedContainer(forKey: .features) | |
while featuresContainer.isAtEnd == false { | |
let nestedContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self) | |
let properties = try nestedContainer.decode(QuakeProperties.self, forKey: .properties) | |
guard properties.isValid() | |
else { | |
print("Ignored: " + "code = \(properties.code ?? ""), mag = \(properties.mag ?? 0) " + | |
"place = \(properties.place ?? ""), time = \(properties.time ?? 0)") | |
continue | |
} | |
let geometry = try nestedContainer.decode(QuakeGeometry.self, forKey: .geometry) | |
// Ignore invalid earthquake data. | |
if geometry.isValid() { | |
var propertiesAndGeometry = properties.dictionary | |
propertiesAndGeometry.merge(geometry.dictionary) { a, _ in return a } | |
quakePropertiesList.append(propertiesAndGeometry) | |
} else { | |
print("Ignored: \(geometry.coordinates)") | |
} | |
} | |
} | |
} | |
struct QuakeProperties: Decodable { | |
let mag: Float? // 1.9 | |
let place: String? // "21km ENE of Honaunau-Napoopoo, Hawaii" | |
let time: Double? // 1539187727610 | |
let code: String? // "70643082" | |
func isValid() -> Bool { | |
return (mag != nil && place != nil && code != nil && time != nil) ? true : false | |
} | |
// The keys must have the same name as the attributes of the Quake entity. | |
var dictionary: [String: Any] { | |
return ["magnitude": mag ?? 0, | |
"place": place ?? "", | |
"time": Date(timeIntervalSince1970: TimeInterval(time ?? 0) / 1000), | |
"code": code ?? ""] | |
} | |
} | |
struct QuakeGeometry: Decodable { | |
let coordinates: [Double] | |
var dictionary: [String: Any] { | |
return ["lat": coordinates[1], | |
"long": coordinates[0], | |
"depth": coordinates[2]] | |
} | |
func isValid() -> Bool { | |
return coordinates.count == 3 | |
} | |
} | |
// MARK: - Run code | |
let cancellable = SomeServiceDefault() | |
.publisherForNewData | |
.print() | |
.sink { completion in | |
print("completion \(completion)") | |
} receiveValue: { response in | |
print("response: \(response)") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment