Last active
December 22, 2015 23:21
-
-
Save jon-cotton/e7971ce6ae1c4f77f388 to your computer and use it in GitHub Desktop.
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
// Copy and Paste into a Swift playground | |
import Foundation | |
import XCPlayground | |
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true | |
// Data Source (anything that implements subscripting for accessing data) | |
//protocol MapDataProviding { | |
// typealias Key: Hashable | |
// typealias Value | |
// | |
// subscript (key: Key) -> Value? {get} | |
//} | |
//extension Dictionary : MapDataProviding {} | |
typealias MapDataProviding = Dictionary<String, AnyObject> | |
// Mappable | |
protocol Mappable { | |
//typealias DataProvider: MapDataProviding | |
// map and populate porperties from the supplied data provider | |
mutating func mapFromDataProvider(dataProvider: MapDataProviding) throws | |
} | |
struct MappingError : ErrorType {} | |
// Mapping operator | |
// It's pretty rubbish that these have to be defined multiple times to cope with optionals, I'm sure there's a better way to do this, I just haven't come across it yet | |
infix operator <- {} | |
func <- <T,U>(inout destination: T, source: U?) throws { | |
if let sourceSameType = source as? T { | |
destination = sourceSameType | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T where T:Mappable>(inout destination: T, source: MapDataProviding) throws { | |
// if let dataProvider = source { | |
// destination | |
try destination.mapFromDataProvider(source) | |
// } else { | |
// throw MappingError() | |
// } | |
} | |
func <- <T,U>(inout destination: T!, source: U?) throws { | |
if let sourceSameType = source as? T { | |
destination = sourceSameType | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U>(inout destination: T?, source: U?) throws { | |
if let sourceSameType = source as? T { | |
destination = sourceSameType | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U,V>(inout destination: T, source: (rawValue: U?, transformer: (rawValue: V) -> (T?))) throws { | |
if let | |
rawValueCorrectType = source.rawValue as? V, | |
transformedSource = source.transformer(rawValue: rawValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U,V>(inout destination: T!, source: (rawValue: U?, transformer: (rawValue: V) -> (T?))) throws { | |
if let | |
rawValueCorrectType = source.rawValue as? V, | |
transformedSource = source.transformer(rawValue: rawValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U,V>(inout destination: T?, source: (rawValue: U?, transformer: (rawValue: V) -> (T?))) throws { | |
if let | |
rawValueCorrectType = source.rawValue as? V, | |
transformedSource = source.transformer(rawValue: rawValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
// common transformers (closures in disguise) | |
struct Transformers { | |
static let URL = { (rawValue: String) -> NSURL? in | |
return NSURL(string:rawValue) | |
} | |
static let Date = { (rawValue: String) -> NSDate? in | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateFormat = "YYYY-MM-dd" | |
return dateFormatter.dateFromString(rawValue) | |
} | |
} | |
// Base implementation of mappable protocol that's thread safe as it serializes calls to mapFromDataProvider | |
// The only issue with this approach is that it can't be failable because the actual mapping is carried out within an operation block | |
class MappableWithSerializedWrite : Mappable { | |
private let operationQueue = NSOperationQueue() | |
init() { | |
operationQueue.maxConcurrentOperationCount = 1 | |
} | |
func mappingOperation() -> ((dataProvider: [String : AnyObject]) -> ()) { | |
return { (dataProvider: [String : AnyObject]) in } | |
} | |
func mapFromDataProvider(dataProvider: [String : AnyObject]) { | |
mapFromDataProvider(dataProvider, completion: nil) | |
} | |
func mapFromDataProvider(dataProvider: [String : AnyObject], completion: (() -> ())?) { | |
// force calls to this method to be performed in a serialized queue | |
let operation = NSBlockOperation(block: { self.mappingOperation()(dataProvider: dataProvider) }) | |
if let completionCallback = completion { | |
operation.completionBlock = completionCallback | |
} | |
operationQueue.addOperation(operation) | |
} | |
} | |
///////////////////// | |
// App Config | |
///////////////////// | |
class AppConfig : MappableWithSerializedWrite { | |
private(set) var serviceAppHost = "www.bbc.co.uk" | |
private(set) var featureIsEnabled = false | |
private(set) var nillableValue: String? | |
private(set) var featureReleaseDate: NSDate? | |
private(set) var someURL = NSURL(string:"https://www.yahoo.com/some/path")! | |
private(set) var someOtherURL = NSURL(string:"https://www.yahoo.com/some/other/path") | |
private(set) var anotherDate: NSDate? | |
override func mappingOperation() -> ((dataProvider: [String : AnyObject]) -> ()) { | |
return { [unowned self] (dataProvider: [String : AnyObject]) in | |
try? self.serviceAppHost <- dataProvider["serviceAppHost"] | |
try? self.featureIsEnabled <- dataProvider["featureIsEnabled"] | |
try? self.nillableValue <- dataProvider["nillableValue"] | |
// these values should be transformed from the raw type first | |
try? self.featureReleaseDate <- (dataProvider["featureReleaseDate"], { (rawValue: String) -> NSDate? in | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateFormat = "YYYY-MM-dd" | |
return dateFormatter.dateFromString(rawValue) | |
}) | |
try? self.someURL <- (dataProvider["someURL"], { (rawValue: String) -> NSURL? in | |
return NSURL(string:rawValue) | |
}) | |
// Same thing, but using the supplied transformers | |
try? self.someOtherURL <- (dataProvider["someOtherURL"], Transformers.URL) | |
try? self.anotherDate <- (dataProvider["anotherDate"], Transformers.Date) | |
} | |
} | |
} | |
// Usage | |
// hard defaults | |
var appConfig = AppConfig() | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.someOtherURL | |
appConfig.anotherDate | |
// mapped from configData | |
let configData = [ | |
"serviceAppHost": "www.google.com", | |
"featureIsEnabled": true, | |
"featureReleaseDate": "2016-01-31", | |
"someURL": "https://www.sky.com", | |
"someOtherURL": "https://www.sky.com/a/path/to/something", | |
"anotherDate": "2015-12-01" | |
] | |
appConfig.mapFromDataProvider(configData) { | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.anotherDate | |
} | |
try? appConfig <- [ | |
"nillableValue": "2099-01-31" | |
] | |
// some remote overrides arrive at a later time | |
let remoteOverrides = [ | |
"serviceAppHost": "secure.sky.com", | |
"featureIsEnabled": false | |
] | |
appConfig.mapFromDataProvider(remoteOverrides) { | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.anotherDate | |
appConfig.nillableValue | |
} | |
////////////////////// | |
// Survey | |
////////////////////// | |
// Unlike the config objects, Survey Questions should only exist if they map from the data source correctly as they don't have hard coded defaults, | |
// therefore they implement the Mappable protocol directly themselves and throw an error if any mapping error occurs | |
struct SurveyQuestion : Mappable { | |
private(set) var questionText: String! | |
private(set) var type: String! | |
private(set) var shouldValidateAnswer = false | |
mutating func mapFromDataProvider(dataProvider: [String : AnyObject]) throws { | |
try self.questionText <- dataProvider["questionText"] | |
try self.type <- dataProvider["type"] | |
// we can still have a valid question if this data does not exist as we already have a default | |
try? self.shouldValidateAnswer <- dataProvider["shouldValidateAnswer"] | |
} | |
} | |
class ChatExitSurveyConfig : MappableWithSerializedWrite { | |
private(set) var shouldShowSurvey = false | |
private(set) var surveyTitle = "This is a survey" | |
private(set) var questions: [SurveyQuestion] = [] | |
override func mappingOperation() -> ((dataProvider: [String : AnyObject]) -> ()) { | |
return { [unowned self] (dataProvider: [String : AnyObject]) in | |
try? self.shouldShowSurvey <- dataProvider["shouldShowSurvey"] | |
try? self.surveyTitle <- dataProvider["surveyTitle"] | |
try? self.questions <- (dataProvider["questions"], Transformers.SurveyQuestions) | |
} | |
} | |
} | |
extension Transformers { | |
static let SurveyQuestions = { (rawValue: [[String : AnyObject]]) -> [SurveyQuestion]? in | |
var questions: [SurveyQuestion] = [] | |
for questionData in rawValue { | |
do { | |
// here, we make use of the fact that Survey Questions throw an error on any mapping failure | |
var question = SurveyQuestion() | |
try question <- questionData | |
questions.append(question) | |
} catch { | |
print("Mapping Error: Failed to create question with data: \(questionData)") | |
} | |
} | |
return questions | |
} | |
} | |
let surveyConfig = ChatExitSurveyConfig() | |
surveyConfig.shouldShowSurvey | |
surveyConfig.questions | |
surveyConfig.questions.count | |
let surveyOverrides = [ | |
"shouldShowSurvey": true, | |
"questions": [ | |
["quesschunTEXT": "Who?", "type": "Multiple Choice"], // This one will fail to be created as it doesn't contain a questionText key | |
["questionText": "How Many?", "type": "Slider"], | |
["questionText": "Why?", "type": "Multiple Choice", "shouldValidateAnswer": true], | |
["questionText": "Tell us more", "type": "Free Text"] | |
] | |
] | |
surveyConfig.mapFromDataProvider(surveyOverrides) { | |
surveyConfig.surveyTitle | |
surveyConfig.shouldShowSurvey | |
surveyConfig.questions | |
surveyConfig.questions.count | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment