Last active
March 28, 2019 05:44
-
-
Save dreymonde/3a7d877e5bb6fa698f4201fdd55f1850 to your computer and use it in GitHub Desktop.
Next generation of Mapper, chapter 2
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
import Foundation | |
enum IndexPathValue { | |
case index(Int) | |
case key(String) | |
} | |
protocol IndexPathElement { | |
var indexPathValue: IndexPathValue { get } | |
} | |
extension Int: IndexPathElement { | |
var indexPathValue: IndexPathValue { | |
return .index(self) | |
} | |
} | |
extension String: IndexPathElement { | |
var indexPathValue: IndexPathValue { | |
return .key(self) | |
} | |
} | |
protocol MapProtocol { | |
subscript(indexPath: IndexPathElement) -> Self? { get } | |
var asArray: [Self]? { get } | |
func get<T>() -> T? | |
} | |
extension MapProtocol { | |
subscript(indexPath: [IndexPathElement]) -> Self? { | |
get { | |
var result = self | |
for index in indexPath { | |
if let deeped = result[index] { | |
result = deeped | |
} else { | |
break | |
} | |
} | |
return result | |
} | |
} | |
} | |
protocol Mappable { | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws | |
} | |
protocol MappableWithContext: Mappable { | |
associatedtype Context | |
init<Map: MapProtocol>(mapper: ContextualMapper<Map, Context>) throws | |
} | |
extension MappableWithContext { | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
let contextual = ContextualMapper<Map, Context>(mapper.map, context: nil) | |
try self.init(mapper: contextual) | |
} | |
} | |
enum MapperError: Error { | |
case noValue(forIndexPath: [IndexPathElement]) | |
case wrongType | |
case cannotInitializeFromRawValue | |
case cannotRepresentAsArray | |
} | |
protocol MapperProtocol { | |
associatedtype Map: MapProtocol | |
var map: Map { get } | |
} | |
extension MapperProtocol { | |
fileprivate func deep(_ indexPath: [IndexPathElement]) throws -> Map { | |
if let value = map[indexPath] { | |
return value | |
} else { | |
print(map) | |
throw MapperError.noValue(forIndexPath: indexPath) | |
} | |
} | |
fileprivate func get<T>(from map: Map) throws -> T { | |
if let value: T = map.get() { | |
return value | |
} else { | |
throw MapperError.wrongType | |
} | |
} | |
fileprivate func array(_ map: Map) throws -> [Map] { | |
if let array = map.asArray { | |
return array | |
} else { | |
throw MapperError.cannotRepresentAsArray | |
} | |
} | |
func map<T>(from indexPath: IndexPathElement...) throws -> T { | |
let leveled = try deep(indexPath) | |
return try get(from: leveled) | |
} | |
func map<T: Mappable>(from indexPath: IndexPathElement...) throws -> T { | |
let leveled = try deep(indexPath) | |
return try T(mapper: Mapper(leveled)) | |
} | |
func map<T: MappableWithContext>(from indexPath: IndexPathElement..., usingContext context: T.Context) throws -> T { | |
let leveled = try deep(indexPath) | |
return try T(mapper: ContextualMapper(leveled, context: context)) | |
} | |
func map<T: RawRepresentable>(from indexPath: IndexPathElement...) throws -> T { | |
let leveled = try deep(indexPath) | |
let raw: T.RawValue = try get(from: leveled) | |
if let value = T(rawValue: raw) { | |
return value | |
} else { | |
throw MapperError.cannotInitializeFromRawValue | |
} | |
} | |
func map<T>(arrayFrom indexPath: IndexPathElement...) throws -> [T] { | |
let leveled = try deep(indexPath) | |
let array = try self.array(leveled) | |
return try array.map({ try get(from: $0) }) | |
} | |
func map<T: Mappable>(arrayFrom indexPath: IndexPathElement...) throws -> [T] { | |
let leveled = try deep(indexPath) | |
let array = try self.array(leveled) | |
return try array.map({ try T(mapper: Mapper($0)) }) | |
} | |
func map<T: MappableWithContext>(arrayFrom indexPath: IndexPathElement..., usingContext context: T.Context) throws -> [T] { | |
let leveled = try deep(indexPath) | |
let array = try self.array(leveled) | |
return try array.map({ try T(mapper: ContextualMapper($0, context: context)) }) | |
} | |
func map<T: RawRepresentable>(arrayFrom indexPath: IndexPathElement...) throws -> [T] { | |
let leveled = try deep(indexPath) | |
let array = try self.array(leveled) | |
return try array.map({ | |
let raw: T.RawValue = try get(from: $0) | |
if let value = T(rawValue: raw) { | |
return value | |
} else { | |
throw MapperError.cannotInitializeFromRawValue | |
} | |
}) | |
} | |
} | |
struct Mapper<Map: MapProtocol>: MapperProtocol { | |
let map: Map | |
init(_ map: Map) { | |
self.map = map | |
} | |
} | |
struct ContextualMapper<Map: MapProtocol, Context>: MapperProtocol { | |
let map: Map | |
let context: Context? | |
init(_ map: Map, context: Context?) { | |
self.map = map | |
self.context = context | |
} | |
func map<T: MappableWithContext where T.Context == Context>(withContextFrom indexPath: IndexPathElement...) throws -> T { | |
let leveled = try deep(indexPath) | |
return try T(mapper: ContextualMapper(leveled, context: context)) | |
} | |
func map<T: MappableWithContext where T.Context == Context>(arrayWithContextFrom indexPath: IndexPathElement...) throws -> [T] { | |
let leveled = try deep(indexPath) | |
let array = try self.array(leveled) | |
return try array.map({ try T(mapper: ContextualMapper($0, context: self.context)) }) | |
} | |
} | |
enum ThatThingOne: MapProtocol { | |
case number(Int) | |
case string(String) | |
case array([ThatThingOne]) | |
case dict([String: ThatThingOne]) | |
subscript(indexPath: IndexPathElement) -> ThatThingOne? { | |
guard case .key(let key) = indexPath.indexPathValue else { | |
return nil | |
} | |
if case .dict(let dict) = self { | |
return dict[key] | |
} else { | |
return nil | |
} | |
} | |
func get<T>() -> T? { | |
switch self { | |
case .number(let value as T): return value | |
case .string(let value as T): return value | |
case .array(let value as T): return value | |
case .dict(let value as T): return value | |
default: | |
return nil | |
} | |
} | |
var asArray: [ThatThingOne]? { | |
if case .array(let array) = self { | |
return array | |
} | |
return nil | |
} | |
} | |
extension Mappable { | |
init?(thatThingOne: ThatThingOne) { | |
let mapper = Mapper(thatThingOne) | |
try? self.init(mapper: mapper) | |
} | |
} | |
extension MappableWithContext { | |
init?(thatThingOne: ThatThingOne, withContext context: Context) { | |
let mapper = ContextualMapper(thatThingOne, context: context) | |
try? self.init(mapper: mapper) | |
} | |
} | |
enum StringAny: MapProtocol { | |
case dict([String: Any]) | |
case any(Any) | |
subscript(indexPath: IndexPathElement) -> StringAny? { | |
guard case .key(let key) = indexPath.indexPathValue else { | |
return nil | |
} | |
switch self { | |
case .dict(let dict): | |
return dict[key].map({ .any($0) }) | |
case .any(let any): | |
return (any as? [String: Any])?[key].map({ .any($0) }) | |
} | |
} | |
func get<T>() -> T? { | |
switch self { | |
case .any(let any): | |
return any as? T | |
case .dict(let dict): | |
return dict as? T | |
} | |
} | |
var asArray: [StringAny]? { | |
if case .any(let any) = self { | |
if let anies = any as? [Any] { | |
return anies.map({ .any($0) }) | |
} | |
if let dicts = any as? [[String: Any]] { | |
return dicts.map({ .dict($0) }) | |
} | |
return nil | |
} | |
return nil | |
} | |
} | |
extension Mappable { | |
init?(fromDictionary dictionary: [String: Any]) { | |
let mapper = Mapper(StringAny.dict(dictionary)) | |
try? self.init(mapper: mapper) | |
} | |
} | |
extension MappableWithContext { | |
init?(fromDictionary dictionary: [String: Any], withContext context: Context?) { | |
let mapper = ContextualMapper(StringAny.dict(dictionary), context: context) | |
try? self.init(mapper: mapper) | |
} | |
} | |
struct Human: Mappable { | |
let id: Int | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.id = try mapper.map(from: "id") | |
} | |
} | |
struct Top: Mappable { | |
let male: Human | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.male = try mapper.map(from: "human") | |
} | |
} | |
let human: [String: Any] = ["id": 5] | |
let repr: [String: Any] = ["human": human] | |
let top = Top(fromDictionary: repr)! | |
print(top) | |
// Top(male: Human(id: 5)) | |
let humanTTO = ThatThingOne.dict(["human": .dict(["id": .number(7)])]) | |
let topi = Top(thatThingOne: humanTTO)! | |
print(topi) | |
// Top(male: Human(id: 7)) | |
struct Guitar: Mappable { | |
enum Wood: String { | |
case black, brown | |
} | |
enum Strings: Int { | |
case six = 6, seven = 7 | |
} | |
let wood: Wood | |
let strings: Strings | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.wood = try mapper.map(from: "wood") | |
self.strings = try mapper.map(from: "strings") | |
} | |
} | |
let blackSeven: [String: Any] = ["wood": "black", "strings": 7] | |
let guitar = Guitar(fromDictionary: blackSeven)! | |
print(guitar) | |
// Guitar(wood: Guitar.Wood.black, strings: Guitar.Strings.seven) | |
struct Guitars: Mappable { | |
let guitars: [Guitar] | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.guitars = try mapper.map(arrayFrom: "guitars") | |
} | |
} | |
let brownSix: [String: Any] = ["wood": "brown", "strings": 6] | |
let guitarsDict: [[String: Any]] = [blackSeven, brownSix] | |
let guitars = Guitars(fromDictionary: ["guitars": guitarsDict])! | |
print(guitars) | |
// Guitars(guitars: [Guitar(wood: Guitar.Wood.black, strings: Guitar.Strings.seven), map.Guitar(wood: Guitar.Wood.brown, strings: Guitar.Strings.six)]) | |
struct Strings: Mappable { | |
let strings: [String] | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.strings = try mapper.map(arrayFrom: "strings") | |
} | |
} | |
let anyStrings: [Any] = ["Give", "Take"] | |
let mappedStrings = Strings(fromDictionary: ["strings": anyStrings])! | |
print(mappedStrings) | |
// Strings(strings: ["Give", "Take"]) | |
enum BatFamilyMappingContext { | |
case normal | |
case json | |
case mongo | |
} | |
struct Batman: MappableWithContext { | |
let coolnessRate: Int | |
init<Map: MapProtocol>(mapper: ContextualMapper<Map, BatFamilyMappingContext>) throws { | |
let context = mapper.context ?? .normal | |
switch context { | |
case .normal: | |
coolnessRate = try mapper.map(from: "coolnessRate") | |
case .json: | |
coolnessRate = try mapper.map(from: "coolness_rate") | |
case .mongo: | |
coolnessRate = try mapper.map(from: "rate") | |
} | |
} | |
} | |
struct BatMobile: MappableWithContext { | |
let batmanInside: Batman | |
init<Map: MapProtocol>(mapper: ContextualMapper<Map, BatFamilyMappingContext>) throws { | |
self.batmanInside = try mapper.map(withContextFrom: "batman") | |
} | |
} | |
let coolness: [String: Any] = ["coolness_rate": 99] | |
let batmanDict: [String: Any] = ["batman": coolness] | |
let batMobile = BatMobile(fromDictionary: batmanDict, withContext: .json)! | |
print(batMobile) | |
// BatMobile(batmanInside: Batman(coolnessRate: 99)) | |
struct RegularBatmobileAndString: Mappable { | |
let batmobile: BatMobile | |
let string: String | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.batmobile = try mapper.map(from: "batmobile", usingContext: .json) | |
self.string = try mapper.map(from: "string") | |
} | |
} | |
let regularBatmobileDict: [String: Any] = ["batmobile": batmanDict, "string": "Religion (Honeymoon, 2016)"] | |
let regularBatmobileAndString = RegularBatmobileAndString(fromDictionary: regularBatmobileDict)! | |
print(regularBatmobileAndString) | |
// RegularBatmobileAndString(batmobile: BatMobile(batmanInside: Batman(coolnessRate: 99)), string: "Religion (Honeymoon, 2016)") | |
struct DeepMapping: Mappable { | |
let deepString: String | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.deepString = try mapper.map(from: "deep", "deeper", "evenDeeper") | |
} | |
} | |
let evenDeeper: [String: Any] = ["evenDeeper": "heeveear"] | |
let deeper: [String: Any] = ["deeper": evenDeeper] | |
let deep: [String: Any] = ["deep": deeper] | |
let deepString = DeepMapping(fromDictionary: deep)! | |
print(deepString) | |
// DeepMapping(deepString: "heeveear") | |
struct JustDict: Mappable { | |
let dict: [String: Any] | |
init<Map: MapProtocol>(mapper: Mapper<Map>) throws { | |
self.dict = try mapper.map() | |
} | |
} | |
let jdd: [String: Any] = ["A": 5, "B": "b"] | |
let jdict = JustDict(fromDictionary: jdd)! | |
print(jdict) | |
// JustDict(dict: ["B": "b", "A": 5]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment