Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
@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.
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])
//
// 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)
}
}
//
// 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)
}
}
@GeekAndDadDad

This comment has been minimized.

Copy link
Owner Author

@GeekAndDadDad GeekAndDadDad commented Sep 22, 2020

switched to spaces because GitHub can't seem to pay attention to "4 space wide tabs" and just always makes them 8 (gross).

@GeekAndDadDad

This comment has been minimized.

Copy link
Owner Author

@GeekAndDadDad GeekAndDadDad commented Sep 22, 2020

Revision 4 changes the try? to try on line 118 so that errors are thrown if it hits JSON that is malformed.

@GeekAndDadDad

This comment has been minimized.

Copy link
Owner Author

@GeekAndDadDad GeekAndDadDad commented Sep 22, 2020

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
You can’t perform that action at this time.