Skip to content

Instantly share code, notes, and snippets.

@ajjames
Last active August 4, 2021 16:52
Show Gist options
  • Save ajjames/8f48f3869ae07698546cfc778024a965 to your computer and use it in GitHub Desktop.
Save ajjames/8f48f3869ae07698546cfc778024a965 to your computer and use it in GitHub Desktop.
Earthquake Service Demo
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