Skip to content

Commit

Permalink
Attributed Intrinsic (#73)
Browse files Browse the repository at this point in the history
## Overview

This PR fixes #12: decoding of unkeyed single value elements that contain attributes as reported in that issue.

## Example

```xml
<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>
``` 

```swift
private struct Foo: Codable, DynamicNodeEncoding {
    let id: String
    let value: String

    enum CodingKeys: String, CodingKey {
        case id
        case value
        // case value = "" would also work
    }

    static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
        switch key {
        case CodingKeys.id:
            return .attribute
        default:
            return .element
        }
    }
}
```

Previously this XML example would fail to decode. This PR allows two different methods of decoding discussed in usage.

## Usage

This PR will support decoding the example XML in two cases as long as the prerequisite cases are matched.

### Prerequisites

1. No keyed child elements exist
2. Any keyed child nodes are supported Attribute types and are indicated to decode as such

### Supported cases

1. An instance var with the key `value` of any decodable type.
2. An instance var of any key that has a Decoding key of String value "value" or "".

The decoder will look for the case where an element was keyed with either "value" or "", but not both, and only one of those values (ie; no other keyed elements). It will automatically find the correct value based on the CodingKey supplied.

## Other considerations

The choice to decode as either "value" or "" keys was purely to try to support the inverse to XML version which would only work if an instance var specifically has a `CodingKey` with associated value type `String` that returns an empty string, if PR #70 is commited as-is, which adds XML coding support for unkeyed attributed value elements.

The 'value' variant was added as a simpler means to support decoding a nested unkeyed element without having to provide custom CodingKey enum for a struct. Something needed to be provided since Swift doesn't have empty string iVars `let "" : String`, isn't a valid iVar token for example, so `value` was chosen as a logical default.

## Notes

This PR is an extension of #70 , though it could be recoded to work off of `master`. The last commit in this PR is the only commit specific to this feature, though #70 provides the inverse solution of creating XML from an attributed value wrapping struct.

Coding and decoding unit tests of String and Int values are included.
  • Loading branch information
JoeMatt authored and MaxDesiatov committed Feb 22, 2019
1 parent eafcdf1 commit 011f493
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 10 deletions.
11 changes: 11 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ extension KeyedBox: Box {
}
}

extension KeyedBox {
var value: SimpleBox? {
guard
elements.count == 1,
let value = elements["value"] as? SimpleBox
?? elements[""] as? SimpleBox,
!value.isNull else { return nil }
return value
}
}

extension KeyedBox: CustomStringConvertible {
var description: String {
return "{attributes: \(attributes), elements: \(elements)}"
Expand Down
7 changes: 6 additions & 1 deletion Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable {
func flatten() -> KeyedBox {
let attributes = self.attributes.mapValues { StringBox($0) }

let keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in
var keyedElements = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in
var result = result
let key = element.key

Expand Down Expand Up @@ -93,6 +93,11 @@ struct XMLCoderElement: Equatable {
return result
}

// Handle attributed unkeyed value <foo attr="bar">zap</foo>
// Value should be zap. Detect only when no other elements exist
if keyedElements.isEmpty, let value = value {
keyedElements["value"] = StringBox(value)
}
let keyedBox = KeyedBox(elements: keyedElements, attributes: attributes)

return keyedBox
Expand Down
23 changes: 15 additions & 8 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,18 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
public func decode<T: Decodable>(
_ type: T.Type, forKey key: Key
) throws -> T {
let attributeNotFound = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue] == nil
let attributeFound = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue] != nil
}
let elementNotFound = container.withShared { keyedBox in
keyedBox.elements[key.stringValue] == nil

let elementFound = container.withShared { keyedBox in
keyedBox.elements[key.stringValue] != nil || keyedBox.value != nil
}

if let type = type as? AnyEmptySequence.Type, attributeNotFound,
elementNotFound, let result = type.init() as? T {
if let type = type as? AnyEmptySequence.Type,
!attributeFound,
!elementFound,
let result = type.init() as? T {
return result
}

Expand Down Expand Up @@ -163,8 +166,12 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
_ type: T.Type,
forKey key: Key
) throws -> T {
let elementOrNil = container.withShared { keyedBox in
keyedBox.elements[key.stringValue]
let elementOrNil = container.withShared { keyedBox -> KeyedBox.Element? in
if ["value", ""].contains(key.stringValue) {
return keyedBox.elements[key.stringValue] ?? keyedBox.value
} else {
return keyedBox.elements[key.stringValue]
}
}

let attributeOrNil = container.withShared { keyedBox in
Expand Down
206 changes: 206 additions & 0 deletions Tests/XMLCoderTests/AttributedIntrinsicTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//
// AttributedIntrinsicTest.swift
// XMLCoderTests
//
// Created by Joseph Mattiello on 1/23/19.
//

import Foundation
import XCTest
@testable import XMLCoder

let fooXML = """
<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>
""".data(using: .utf8)!

private struct Foo: Codable, DynamicNodeEncoding {
let id: String
let value: String

enum CodingKeys: String, CodingKey {
case id
case value
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}

private struct FooEmptyKeyed: Codable, DynamicNodeEncoding {
let id: String
let unkeyedValue: Int

enum CodingKeys: String, CodingKey {
case id
case unkeyedValue = ""
}

static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}

final class AttributedIntrinsicTest: XCTestCase {
func testEncode() throws {
let encoder = XMLEncoder()
encoder.outputFormatting = []

let foo1 = FooEmptyKeyed(id: "123", unkeyedValue: 456)

let header = XMLHeader(version: 1.0, encoding: "UTF-8")
let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header)
let xmlString = String(data: encoded, encoding: .utf8)
XCTAssertNotNil(xmlString)

// Test string equivalency
let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines)
let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(encodedXML, originalXML)
}

func testDecode() throws {
let decoder = XMLDecoder()
decoder.errorContextLength = 10

let foo1 = try decoder.decode(Foo.self, from: fooXML)
XCTAssertEqual(foo1.id, "123")
XCTAssertEqual(foo1.value, "456")

let foo2 = try decoder.decode(FooEmptyKeyed.self, from: fooXML)
XCTAssertEqual(foo2.id, "123")
XCTAssertEqual(foo2.unkeyedValue, 456)
}

static var allTests = [
("testEncode", testEncode),
("testDecode", testDecode),
]
}

// MARK: - Enums

let attributedEnumXML = """
<?xml version="1.0" encoding="UTF-8"?>
<foo><number type="string">ABC</number><number type="int">123</number></foo>
""".data(using: .utf8)!

private struct Foo2: Codable {
let number: [FooNumber]
}

private struct FooNumber: Codable, DynamicNodeEncoding {
public let type: FooEnum

public init(type: FooEnum) {
self.type = type
}

enum CodingKeys: String, CodingKey {
case type
case typeValue = ""
}

public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case FooNumber.CodingKeys.type: return .attribute
default: return .element
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

type = try container.decode(FooEnum.self, forKey: .type)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch type {
case let .string(value):
try container.encode("string", forKey: .type)
try container.encode(value, forKey: .typeValue)
case let .int(value):
try container.encode("int", forKey: .type)
try container.encode(value, forKey: .typeValue)
}
}
}

private enum FooEnum: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case string
case int
}

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let value = try values.decodeIfPresent(String.self, forKey: .string) {
self = .string(value)
return
} else if let value = try values.decodeIfPresent(Int.self, forKey: .int) {
self = .int(value)
return
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "No coded value for string or int"))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .string(value):
try container.encode(value, forKey: .string)
case let .int(value):
try container.encode(value, forKey: .int)
}
}

case string(String)
case int(Int)
}

final class AttributedEnumIntrinsicTest: XCTestCase {
func testEncode() throws {
let encoder = XMLEncoder()
encoder.outputFormatting = []

let foo1 = Foo2(number: [FooNumber(type: FooEnum.string("ABC")), FooNumber(type: FooEnum.int(123))])

let header = XMLHeader(version: 1.0, encoding: "UTF-8")
let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header)
let xmlString = String(data: encoded, encoding: .utf8)
XCTAssertNotNil(xmlString)
// Test string equivalency
let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines)
let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(encodedXML, originalXML)
}

// TODO: Fix decoding
// func testDecode() throws {
// let decoder = XMLDecoder()
// decoder.errorContextLength = 10
//
// let foo = try decoder.decode(Foo2.self, from: attributedEnumXML)
// XCTAssertEqual(foo.number[0].type, FooEnum.string("ABC"))
// XCTAssertEqual(foo.number[1].type, FooEnum.int(123))
// }

static var allTests = [
("testEncode", testEncode),
// ("testDecode", testDecode),
]
}
2 changes: 1 addition & 1 deletion Tests/XMLCoderTests/BooksTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ final class BooksTest: XCTestCase {

XCTAssertEqual(book1, book2)

// Test string equivlancy
// Test string equivalency
let encodedXML = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
let originalXML = String(data: bookXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(encodedXML, originalXML)
Expand Down
4 changes: 4 additions & 0 deletions XMLCoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; };
B34B3C08220381AC00BCBA30 /* String+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */; };
B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; };
B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */; };
B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; };
B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */; };
B3BE1D652202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */; };
Expand Down Expand Up @@ -135,6 +136,7 @@
A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = "<group>"; };
B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTests.swift"; sourceTree = "<group>"; };
B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = "<group>"; };
B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedIntrinsicTest.swift; sourceTree = "<group>"; };
B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = "<group>"; };
B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLEncoderImplementation.swift; sourceTree = "<group>"; };
B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XMLEncoderImplementation+SingleValueEncodingContainer.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -378,6 +380,7 @@
OBJ_38 /* RelationshipsTest.swift */,
BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */,
D1FC040421C7EF8200065B43 /* RJISample.swift */,
B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */,
B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */,
A61DCCD621DF8DB300C0A19D /* ClassTests.swift */,
D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */,
Expand Down Expand Up @@ -618,6 +621,7 @@
A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */,
BF63EF6B21D10284001D38C5 /* XMLElementTests.swift in Sources */,
BF9457ED21CBB6BC005ACFDE /* BoolTests.swift in Sources */,
B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */,
D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */,
BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */,
BF9457CE21CBB516005ACFDE /* StringBoxTests.swift in Sources */,
Expand Down

0 comments on commit 011f493

Please sign in to comment.