Created
September 20, 2016 03:31
-
-
Save maxchuquimia/21c26930fc89b6f83933fbfa937f1d04 to your computer and use it in GitHub Desktop.
Seralizing JSON in Swift without casting
This file contains hidden or 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
// | |
// Final implementation, as described in | |
// http://nspasteboard.com/2016/09/09/reducing-the-number-of-lines-used-to-serialize-from-json/ | |
// | |
// This example has been updated to Swift 3.0 (Xcode 8S193k) | |
// | |
import Foundation | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// Implementation (required) | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
typealias JSON = [String: AnyObject] | |
/// Provides a way to track the success or failure of a task | |
enum Response<O> { | |
case Success(value: O) | |
case Failure(error: NSError) | |
/// A simple way to create a failure | |
static func FailureMessage(message: String) -> Response { | |
let error = NSError(domain: "", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) | |
return .Failure(error: error) | |
} | |
/// If `self` is `.Success(_)`, returns `O`, else returns `nil` | |
var successValue: O? { | |
switch self { | |
case let .Success(val): return val | |
default: return nil | |
} | |
} | |
/// If `self` is `.Success(_)`, returns `O`, else throws an error | |
func value() throws -> O { | |
switch self { | |
case let .Success(val): | |
return val | |
case let .Failure(error): | |
throw error | |
} | |
} | |
} | |
/// A protocol defining an object that can be serialised from a JSON dictionary | |
protocol JSONSerializable { | |
static func with(json: JSON) -> Response<Self> | |
} | |
/// A function that takes a function and converts it's return value or thrown value into a `Response` | |
func response<T>(block: ((Void) throws -> T)) -> Response<T> { | |
let val: T | |
do { | |
val = try block() | |
} catch let e as NSError { | |
print("Response<\(T.self)> Error: \(e.localizedDescription)") | |
return .Failure(error: e) | |
} | |
return .Success(value: val) | |
} | |
extension Sequence where Iterator.Element == JSON { | |
func flatMapResponse<T>(block: ((Iterator.Element) -> Response<T>)) -> [T] { | |
return self.map(block).flatMap({$0.successValue}) | |
} | |
} | |
func flatMapJSONSerializable<T, X where X: JSONSerializable>(json: [JSON], object: X.Type) -> [T] { | |
return json.map(object.with).flatMap({$0.successValue as? T}) | |
} | |
extension Dictionary { | |
/// Base getter for primitive non-optionals | |
func get<T>(_ x: Key) throws -> T { | |
guard let o = self[x] as? T else { | |
throw NSError(domain: "", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot parse `\(x)` as `\(T.self)`"]) | |
} | |
return o | |
} | |
/// Base getter for primitive optionals | |
func getOptional<T>(_ x: Key) -> T? { | |
return self[x] as? T | |
} | |
/// Getter for JSONSerializable non-optionals | |
func get<T where T: JSONSerializable>(_ x: Key) throws -> T { | |
do { | |
return try T.with(json: get(x)).value() | |
} catch let e as NSError { | |
throw e | |
} | |
} | |
/// Getter for JSONSerializable optionals | |
func getOptional<T where T: JSONSerializable>(_ x: Key) -> T? { | |
return response { | |
return try T.with(json: self.get(x)).value() | |
} | |
.successValue | |
} | |
/// Getter for [JSONSerializable] non-optionals | |
func get<A where A: JSONSerializable >(_ x: Key) throws -> [A] { | |
do { | |
return try flatMapJSONSerializable(json: get(x), object: A.self) | |
} catch let e { | |
throw e | |
} | |
} | |
/// Getter for [JSONSerializable] optionals | |
func getOptional<A where A: JSONSerializable >(_ x: Key) -> [A]? { | |
return response { | |
return try flatMapJSONSerializable(json: self.get(x), object: A.self) | |
} | |
.successValue | |
} | |
} | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// Test Models | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
struct RegionModel: JSONSerializable { | |
struct Location: JSONSerializable { | |
let lat: Double | |
let lon: Double | |
let radius: Double | |
static func with(json: JSON) -> Response<Location> { | |
return response { | |
return try Location( | |
lat: json.get("lat"), | |
lon: json.get("lon"), | |
radius: json.get("radius") | |
) | |
} | |
} | |
} | |
let identifier: Int | |
let title: String | |
let message: String | |
let location: Location | |
static func with(json: JSON) -> Response<RegionModel> { | |
return response { | |
return try RegionModel( | |
identifier: json.get("id"), | |
title: json.get("title"), | |
message: json.get("message"), | |
location: json.get("location") | |
) | |
} | |
} | |
} | |
struct RegionPage: JSONSerializable { | |
let items: [RegionModel] | |
let hasMoreRows: Bool | |
static func with(json: JSON) -> Response<RegionPage> { | |
return response { | |
return try RegionPage( | |
items: json.get("items"), | |
hasMoreRows: json.get("hasMoreRows") | |
) | |
} | |
} | |
} | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// Test Data | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
let json: JSON = [ | |
"id": 57, | |
"title": "Restricted Region", | |
"message": "You may not fly in this area.", | |
"location": [ | |
"lat": -33.865143, | |
"lon": 151.209900, | |
"radius": 3500.0 | |
] | |
] | |
let pageJSON: JSON = [ | |
"items" : [ | |
json, | |
json | |
], | |
"hasMoreRows": false | |
] | |
print(RegionPage.with(json: pageJSON)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment