Skip to content

Instantly share code, notes, and snippets.

@jon-cotton
Last active December 22, 2015 23:21
Show Gist options
  • Save jon-cotton/e7971ce6ae1c4f77f388 to your computer and use it in GitHub Desktop.
Save jon-cotton/e7971ce6ae1c4f77f388 to your computer and use it in GitHub Desktop.
// 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