Skip to content

Instantly share code, notes, and snippets.

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