Last active
September 22, 2020 10:13
-
-
Save GeekAndDad/9b5dc571cb7c724c0f655d06951622e8 to your computer and use it in GitHub Desktop.
@chunkyguy on twitter said they were facing a similar challenge to one I was facing, but their challenge actually seemed a little different and potentially easier. So I explored their challenge and created this solution.
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
ParseEngine(parsedObjects: [ | |
Text: | |
string: Text Value 1 | |
font size: 12, | |
Image: | |
at: http://disney.com/images/image1.png | |
width: 300 | |
height: 300, | |
Text: | |
string: Text Value 2 | |
font size: 14]) |
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
// | |
// TestDecodingFunTests.swift | |
// TestDecodingFunTests | |
// | |
// Created by Dad on 9/22/20. | |
// | |
// Poster wanted to be able to parse (visual) "components" out of a json file that would | |
// have various components of various types. | |
// Didn't want to have to modify the parser every time they added a new component type. | |
// So this is what I came up with in a flurry of code well past when I should have gone to sleep... | |
// (so pardon the names and less then beautiful code - it's a proof of concept for educational purposes). | |
import XCTest | |
public struct DynamicCodingKeys: CodingKey { | |
public var stringValue: String | |
public var intValue: Int? | |
public init?(stringValue: String) { | |
self.stringValue = stringValue | |
} | |
public init?(intValue: Int) { | |
self.intValue = intValue | |
stringValue = "\(intValue)" | |
} | |
} | |
protocol Component: Decodable { | |
static var type: String { get } | |
init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
} | |
class Text: Component, Decodable { | |
var value: String | |
var size: Int | |
// protocol conformance: | |
static let type: String = "text" | |
typealias ComponentCodingKeys = TextCodingKeys | |
enum TextCodingKeys: String, CodingKey { | |
case value, size | |
} | |
required init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
{ | |
let nestedContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: key) | |
value = try nestedContainer.decode(String.self, forKey: .value) | |
size = try nestedContainer.decode(Int.self, forKey: .size) | |
} | |
} | |
extension Text: CustomStringConvertible { | |
var description: String { | |
""" | |
\nText: | |
string: \(value) | |
font size: \(size) | |
""" | |
} | |
} | |
class Image: Component, Decodable { | |
var url: String | |
var width: Int | |
var height: Int | |
// protocol conformance: | |
static let type: String = "image" | |
typealias ComponentCodingKeys = ImageCodingKeys | |
enum ImageCodingKeys: String, CodingKey { | |
case url, width, height | |
} | |
required init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
{ | |
let nestedContainer = try container.nestedContainer(keyedBy: ImageCodingKeys.self, forKey: key) | |
url = try nestedContainer.decode(String.self, forKey: .url) | |
width = try nestedContainer.decode(Int.self, forKey: .width) | |
height = try nestedContainer.decode(Int.self, forKey: .height) | |
} | |
} | |
extension Image: CustomStringConvertible { | |
var description: String { | |
""" | |
\nImage: | |
at: \(url) | |
width: \(width) | |
height: \(height) | |
""" | |
} | |
} | |
enum TypeCodingKeys: String, CodingKey { | |
case type | |
} | |
struct ParseEngine: Decodable { | |
static var registeredComponentTypes: [String: Component.Type] = [:] | |
var parsedObjects: [Component] = [] | |
init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: DynamicCodingKeys.self) | |
for key in container.allKeys { | |
let nestedContainer = try container.nestedContainer(keyedBy: TypeCodingKeys.self, forKey: key) | |
if let typeString = try? nestedContainer.decode(String.self, forKey: .type) { | |
if let theType = Self.registeredComponentTypes[typeString] { | |
do { | |
if let foo = try theType.init(from: container, key: key) { | |
parsedObjects.append(foo) | |
} | |
} catch { | |
print("\nERROR parsing key: \"\(key.stringValue)\" as \"\(typeString)\" type. Skipping and trying next key.\n") | |
} | |
} | |
} | |
} | |
} | |
} | |
class TestDecodingFunTests: XCTestCase { | |
override func setUpWithError() throws { | |
// Put setup code here. This method is called before the invocation of each test method in the class. | |
} | |
override func tearDownWithError() throws { | |
// Put teardown code here. This method is called after the invocation of each test method in the class. | |
} | |
func testExample() throws { | |
let testJSON = """ | |
{ | |
"key1" : { "type" : "text", "value" : "Text Value 1", "size": 12 }, | |
"key2" : { "type" : "image", "url" : "http://disney.com/images/image1.png", "width": 300, "height": 300 }, | |
"key3" : { "type" : "text", "value" : "Text Value 2", "size" : 14 } | |
} | |
""".data(using: .utf8)! | |
ParseEngine.registeredComponentTypes[Image.type] = Image.self | |
ParseEngine.registeredComponentTypes[Text.type] = Text.self | |
let decoder = JSONDecoder() | |
let obj = try decoder.decode(ParseEngine.self, from: testJSON) | |
print(obj) | |
XCTAssertEqual(obj.parsedObjects.count, 3) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Text != nil }).count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Image != nil }).count, 1) | |
} | |
func testExampleErrorReporting() throws { | |
let testJSON = """ | |
{ | |
"key1" : { "type" : "text", "value" : "Text Value 1", "size": 12 }, | |
"key2" : { "type" : "image", "BAD_BAD_BAD_url" : "http://disney.com/images/image1.png", "width": 300, "height": 300 }, | |
"key3" : { "type" : "text", "value" : "Text Value 2", "size" : 14 } | |
} | |
""".data(using: .utf8)! | |
ParseEngine.registeredComponentTypes[Image.type] = Image.self | |
ParseEngine.registeredComponentTypes[Text.type] = Text.self | |
let decoder = JSONDecoder() | |
let obj = try decoder.decode(ParseEngine.self, from: testJSON) | |
print(obj) | |
XCTAssertEqual(obj.parsedObjects.count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Text != nil }).count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Image != nil }).count, 0) | |
} | |
} |
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
// | |
// TestDecodingFunTests.swift | |
// TestDecodingFunTests | |
// | |
// Created by Dad on 9/22/20. | |
// | |
// THIS VERSION Modified to use `struct`s instead of classes. Still works! | |
// Poster @chunkyguy wanted to be able to parse (visual) "components" out of a json file that would | |
// have various components of various types. | |
// Didn't want to have to modify the parser every time they added a new component type. | |
// So this is what I came up with in a flurry of code well past when I should have gone to sleep... | |
// (so pardon the names and less then beautiful code - it's a proof of concept for educational purposes). | |
import XCTest | |
public struct DynamicCodingKeys: CodingKey { | |
public var stringValue: String | |
public var intValue: Int? | |
public init?(stringValue: String) { | |
self.stringValue = stringValue | |
} | |
public init?(intValue: Int) { | |
self.intValue = intValue | |
stringValue = "\(intValue)" | |
} | |
} | |
protocol Component: Decodable { | |
static var type: String { get } | |
init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
} | |
struct Text: Component, Decodable { | |
var value: String | |
var size: Int | |
// protocol conformance: | |
static let type: String = "text" | |
typealias ComponentCodingKeys = TextCodingKeys | |
enum TextCodingKeys: String, CodingKey { | |
case value, size | |
} | |
init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
{ | |
let nestedContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: key) | |
value = try nestedContainer.decode(String.self, forKey: .value) | |
size = try nestedContainer.decode(Int.self, forKey: .size) | |
} | |
} | |
extension Text: CustomStringConvertible { | |
var description: String { | |
""" | |
\nText: | |
string: \(value) | |
font size: \(size) | |
""" | |
} | |
} | |
struct Image: Component, Decodable { | |
var url: String | |
var width: Int | |
var height: Int | |
// protocol conformance: | |
static let type: String = "image" | |
typealias ComponentCodingKeys = ImageCodingKeys | |
enum ImageCodingKeys: String, CodingKey { | |
case url, width, height | |
} | |
init?<T, K>(from container: T, key: K) throws | |
where T: KeyedDecodingContainerProtocol, K == T.Key | |
{ | |
let nestedContainer = try container.nestedContainer(keyedBy: ImageCodingKeys.self, forKey: key) | |
url = try nestedContainer.decode(String.self, forKey: .url) | |
width = try nestedContainer.decode(Int.self, forKey: .width) | |
height = try nestedContainer.decode(Int.self, forKey: .height) | |
} | |
} | |
extension Image: CustomStringConvertible { | |
var description: String { | |
""" | |
\nImage: | |
at: \(url) | |
width: \(width) | |
height: \(height) | |
""" | |
} | |
} | |
enum TypeCodingKeys: String, CodingKey { | |
case type | |
} | |
struct ParseEngine: Decodable { | |
static var registeredComponentTypes: [String: Component.Type] = [:] | |
var parsedObjects: [Component] = [] | |
init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: DynamicCodingKeys.self) | |
for key in container.allKeys { | |
let nestedContainer = try container.nestedContainer(keyedBy: TypeCodingKeys.self, forKey: key) | |
if let typeString = try? nestedContainer.decode(String.self, forKey: .type) { | |
if let theType = Self.registeredComponentTypes[typeString] { | |
do { | |
if let foo = try theType.init(from: container, key: key) { | |
parsedObjects.append(foo) | |
} | |
} catch { | |
print("\nERROR parsing key: \"\(key.stringValue)\" as \"\(typeString)\" type. Skipping and trying next key.\n") | |
} | |
} | |
} | |
} | |
} | |
} | |
class TestDecodingFunTests: XCTestCase { | |
override func setUpWithError() throws { | |
// Put setup code here. This method is called before the invocation of each test method in the class. | |
} | |
override func tearDownWithError() throws { | |
// Put teardown code here. This method is called after the invocation of each test method in the class. | |
} | |
func testExample() throws { | |
let testJSON = """ | |
{ | |
"key1" : { "type" : "text", "value" : "Text Value 1", "size": 12 }, | |
"key2" : { "type" : "image", "url" : "http://disney.com/images/image1.png", "width": 300, "height": 300 }, | |
"key3" : { "type" : "text", "value" : "Text Value 2", "size" : 14 } | |
} | |
""".data(using: .utf8)! | |
ParseEngine.registeredComponentTypes[Image.type] = Image.self | |
ParseEngine.registeredComponentTypes[Text.type] = Text.self | |
let decoder = JSONDecoder() | |
let obj = try decoder.decode(ParseEngine.self, from: testJSON) | |
print(obj) | |
XCTAssertEqual(obj.parsedObjects.count, 3) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Text != nil }).count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Image != nil }).count, 1) | |
} | |
func testExampleErrorReporting() throws { | |
let testJSON = """ | |
{ | |
"key1" : { "type" : "text", "value" : "Text Value 1", "size": 12 }, | |
"key2" : { "type" : "image", "BAD_BAD_BAD_url" : "http://disney.com/images/image1.png", "width": 300, "height": 300 }, | |
"key3" : { "type" : "text", "value" : "Text Value 2", "size" : 14 } | |
} | |
""".data(using: .utf8)! | |
ParseEngine.registeredComponentTypes[Image.type] = Image.self | |
ParseEngine.registeredComponentTypes[Text.type] = Text.self | |
let decoder = JSONDecoder() | |
let obj = try decoder.decode(ParseEngine.self, from: testJSON) | |
print(obj) | |
XCTAssertEqual(obj.parsedObjects.count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Text != nil }).count, 2) | |
XCTAssertEqual(obj.parsedObjects.filter( { $0 as? Image != nil }).count, 0) | |
} | |
} |
Revision 4 changes the try?
to try
on line 118 so that errors are thrown if it hits JSON that is malformed.
Revision 5 changes line 118 area to add a try/catch block and report an error but then continue one with the other top-level keys in the json and adds a new test to verify that the other keys are successfully decoded. Output of the second test is:
ERROR parsing key: "key2" as "image" type. Skipping and trying next key.
ParseEngine(parsedObjects: [
Text:
string: Text Value 1
font size: 12,
Text:
string: Text Value 2
font size: 14])
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
switched to spaces because GitHub can't seem to pay attention to "4 space wide tabs" and just always makes them 8 (gross).