Skip to content

Instantly share code, notes, and snippets.

@snoozemoose
Last active September 19, 2019 15:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save snoozemoose/e4c8b683257d7c50057eb17c6f1b434b to your computer and use it in GitHub Desktop.
Save snoozemoose/e4c8b683257d7c50057eb17c6f1b434b to your computer and use it in GitHub Desktop.
Support for parsing JSON into Dictionary with string backed enum as keys
//
// 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
}
}
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)"] }
}
}
@snoozemoose
Copy link
Author

snoozemoose commented Sep 19, 2019

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