Skip to content

Commit

Permalink
Encoding fixes. (#25)
Browse files Browse the repository at this point in the history
Thanks so much @adam-fowler
  • Loading branch information
fabianfett authored Mar 20, 2020
1 parent 9071a99 commit 92f2f45
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 27 deletions.
102 changes: 80 additions & 22 deletions Sources/PureSwiftJSONCoding/Encoding/JSONEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,102 @@
import PureSwiftJSONParsing

enum JSONFuture {
case value(JSONValue)
case nestedArray(JSONArray)
case nestedObject(JSONObject)
}

class JSONArray {

private(set) var array: [JSONValue] = []
private(set) var array: [JSONFuture] = []

init() {
array.reserveCapacity(10)
}

@inline(__always) func append(_ element: JSONValue) {
self.array.append(element)
self.array.append(.value(element))
}

@inline(__always) func appendArray() -> JSONArray {
let array = JSONArray()
self.array.append(.nestedArray(array))
return array
}

@inline(__always) func appendObject() -> JSONObject {
let object = JSONObject()
self.array.append(.nestedObject(object))
return object
}

var values: [JSONValue] {
self.array.map { (future) -> JSONValue in
switch future {
case .value(let value):
return value
case .nestedArray(let array):
return .array(array.values)
case .nestedObject(let object):
return .object(object.values)
}
}
}
}

class JSONObject {

private(set) var dict: [String: JSONValue] = [:]
private(set) var dict: [String: JSONFuture] = [:]

init() {
dict.reserveCapacity(20)
}

@inline(__always) func set(_ value: JSONValue, for key: String) {
self.dict[key] = value
self.dict[key] = .value(value)
}

@inline(__always) func setArray(for key: String) -> JSONArray {
if case .nestedArray(let array) = self.dict[key] {
return array
}

if case .nestedObject(_) = self.dict[key] {
preconditionFailure("For key \"\(key)\" a keyed container has already been created.")
}

let array = JSONArray()
self.dict[key] = .nestedArray(array)
return array
}

@inline(__always) func setObject(for key: String) -> JSONObject {
if case .nestedObject(let object) = self.dict[key] {
return object
}

if case .nestedArray(_) = self.dict[key] {
preconditionFailure("For key \"\(key)\" an unkeyed container has already been created.")
}

let object = JSONObject()
self.dict[key] = .nestedObject(object)
return object
}

var values: [String: JSONValue] {
self.dict.mapValues { (future) -> JSONValue in
switch future {
case .value(let value):
return value
case .nestedArray(let array):
return .array(array.values)
case .nestedObject(let object):
return .object(object.values)
}
}
}

}

public class JSONEncoder {
Expand Down Expand Up @@ -62,17 +135,17 @@ class JSONEncoderImpl {

var value: JSONValue? {
if let object = self.object {
return .object(object.dict)
return .object(object.values)
}
if let array = self.array {
return .array(array.array)
return .array(array.values)
}
return self.singleValue
}

init(userInfo: [CodingUserInfoKey : Any], codingPath : [CodingKey]) {
self.userInfo = userInfo
self.codingPath = []
self.codingPath = codingPath
}

}
Expand Down Expand Up @@ -115,18 +188,3 @@ extension JSONEncoderImpl: Encoder {
return JSONSingleValueEncodingContainer(impl: self, codingPath: codingPath)
}
}

extension JSONEncoderImpl {

func nestedContainer<NestedKey, Key>(keyedBy keyType: NestedKey.Type, forKey key: Key)
-> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey, Key : CodingKey
{
preconditionFailure("unimplemented")
}

func nestedUnkeyedContainer<Key>(forKey key: Key)
-> UnkeyedEncodingContainer where Key : CodingKey
{
preconditionFailure("unimplemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ struct JSONKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol
self.object = impl.object!
self.codingPath = codingPath
}

// used for nested containers
init(impl: JSONEncoderImpl, object: JSONObject, codingPath: [CodingKey]) {
self.impl = impl
self.object = object
self.codingPath = codingPath
}

mutating func encodeNil(forKey key: Self.Key) throws {

Expand Down Expand Up @@ -89,11 +96,17 @@ struct JSONKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Self.Key) ->
KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey
{
return impl.nestedContainer(keyedBy: keyType, forKey: key)
let newPath = impl.codingPath + [key]
let object = self.object.setObject(for: key.stringValue)
let nestedContainer = JSONKeyedEncodingContainer<NestedKey>(impl: self.impl, object: object, codingPath: newPath)
return KeyedEncodingContainer(nestedContainer)
}

mutating func nestedUnkeyedContainer(forKey key: Self.Key) -> UnkeyedEncodingContainer {
return impl.nestedUnkeyedContainer(forKey: key)
let newPath = impl.codingPath + [key]
let array = self.object.setArray(for: key.stringValue)
let nestedContainer = JSONUnkeyedEncodingContainer(impl: self.impl, array: array, codingPath: newPath)
return nestedContainer
}

mutating func superEncoder() -> Encoder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ struct JSONUnkeyedEncodingContainer: UnkeyedEncodingContainer {
self.impl = impl
self.array = impl.array!
self.codingPath = codingPath

}

// used for nested containers
init(impl: JSONEncoderImpl, array: JSONArray, codingPath: [CodingKey]) {
self.impl = impl
self.array = array
self.codingPath = codingPath
}

mutating func encodeNil() throws {
Expand Down Expand Up @@ -90,11 +96,17 @@ struct JSONUnkeyedEncodingContainer: UnkeyedEncodingContainer {
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) ->
KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey
{
preconditionFailure()
let newPath = impl.codingPath + [ArrayKey(index: count)]
let object = self.array.appendObject()
let nestedContainer = JSONKeyedEncodingContainer<NestedKey>(impl: self.impl, object: object, codingPath: newPath)
return KeyedEncodingContainer(nestedContainer)
}

mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
preconditionFailure()
let newPath = impl.codingPath + [ArrayKey(index: count)]
let array = self.array.appendArray()
let nestedContainer = JSONUnkeyedEncodingContainer(impl: self.impl, array: array, codingPath: newPath)
return nestedContainer
}

mutating func superEncoder() -> Encoder {
Expand Down
32 changes: 32 additions & 0 deletions Tests/JSONCodingTests/Encoding/JSONEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,37 @@ class JSONEncoderTests: XCTestCase {
}
}

func testLastCodingPath() {
struct SubObject: Encodable {
let value: Int

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let key = encoder.codingPath.last {
try container.encode(key.stringValue, forKey: .key)
try container.encode(value, forKey: .value)
}
}

private enum CodingKeys: String, CodingKey {
case key = "key"
case value = "value"
}
}

struct Object: Encodable {
let sub: SubObject
}

do {
let object = Object(sub: SubObject(value: 12))
let json = try PureSwiftJSONCoding.JSONEncoder().encode(object)
let parsed = try JSONParser().parse(bytes: json)
XCTAssertEqual(parsed, .object(["sub": .object(["key": .string("sub"), "value": .number("12")])]))
}
catch {
XCTFail("Unexpected error: \(error)")
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import XCTest
@testable import PureSwiftJSONCoding
@testable import PureSwiftJSONParsing

class JSONKeyedEncodingContainerTests: XCTestCase {

func testNestedKeyedContainer() {
struct Object: Encodable {
let firstName: String
let surname: String

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var nameContainer = container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
try nameContainer.encode(firstName, forKey: .firstName)

var sameContainer = container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
try sameContainer.encode(surname, forKey: .surname)
}

private enum CodingKeys: String, CodingKey {
case name = "name"
}

private enum NameCodingKeys: String, CodingKey {
case firstName = "firstName"
case surname = "surname"
}
}

do {
let object = Object(firstName: "Adam", surname: "Fowler")
let json = try PureSwiftJSONCoding.JSONEncoder().encode(object)

let parsed = try JSONParser().parse(bytes: json)
XCTAssertEqual(parsed, .object(["name": .object(["firstName": .string("Adam"), "surname": .string("Fowler")])]))
}
catch {
XCTFail("Unexpected error: \(error)")
}
}

func testNestedUnkeyedContainer() {
struct NumberStruct: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var numbersContainer = container.nestedUnkeyedContainer(forKey: .numbers)
var sameContainer = container.nestedUnkeyedContainer(forKey: .numbers)

try numbersContainer.encode(1)
try sameContainer.encode(2)
try numbersContainer.encode(3)
try sameContainer.encode(4)
}

private enum CodingKeys: String, CodingKey {
case numbers
}
}

do {
let object = NumberStruct()
let json = try PureSwiftJSONCoding.JSONEncoder().encode(object)

let parsed = try JSONParser().parse(bytes: json)
XCTAssertEqual(parsed, .object(["numbers": .array([.number("1"), .number("2"), .number("3"), .number("4")])]))
}
catch {
XCTFail("Unexpected error: \(error)")
}
}

}



Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import XCTest
@testable import PureSwiftJSONCoding
@testable import PureSwiftJSONParsing

class JSONUnkeyedEncodingContainerTests: XCTestCase {

func testNestedKeyedContainer() {
struct ObjectInArray: Encodable {
let firstName: String
let surname: String

func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
var nestedContainer = container.nestedContainer(keyedBy: NameCodingKeys.self)
try nestedContainer.encode(firstName, forKey: .firstName)
try nestedContainer.encode(surname, forKey: .surname)
}

private enum NameCodingKeys: String, CodingKey {
case firstName = "firstName"
case surname = "surname"
}
}

do {
let object = ObjectInArray(firstName: "Adam", surname: "Fowler")
let json = try PureSwiftJSONCoding.JSONEncoder().encode(object)

let parsed = try JSONParser().parse(bytes: json)
XCTAssertEqual(parsed, .array([.object(["firstName": .string("Adam"), "surname": .string("Fowler")])]))
}
catch {
XCTFail("Unexpected error: \(error)")
}
}

func testNestedUnkeyedContainer() {
struct NumbersInArray: Encodable {
let numbers: [Int]

func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
var numbersContainer = container.nestedUnkeyedContainer()
try numbers.forEach() { try numbersContainer.encode($0) }
}

private enum CodingKeys: String, CodingKey {
case numbers
}
}

do {
let object = NumbersInArray(numbers: [1, 2, 3, 4])
let json = try PureSwiftJSONCoding.JSONEncoder().encode(object)

let parsed = try JSONParser().parse(bytes: json)
XCTAssertEqual(parsed, .array([.array([.number("1"), .number("2"), .number("3"), .number("4")])]))
}
catch {
XCTFail("Unexpected error: \(error)")
}
}
}



0 comments on commit 92f2f45

Please sign in to comment.