Last active
December 23, 2015 13:40
-
-
Save jon-cotton/03108f1127f504e978d4 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 | |
typealias MappingDataSource = [String : AnyObject] | |
// Mappable | |
protocol Mappable { | |
// mappable types must provide a way to be created without any dependencies | |
init() | |
// map and populate porperties from the supplied data provider | |
mutating func mapFromDataSource(dataSource: MappingDataSource) throws | |
} | |
extension Mappable { | |
init?(mappingDataSource: MappingDataSource) { | |
if let mappable = Self.createFromDataSource(mappingDataSource) { | |
self = mappable | |
} else { | |
return nil | |
} | |
} | |
private static func createFromDataSource(dataSource: MappingDataSource) -> Self? { | |
var mappableSelf = Self() | |
do { | |
try mappableSelf <- dataSource | |
} catch { | |
return nil | |
} | |
return mappableSelf | |
} | |
} | |
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: MappingDataSource?) throws { | |
if let dataSource = source { | |
try destination.mapFromDataSource(dataSource) | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- (inout destination: MappingDataSource, source: MappingDataSource?) throws { | |
if let source = source { | |
destination += source | |
} else { | |
throw MappingError() | |
} | |
} | |
// allows mapping a collection of mappables directly to an array of data sources | |
func <- <T where T:Mappable>(inout destination: [T], source: [MappingDataSource]?) throws { | |
if let dataSources = source { | |
var mappableCollection = [T]() | |
for dataSource in dataSources { | |
if let mappable = T(mappingDataSource: dataSource) { | |
mappableCollection.append(mappable) | |
} | |
} | |
destination = mappableCollection | |
} 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: (originalValue: U?, transformer: (originalValue: V) -> (T?))) throws { | |
if let | |
originalValueCorrectType = source.originalValue as? V, | |
transformedSource = source.transformer(originalValue: originalValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U,V>(inout destination: T!, source: (originalValue: U?, transformer: (originalValue: V) -> (T?))) throws { | |
if let | |
originalValueCorrectType = source.originalValue as? V, | |
transformedSource = source.transformer(originalValue: originalValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
func <- <T,U,V>(inout destination: T?, source: (originalValue: U?, transformer: (originalValue: V) -> (T?))) throws { | |
if let | |
originalValueCorrectType = source.originalValue as? V, | |
transformedSource = source.transformer(originalValue: originalValueCorrectType) | |
{ | |
destination = transformedSource | |
} else { | |
throw MappingError() | |
} | |
} | |
// recursively merges two dictionaries | |
func +=<T,U> (inout left: Dictionary<T,U>, right: Dictionary<T,U>) -> Dictionary<T,U> { | |
for (key,value) in right { | |
if var leftDict = left[key] as? Dictionary<T,U>, | |
let rightDict = right[key] as? Dictionary<T,U>, | |
let mergedDict = (leftDict += rightDict) as? U | |
{ | |
left[key] = mergedDict | |
} else { | |
left[key] = value | |
} | |
} | |
return left | |
} | |
// common transformers (closures in disguise) | |
struct Transformers { | |
static let dateFormatter = NSDateFormatter() | |
static let URL = { (originalValue: String) -> NSURL? in | |
return NSURL(string:originalValue) | |
} | |
static let Date = { (originalValue: String) -> NSDate? in | |
dateFormatter.dateFormat = "YYYY-MM-dd" | |
return dateFormatter.dateFromString(originalValue) | |
} | |
} | |
// Thread safe implementation of mappable protocol that serializes calls to mapFromDataSource | |
// 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() | |
required init() { | |
operationQueue.maxConcurrentOperationCount = 1 | |
} | |
func mappingOperation() -> ((dataSource: MappingDataSource) -> ()) { | |
return { (dataSource: MappingDataSource) in } | |
} | |
func mapFromDataSource(dataSource: MappingDataSource) { | |
mapFromDataSource(dataSource, completion: nil) | |
} | |
func mapFromDataSource(dataSource: MappingDataSource, completion: (() -> ())?) { | |
// force calls to this method to be performed in a serialized queue | |
let operation = NSBlockOperation(block: { self.mappingOperation()(dataSource: dataSource) }) | |
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? | |
private(set) var surveyData: MappingDataSource = [:] | |
override func mappingOperation() -> ((dataSource: MappingDataSource) -> ()) { | |
return { [unowned self] (dataSource: MappingDataSource) in | |
try? self.serviceAppHost <- dataSource["serviceAppHost"] | |
try? self.featureIsEnabled <- dataSource["featureIsEnabled"] | |
try? self.nillableValue <- dataSource["nillableValue"] | |
// these values should be transformed from the raw type first | |
try? self.featureReleaseDate <- (dataSource["featureReleaseDate"], { (originalValue: String) -> NSDate? in | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateFormat = "YYYY-MM-dd" | |
return dateFormatter.dateFromString(originalValue) | |
}) | |
try? self.someURL <- (dataSource["someURL"], { (originalValue: String) -> NSURL? in | |
return NSURL(string:originalValue) | |
}) | |
// Same thing, but using the supplied transformers | |
try? self.someOtherURL <- (dataSource["someOtherURL"], Transformers.URL) | |
try? self.anotherDate <- (dataSource["anotherDate"], Transformers.Date) | |
// store the survey data in its raw form for use later on | |
try? self.surveyData <- dataSource["surveyData"] as? MappingDataSource | |
} | |
} | |
} | |
////////////////////// | |
// 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 mapFromDataSource(dataSource: MappingDataSource) throws { | |
try self.questionText <- dataSource["questionText"] | |
try self.type <- dataSource["type"] | |
// we can still have a valid question if this data does not exist as we already have a default | |
try? self.shouldValidateAnswer <- dataSource["shouldValidateAnswer"] | |
} | |
} | |
struct Survey : Mappable { | |
private(set) var shouldShowSurvey = false | |
private(set) var surveyTitle = "This is a survey" | |
private(set) var questions: [SurveyQuestion] = [] | |
mutating func mapFromDataSource(dataSource: MappingDataSource) throws { | |
try self.shouldShowSurvey <- dataSource["shouldShowSurvey"] | |
try self.surveyTitle <- dataSource["surveyTitle"] | |
try self.questions <- dataSource["questions"] as? [MappingDataSource] | |
} | |
} | |
// Usage | |
// hard defaults | |
var appConfig = AppConfig() | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.someOtherURL | |
appConfig.anotherDate | |
appConfig.surveyData | |
let configData: MappingDataSource = [ | |
"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", | |
"surveyData": [ | |
"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"] | |
] | |
] | |
] | |
let remoteOverrides: MappingDataSource = [ | |
"serviceAppHost": "secure.sky.com", | |
"featureIsEnabled": false, | |
"surveyData": ["surveyTitle": "How did we do?"] | |
] | |
var survey = Survey() | |
appConfig.mapFromDataSource(configData) { | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.anotherDate | |
appConfig.surveyData | |
try? survey <- appConfig.surveyData | |
survey.surveyTitle | |
survey.shouldShowSurvey | |
survey.questions | |
survey.questions.count | |
// some remote overrides arrive at a later time | |
appConfig.mapFromDataSource(remoteOverrides) { | |
appConfig.featureIsEnabled | |
appConfig.serviceAppHost | |
appConfig.featureReleaseDate | |
appConfig.someURL | |
appConfig.anotherDate | |
appConfig.nillableValue | |
appConfig.surveyData | |
try? survey <- appConfig.surveyData | |
survey.surveyTitle | |
survey.shouldShowSurvey | |
survey.questions | |
survey.questions.count | |
} | |
} | |
// an example showing mapping to an immutable type, obviously this can't be updated with remote overrides later on | |
if let immutableSurvey = Survey(mappingDataSource: ["surveyTitle": "You can't change this!"]) { | |
immutableSurvey.surveyTitle | |
immutableSurvey.shouldShowSurvey | |
immutableSurvey.questions | |
immutableSurvey.questions.count | |
} | |
// testing how the initialiser behaves with a serialized write mapper, it's working here, but I suspect it will be possible to sometimes get back the default values if you try to read them immediately | |
if let someOtherConfig = AppConfig(mappingDataSource: ["serviceAppHost":"something"]) { | |
someOtherConfig.serviceAppHost | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment