Support for parsing JSON into Dictionary with string backed enum as keys
This file contains 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
// | |
// CodableRawRepresentableKeyedDictionary.swift | |
// | |
// Created by Mattias Persson on 2019-09-17. | |
// Copyright © 2019 Mattias Persson. All rights reserved. | |
// | |
import Foundation | |
@dynamicMemberLookup | |
public struct CodableRawRepresentableKeyedDictionary<Key: Codable & Hashable & RawRepresentable, Value: Codable>: Codable where Key.RawValue: Codable & Hashable { | |
private var dictionary: [Key : Value] | |
// MARK: - Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
let rawKeyedDict = try container.decode([Key.RawValue : Value].self) | |
self.dictionary = rawKeyedDict.reduce(into: [Key : Value](), { result, element in | |
if let enumCase = Key(rawValue: element.key) { | |
result[enumCase] = element.value | |
} | |
}) | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
let rawKeyedDict = dictionary.reduce(into: [Key.RawValue : Value]()) { result, element in | |
result[element.key.rawValue] = element.value | |
} | |
try container.encode(rawKeyedDict) | |
} | |
} | |
public extension CodableRawRepresentableKeyedDictionary { | |
// Mark: - dynamicMemberLookup | |
subscript<U>(dynamicMember member: KeyPath<Dictionary<Key, Value>, U>) -> U { | |
get { return dictionary[keyPath: member] } | |
} | |
subscript<U>(dynamicMember member: WritableKeyPath<Dictionary<Key, Value>, U>) -> U { | |
get { return dictionary[keyPath: member] } | |
set { dictionary[keyPath: member] = newValue } | |
} | |
} | |
extension CodableRawRepresentableKeyedDictionary: ExpressibleByDictionaryLiteral { | |
// MARK: - ExpressibleByDictionaryLiteral | |
public init(dictionaryLiteral elements: (Key, Value)...) { | |
self.dictionary = elements.reduce(into: [Key : Value]()) { result, element in | |
result[element.0] = element.1 | |
} | |
} | |
} | |
public extension CodableRawRepresentableKeyedDictionary{ | |
// Mark: - subscript | |
subscript(key: Key) -> Value? { | |
get { return dictionary[key] } | |
set { dictionary[key] = newValue } | |
} | |
} | |
// MARK: - KeyPath does not support functions - redirect calls to the dictionary | |
public extension CodableRawRepresentableKeyedDictionary { | |
func map<T>(_ transform: ((key: Key, value: Value)) throws -> T) rethrows -> [T] | |
{ | |
try dictionary.map(transform) | |
} | |
func compactMap<ElementOfResult>(_ transform: ((key: Key, value: Value)) throws -> ElementOfResult?) rethrows -> [ElementOfResult] | |
{ | |
try dictionary.compactMap(transform) | |
} | |
func filter(_ isIncluded: (Dictionary<Key, Value>.Element) throws -> Bool) rethrows -> [Key : Value] | |
{ | |
try dictionary.filter(isIncluded) | |
} | |
func forEach(_ body: ((key: Key, value: Value)) throws -> Void) rethrows | |
{ | |
try dictionary.forEach(body) | |
} | |
mutating func removeValue(forKey key: Key) -> Value? | |
{ | |
dictionary.removeValue(forKey: key) | |
} | |
} | |
extension CodableRawRepresentableKeyedDictionary: Equatable where Key: Equatable, Value: Equatable { | |
// Mark: - Equatable | |
public static func == (lhs: CodableRawRepresentableKeyedDictionary<Key, Value>, | |
rhs: CodableRawRepresentableKeyedDictionary<Key, Value>) -> Bool | |
{ | |
lhs.dictionary == rhs.dictionary | |
} | |
} |
This file contains 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 XCTest | |
enum CreditKey: String, Codable, Equatable { | |
case actor | |
case director | |
case author | |
} | |
struct Content: Codable, Equatable { | |
let credits: CodableRawRepresentableKeyedDictionary<CreditKey, [String]> | |
} | |
class CodableRawRepresentableKeyedDictionaryTests: XCTestCase { | |
let json = """ | |
{ | |
"credits": { | |
"actor": [ | |
"Scarlett Johansson", | |
"Karen Gillan", | |
"Brie Larson" | |
], | |
"director": [ | |
"Joe Russo", | |
"Anthony Russo" | |
] | |
} | |
} | |
""" | |
override func setUp() { | |
// Put setup code here. This method is called before the invocation of each test method in the class. | |
} | |
override func tearDown() { | |
// Put teardown code here. This method is called after the invocation of each test method in the class. | |
} | |
private func verify(_ content: Content) { | |
let actorList = content.credits[.actor] | |
XCTAssert(actorList?.contains("Scarlett Johansson") == true) | |
XCTAssert(actorList?.contains("Karen Gillan") == true) | |
XCTAssert(actorList?.contains("Brie Larson") == true) | |
XCTAssert(actorList?.count == 3) | |
let directorList = content.credits[.director] | |
XCTAssert(directorList?.contains("Joe Russo") == true) | |
XCTAssert(directorList?.contains("Anthony Russo") == true) | |
XCTAssert(directorList?.count == 2) | |
XCTAssert(content.credits[.author] == nil) | |
} | |
func testJsonDecode() { | |
do { | |
let content = try JSONDecoder().decode(Content.self, from: json.data(using: .utf8)!) | |
verify(content) | |
} catch { | |
XCTAssert(false) | |
} | |
} | |
func testJsonEncode() { | |
do { | |
let content = try JSONDecoder().decode(Content.self, from: json.data(using: .utf8)!) | |
// Test that we can re-encode the data and then decode it again, i.e. test that we | |
// JSON encode to the same format as we can parse from. | |
let encoder = JSONEncoder() | |
let encodedData = try encoder.encode(content) | |
guard let newJson = String(data: encodedData, encoding: .utf8) else { return XCTAssert(false) } | |
let secondContent = try JSONDecoder().decode(Content.self, from: newJson.data(using: .utf8)!) | |
verify(secondContent) | |
XCTAssert(content == secondContent) | |
} catch { | |
XCTAssert(false) | |
} | |
} | |
func testDictionaryKeyPath() { | |
let creditMap = CodableRawRepresentableKeyedDictionary<CreditKey, [String]>() | |
XCTAssert(creditMap.count == 0) | |
} | |
func testDictionaryWritableKeyPath() { | |
var creditMap = CodableRawRepresentableKeyedDictionary<CreditKey, [String]>() | |
creditMap.writeable = 12 | |
XCTAssert(creditMap[.actor]?.first == "Number 12") | |
} | |
func testDictionaryLiteral() { | |
let creditMap: CodableRawRepresentableKeyedDictionary<CreditKey, [String]> = | |
[ | |
.actor : ["Sascha Baron Cohen", "Angelina Jolie"], | |
.author : ["William Shakespere"], | |
.director : ["Martin Scorsese"] | |
] | |
XCTAssert(creditMap.count == 3) | |
} | |
func testInsertDictionaryValue() { | |
var creditMap = CodableRawRepresentableKeyedDictionary<CreditKey, [String]>() | |
creditMap[.actor] = ["Karen Gillan"] | |
XCTAssert(creditMap.count == 1) | |
XCTAssert(creditMap[.actor]?.first == "Karen Gillan") | |
} | |
} | |
extension Dictionary where Key == CreditKey, Value == [String] { | |
// Bogus implementation to be able to test | |
// writing via WritableKeyPath | |
var writeable: Int { | |
get { return 11 } | |
set { self[.actor] = ["Number \(newValue)"] } | |
} | |
} |
Great to hear that it helped you out :-). I've just now added some Dictionary functions that I use a lot in my current project since DML doesn't work for functions in Swift (yet anyway). Was this the problem you had with DML?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I Couldn't get the DML to work, but this helped me immensely! THANKS