diff --git a/CHANGELOG.md b/CHANGELOG.md index a4da0922..da47bb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,36 @@ -# 0.7.0 (July 2, 2019) +# 0.8.0 (August 4, 2019) + +This release adds support for decoding and encoding ordered sequences of +different elements as enums with associated values. In addition, XMLCoder now +supports Linux. Many thanks to [@jsbean](https://github.com/jsbean), +[@bwetherfield](https://github.com/bwetherfield) and +[@drewag](https://github.com/drewag) for implementing this! + +**Breaking changes:** + +- Fixed typo in `XMLDecoder` property: `errorContextLenght` has been renamed to `errorContextLength` in [\#114](https://github.com/MaxDesiatov/XMLCoder/pull/114). + +**Closed issues:** + +- XML with autoclosed tags [\#116](https://github.com/MaxDesiatov/XMLCoder/issues/116) +- Arrays of enums [\#91](https://github.com/MaxDesiatov/XMLCoder/issues/91) +- Array of enums with associated values [\#25](https://github.com/MaxDesiatov/XMLCoder/issues/25) + +**Merged pull requests:** + +- Decoding choice elements that can hold empty structs + [\#120](https://github.com/MaxDesiatov/XMLCoder/pull/120) + ([@bwetherfield](https://github.com/bwetherfield)) +- `Encodable` and `Decodable` support for choice elements + [\#119](https://github.com/MaxDesiatov/XMLCoder/pull/119) + ([@jsbean](https://github.com/jsbean)) +- Add Linux support [\#117](https://github.com/MaxDesiatov/XMLCoder/pull/117) + ([@drewag](https://github.com/drewag)) +- Fix typo: `errorContextLenght` -\> `errorContextLength` + [\#114](https://github.com/MaxDesiatov/XMLCoder/pull/114) + ([@jsbean](https://github.com/jsbean)) + +# 0.7.0 (July 2, 2019) This release changes the behavior of attributes coding: now order of XML attributes is fully preserved. One of the benefits is that it improves unit @@ -9,19 +41,19 @@ Travis for CI with great improvements to overall CI stability, speed, and parallel builds. Thanks to [Andrés Cecilia Luque](https://github.com/acecilia) and [Jay Hickey](https://github.com/jayhickey) for the contributions! -## Merged pull requests +**Merged pull requests:** - Change components variable from var to let [\#107](https://github.com/MaxDesiatov/XMLCoder/pull/107) - ([jayhickey](https://github.com/jayhickey)) + ([@jayhickey](https://github.com/jayhickey)) - Keep the order of the attributes during encoding operations [\#110](https://github.com/MaxDesiatov/XMLCoder/pull/110) - ([acecilia](https://github.com/acecilia)) + ([@acecilia](https://github.com/acecilia)) - Migrate from Travis to Azure Pipelines [\#111](https://github.com/MaxDesiatov/XMLCoder/pull/111) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.6.0 (June 17, 2019) +# 0.6.0 (June 17, 2019) An improvement release that introduces `convertFromKebabCase` and `convertToKebabCase` key decoding strategies. There were a few changes that @@ -30,39 +62,38 @@ has changed and a few more tests added. Thanks to [Andrés Cecilia Luque](https://github.com/acecilia) and [Vincent Esche](https://github.com/regexident) for the contributions! -## Merged pull requests +**Merged pull requests:** - Add support for kebab-case KeyDecodingStrategy [\#105](https://github.com/MaxDesiatov/XMLCoder/pull/105) - ([acecilia](https://github.com/acecilia)) + ([@acecilia](https://github.com/acecilia)) - Replace UnkeyedBox with Array, refine KeyedStorage [\#102](https://github.com/MaxDesiatov/XMLCoder/pull/102) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) - Add tests for nested keyed/unkeyed collections [\#38](https://github.com/MaxDesiatov/XMLCoder/pull/38) - ([regexident](https://github.com/regexident)) - + ([@regexident](https://github.com/regexident)) -# 0.5.1 (May 2, 2019) +# 0.5.1 (May 2, 2019) Bugfix release that restores decoding of empty sequences, which became broken in 0.5.0. -## Merged pull requests +**Merged pull requests:** - Fix decoding of empty sequences [\#98](https://github.com/MaxDesiatov/XMLCoder/pull/98) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) - Rename `flatten` to `transformToBoxTree`, rename tests [\#97](https://github.com/MaxDesiatov/XMLCoder/pull/97) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.5.0 (May 2, 2019) +# 0.5.0 (May 2, 2019) A small improvement release tagged early to resolve blocking issues in [CoreXLSX](https://github.com/MaxDesiatov/CoreXLSX). -## Notable changes +**Notable changes:** * Empty value strings are no longer decoded as `nil` when a `String` is expected, but are decoded as empty strings, which represents the actual value. @@ -70,36 +101,36 @@ A small improvement release tagged early to resolve blocking issues in overriding the default behaviour, where starting and trailing whitespaces are trimmed from string values. -## Closed issues +**Closed issues:** - Trimmed whitespace on decoding `String` [\#94](https://github.com/MaxDesiatov/XMLCoder/issues/94) -## Merged pull requests +**Merged pull requests:** - Fixed a bug when decoding a key with one character only [\#96](https://github.com/MaxDesiatov/XMLCoder/pull/96) - ([TheFlow95](https://github.com/TheFlow95)) + ([@TheFlow95](https://github.com/TheFlow95)) - Add more cases to `AttributedIntrinsicTest` [\#95](https://github.com/MaxDesiatov/XMLCoder/pull/95) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) - Use `map` instead of `mapValues`/`shuffle` in `XMLCoderElement.flatten` [\#93](https://github.com/MaxDesiatov/XMLCoder/pull/93) - ([jsbean](https://github.com/jsbean)) + ([@jsbean](https://github.com/jsbean)) - Fix decoding empty element as optional [\#92](https://github.com/MaxDesiatov/XMLCoder/pull/92) - ([MaxDesiatov](https://github.com/MaxDesiatov)) + ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.4.1 (April 12, 2019) +# 0.4.1 (April 12, 2019) A bugfix release removing unused Xcode project scheme to improve build time for Carthage users. -## Notable changes +**Notable changes:** -* Remove unused scheme in Xcode project, [@MaxDesiatov](https://github.com/MaxDesiatov)) +* Remove unused scheme in Xcode project ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.4.0 (April 8, 2019) +# 0.4.0 (April 8, 2019) This is a release with plenty of new features that allow you to parse many more XML variations than previously. Compatibility with Xcode 10.2 and Swift 5.0 is @@ -111,7 +142,7 @@ also improved. A huge thank you to [@JoeMatt](https://github.com/JoeMatt) and [@khoogheem](https://github.com/khoogheem) and [@thecb4](https://github.com/thecb4) for reporting issues during development! -## Notable changes +**Notable changes:** * Ordered encoding: this was one of the most requested changes and it's finally here! 🎉 Now both keyed and unkeyed elements are encoded in the exactly same @@ -139,7 +170,7 @@ also improved. A huge thank you to [@JoeMatt](https://github.com/JoeMatt) and (`"value"` or empty string `""` if you already have an XML attribute named `"value"`). -## Closed issues +**Closed issues:** - Crash: Range invalid bounds in XMLStackParser.swift [\#83](https://github.com/MaxDesiatov/XMLCoder/issues/83) - Document DynamicNodeEncoding and attributed intrinsic [\#80](https://github.com/MaxDesiatov/XMLCoder/issues/80) @@ -148,7 +179,7 @@ also improved. A huge thank you to [@JoeMatt](https://github.com/JoeMatt) and - XmlEncoder: ordering of elements [\#17](https://github.com/MaxDesiatov/XMLCoder/issues/17) - Can’t reach an XML value [\#12](https://github.com/MaxDesiatov/XMLCoder/issues/12) -## Merged pull requests +**Merged pull requests:** - Make value intrinsic smarter [\#89](https://github.com/MaxDesiatov/XMLCoder/pull/89) @@ -181,7 +212,7 @@ also improved. A huge thank you to [@JoeMatt](https://github.com/JoeMatt) and ([@MaxDesiatov](https://github.com/MaxDesiatov)) - Attributed Intrinsic \(value coding key\) [\#73](https://github.com/MaxDesiatov/XMLCoder/pull/73) - ([JoeMatt](https://github.com/JoeMatt)) + ([@JoeMatt](https://github.com/JoeMatt)) - Dynamic node encoding + new formatters + various fixes [\#70](https://github.com/MaxDesiatov/XMLCoder/pull/70) ([@JoeMatt](https://github.com/JoeMatt)) @@ -189,17 +220,17 @@ also improved. A huge thank you to [@JoeMatt](https://github.com/JoeMatt) and [\#45](https://github.com/MaxDesiatov/XMLCoder/pull/45) ([@regexident](https://github.com/regexident)) -# 0.3.1 (February 6, 2019) +# 0.3.1 (February 6, 2019) A bugfix release that adds missing `CFBundleVersion` in generated framework's `Info.plist` ([#72](https://github.com/MaxDesiatov/XMLCoder/issues/72) reported by [@stonedauwg](https://github.com/stonedauwg)). -## Changes +**Changes:** * Set `CURRENT_PROJECT_VERSION` in project file ([#74](https://github.com/MaxDesiatov/XMLCoder/pull/74), [@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.3.0 (January 22, 2019) +# 0.3.0 (January 22, 2019) A maintenance release focused on fixing bugs, improving error reporting and overall internal architecture of the library. For this release we've started @@ -208,7 +239,7 @@ Thanks to [@hodovani](https://github.com/hodovani) and [@regexident](https://github.com/regexident) for their work on improving test coverage in this release. -## Additions +**Additions:** You can now set `errorContextLength: UInt` property on `XMLDecoder` instance, which will make it add a snippet of XML of at most this length from parser state @@ -216,7 +247,7 @@ when a parsing error occurs. This change was provided by [@hodovani](https://github.com/hodovani) and can greatly help with attempts to parse invalid XML, where previously only a line and column number were reported. -## Deprecations +**Deprecations:** `NodeEncodingStrategies` was renamed to `NodeEncodingStrategy` for consistency. `NodeEncodingStrategies` is still available as a deprecated typealias, which @@ -224,7 +255,7 @@ will be removed in future versions. Thanks to [@regexident](https://github.com/regexident) for cleaning this up and providing many more changes in this release that make `XMLCoder` better and easier to use. -## Changes +**Changes:** * Add SwiftLint and fix linter errors ([#35](https://github.com/MaxDesiatov/XMLCoder/pull/35), @@ -365,19 +396,19 @@ many more changes in this release that make `XMLCoder` better and easier to use. * Run tests with coverage, upload to codecov.io ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.2.1 (November 18, 2018) +# 0.2.1 (November 18, 2018) * watchOS deployment target set to 2.0 for Carthage ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.2.0 (November 18, 2018) +# 0.2.0 (November 18, 2018) * Add watchOS 2.0 deployment target ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.1.1 (November 18, 2018) +# 0.1.1 (November 18, 2018) * Set iOS deployment target to 9.0 ([@MaxDesiatov](https://github.com/MaxDesiatov)) -# 0.1.0 (November 8, 2018) +# 0.1.0 (November 8, 2018) * Add support for decoupled, type-dependent node-encoding strategies ([@regexident](https://github.com/regexident)) diff --git a/README.md b/README.md index 628d700e..8f2f24bc 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ let returnData = try? XMLEncoder().encode(note, withRootKey: "note") ## Advanced features -These features are available in [0.4.0 -release](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.4.0) or later: +The following features are available in [0.4.0 +release](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.4.0) or later +(unless stated otherwise): ### Stripping namespace prefix @@ -160,6 +161,9 @@ works for this XML: ``` +Please refer to PR [\#70](https://github.com/MaxDesiatov/XMLCoder/pull/70) by +[@JoeMatt](https://github.com/JoeMatt) for more details. + ### Coding key value intrinsic Suppose that you need to decode an XML that looks similar to this: @@ -199,6 +203,9 @@ struct Foo: Codable, DynamicNodeEncoding { } ``` +Thanks to [@JoeMatt](https://github.com/JoeMatt) for implementing this in +in PR [\#73](https://github.com/MaxDesiatov/XMLCoder/pull/73). + ### Preserving whitespaces in element content By default whitespaces are trimmed in element content during decoding. This @@ -209,11 +216,58 @@ you can now set a property `trimValueWhitespaces` to `false` (the default value ### Choice element coding -Starting with [version 0.8](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.8.0), you -now encode and decode union-type–like enums with associated values by conforming your -`CodingKey` type additionally to `XMLChoiceCodingKey`. +Starting with [version 0.8](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.8.0), +you can encode and decode `enum`s with associated values by conforming your +`CodingKey` type additionally to `XMLChoiceCodingKey`. This allows decoding +XML elements similar in structure to this example: + +```xml + + 1 + two + three + 4 + 5 + +``` + +To decode these elements you can use this type: + +```swift +enum IntOrString: Equatable { + case int(Int) + case string(String) +} + +extension IntOrString: Codable { + enum CodingKeys: String, XMLChoiceCodingKey { + case int + case string + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .int(value): + try container.encode(value, forKey: .int) + case let .string(value): + try container.encode(value, forKey: .string) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + self = .int(try container.decode(Int.self, forKey: .int)) + } catch { + self = .string(try container.decode(String.self, forKey: .string)) + } + } +} +``` -For more information, see the [pull request](https://github.com/MaxDesiatov/XMLCoder/pull/119). +This is described in more details in PR [\#119](https://github.com/MaxDesiatov/XMLCoder/pull/119) +by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield). ## Installation @@ -225,7 +279,7 @@ For more information, see the [pull request](https://github.com/MaxDesiatov/XMLC - iOS 9.0 / watchOS 2.0 / tvOS 9.0 / macOS 10.10 or later deployment targets **Linux** -- Ubuntu 14.04 or Later +- Ubuntu 14.04 or later - Swift 5.0.1 or later ### Swift Package Manager @@ -240,7 +294,7 @@ easy as adding it to the `dependencies` value of your `Package.swift`. ```swift dependencies: [ - .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.7.0") + .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.8.0") ] ``` @@ -269,7 +323,7 @@ target 'YourApp' do use_frameworks! # Pods for Test - pod 'XMLCoder', '~> 0.7.0' + pod 'XMLCoder', '~> 0.8.0' end ``` @@ -297,7 +351,7 @@ $ brew install carthage Inside of your `Cartfile`, add GitHub path to `XMLCoder`: ```ogdl -github "MaxDesiatov/XMLCoder" ~> 0.7.0 +github "MaxDesiatov/XMLCoder" ~> 0.8.0 ``` Then, run the following command to build the framework: diff --git a/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift b/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift index 49a82d0d..740455fa 100644 --- a/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift +++ b/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift @@ -83,7 +83,7 @@ extension KeyedStorage where Key == String, Value == Box { } else if let value = element.value { result.append(StringBox(value), at: element.key) } else { - result.append(NullBox(), at: element.key) + result.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key) } return result diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 07ec9b51..8890b8c6 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationXML) +import FoundationXML +#endif class XMLStackParser: NSObject { var root: XMLCoderElement? @@ -126,7 +129,7 @@ extension XMLStackParser: XMLParserDelegate { namespaceURI: String?, qualifiedName: String?, attributes attributeDict: [String: String] = [:]) { - #if os(Linux) + #if os(Linux) && !compiler(>=5.1) // For some reason, element names on linux are coming out with the namespace after the name // https://bugs.swift.org/browse/SR-11191 let elementName = elementName.components(separatedBy: ":").reversed().joined(separator: ":") diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index f3ae46f3..a88d3189 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -101,6 +101,12 @@ class XMLDecoderImplementation: Decoder { attributes: KeyedStorage() )) )) + case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox: + return KeyedDecodingContainer(XMLKeyedDecodingContainer( + referencing: self, wrapping: SharedBox(KeyedBox( + elements: KeyedStorage([("value", StringBox(""))]), attributes: KeyedStorage() + )) + )) case let keyed as SharedBox: return KeyedDecodingContainer(XMLKeyedDecodingContainer( referencing: self, @@ -212,6 +218,10 @@ extension XMLDecoderImplementation { let value = keyedBox.withShared({ $0.value as? B }) else { throw error } return value + case let singleKeyedBox as SingleKeyedBox: + guard let value = singleKeyedBox.element as? B + else { throw error } + return value case is NullBox: throw error case let keyedBox as KeyedBox: diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index cbf42eb2..f16e09dd 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -79,6 +79,10 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { let box = elements.first ?? attributes.first + if let singleKeyed = box as? SingleKeyedBox { + return singleKeyed.element.isNull + } + return box?.isNull ?? true } @@ -156,14 +160,19 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - let elements = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] - } + let elements = container.unboxed.elements[key.stringValue] - return XMLUnkeyedDecodingContainer( - referencing: decoder, - wrapping: SharedBox(elements) - ) + if let containsKeyed = elements as? [KeyedBox], let keyed = containsKeyed.first { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init)) + ) + } else { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(elements) + ) + } } public func superDecoder() throws -> Decoder { @@ -232,14 +241,29 @@ extension XMLKeyedDecodingContainer { let keyString = key.stringValue.isEmpty ? "value" : key.stringValue let value = keyedBox.elements[keyString] if !value.isEmpty { - return value + return value.map { + if let singleKeyed = $0 as? SingleKeyedBox { + return singleKeyed.element + } else { + return $0 + } + } } else if let value = keyedBox.value { return [value] } else { return [] } } else { - return keyedBox.elements[key.stringValue] + #warning("TODO: just return keyedBox.elements[key.stringValue]") + return keyedBox.elements[key.stringValue].map { + if let singleKeyed = $0 as? SingleKeyedBox { + #warning("Don't get rid of key info just yet!") + // return singleKeyed.element + return singleKeyed + } else { + return $0 + } + } } } @@ -266,6 +290,13 @@ extension XMLKeyedDecodingContainer { return empty } + // If we are looking at a coding key value intrinsic where the expected type is `String` and + // the value is empty, return `""`. + if strategy(key) != .attribute, elements.isEmpty, attributes.isEmpty, type == String.self, + key.stringValue == "value" || key.stringValue == "" { + return "" as! T + } + switch strategy(key) { case .attribute: guard @@ -301,7 +332,16 @@ extension XMLKeyedDecodingContainer { let value: T? if !(type is AnySequence.Type), let unkeyedBox = box as? UnkeyedBox, let first = unkeyedBox.first { - value = try decoder.unbox(first) + // Handle case where we have held onto a `SingleKeyedBox` + if let singleKeyed = first as? SingleKeyedBox { + if singleKeyed.element.isNull { + value = try decoder.unbox(singleKeyed) + } else { + value = try decoder.unbox(singleKeyed.element) + } + } else { + value = try decoder.unbox(first) + } } else { value = try decoder.unbox(box) } @@ -318,7 +358,6 @@ extension XMLKeyedDecodingContainer { "Expected \(type) value but found null instead." )) } - return unwrapped } diff --git a/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift b/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift new file mode 100644 index 00000000..77cb48b9 --- /dev/null +++ b/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift @@ -0,0 +1,112 @@ +// +// EmptyElementEmptyStringTests.swift +// XMLCoderTests +// +// Created by James Bean on 9/29/19. +// + +import XCTest +import XMLCoder + +class EmptyElementEmptyStringTests: XCTestCase { + + struct ContainerMultiple: Equatable, Decodable { + let things: [Thing] + + enum CodingKeys: String, CodingKey { + case things + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + var things = [Thing]() + if var thingContainer = try? container.nestedUnkeyedContainer(forKey: .things) { + while !thingContainer.isAtEnd { + things.append(try thingContainer.decode(Thing.self)) + } + } + self.things = things + } + + init(things: [Thing]) { + self.things = things + } + } + + struct ContainerSingle: Equatable, Codable { + let thing: Thing + } + + struct Thing: Equatable, Codable { + let attribute: String? + let value: String + } + + func testEmptyElementEmptyStringDecoding() throws { + let xml = """ + + """ + let expected = Thing(attribute: nil, value: "") + let result = try XMLDecoder().decode(Thing.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testEmptyElementEmptyStringWithAttributeDecoding() throws { + let xml = """ + + """ + let expected = Thing(attribute: "x", value: "") + let result = try XMLDecoder().decode(Thing.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testArrayOfEmptyElementStringDecoding() throws { + let xml = """ + + + + + + """ + let expected = [ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: ""), + Thing(attribute: nil, value: ""), + ] + let result = try XMLDecoder().decode([Thing].self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testNestedEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + """ + let expected = ContainerSingle(thing: Thing(attribute: nil, value: "")) + let result = try XMLDecoder().decode(ContainerSingle.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testNestedArrayOfEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + + + + + """ + let expected = ContainerMultiple( + things: [ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: ""), + Thing(attribute: nil, value: ""), + ] + ) + let result = try XMLDecoder().decode(ContainerMultiple.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } +} diff --git a/Tests/XMLCoderTests/Minimal/StringTests.swift b/Tests/XMLCoderTests/Minimal/StringTests.swift index ea5d6981..54ea2206 100644 --- a/Tests/XMLCoderTests/Minimal/StringTests.swift +++ b/Tests/XMLCoderTests/Minimal/StringTests.swift @@ -25,15 +25,6 @@ class StringTests: XCTestCase { ("foobar", "foobar"), ] - func testMissing() { - let decoder = XMLDecoder() - - let xmlString = "" - let xmlData = xmlString.data(using: .utf8)! - - XCTAssertThrowsError(try decoder.decode(Container.self, from: xmlData)) - } - func testAttribute() throws { let decoder = XMLDecoder() let encoder = XMLEncoder() diff --git a/Tests/XMLCoderTests/NestedChoiceArrayTest.swift b/Tests/XMLCoderTests/NestedChoiceArrayTest.swift new file mode 100644 index 00000000..b7e4c84d --- /dev/null +++ b/Tests/XMLCoderTests/NestedChoiceArrayTest.swift @@ -0,0 +1,116 @@ +// +// NestedChoiceArrayTest.swift +// XMLCoder +// +// Created by Benjamin Wetherfield on 8/22/19. +// + +import XCTest +@testable import XMLCoder + +private struct Book: Decodable { + let title: String + let chapters: [Chapter] + + enum CodingKeys: String, CodingKey { + case title + case chapters + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + title = try container.decode(String.self, forKey: .title) + + var chapters = [Chapter]() + + if var chapterContainer = try? container.nestedUnkeyedContainer(forKey: .chapters) { + while !chapterContainer.isAtEnd { + chapters.append(try chapterContainer.decode(Chapter.self)) + } + } + + self.chapters = chapters + } + + init(title: String, chapters: [Chapter]) { + self.title = title + self.chapters = chapters + } +} + +private enum Chapter { + struct Content { + let title: String + let content: String + } + + case intro(Content) + case body(Content) + case outro(Content) +} + +private enum BookError: Error { + case unknownChapterType +} + +extension Chapter: Decodable { + enum CodingKeys: String, XMLChoiceCodingKey { + case intro, body = "chapter", outro + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + self = .body(try container.decode(Content.self, forKey: .body)) + } catch { + do { + self = .intro(try container.decode(Content.self, forKey: .intro)) + } catch { + self = .outro(try container.decode(Content.self, forKey: .outro)) + } + } + } +} + +extension Chapter.Content: Decodable { + enum CodingKeys: String, CodingKey { + case title + case value = "" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + title = try container.decode(String.self, forKey: .title) + content = try container.decode(String.self, forKey: .value) + } +} + +extension Book: Equatable {} +extension Chapter: Equatable {} +extension Chapter.Content: Equatable {} + +class NestedChoiceArrayTest: XCTestCase { + func testDecodingNestedChoiceArray() throws { + let xml = """ + + + + Content of first chapter + Content of chapter 1 + Content of chapter 2 + Content of last chapter + + + """ + let decoded = try XMLDecoder().decode(Book.self, from: xml.data(using: .utf8)!) + let expected = Book(title: "Example", + chapters: [ + .intro(.init(title: "Intro", content: "Content of first chapter")), + .body(.init(title: "Chapter 1", content: "Content of chapter 1")), + .body(.init(title: "Chapter 2", content: "Content of chapter 2")), + .outro(.init(title: "Epilogue", content: "Content of last chapter")), + ]) + XCTAssertEqual(decoded, expected) + } +} diff --git a/Tests/XMLCoderTests/NestedChoiceTests.swift b/Tests/XMLCoderTests/NestedChoiceTests.swift index 4810c454..44344c49 100644 --- a/Tests/XMLCoderTests/NestedChoiceTests.swift +++ b/Tests/XMLCoderTests/NestedChoiceTests.swift @@ -227,6 +227,50 @@ class NestedChoiceTests: XCTestCase { XCTAssertEqual(result, expected) } + func testNestedEnumsWithEmptyStruct() throws { + let xml = """ + +

+

+ + 1518 + I am answering it again. + + + 431 + A Word About Wake Times + +

+

+ + 1519 + I am answering it again. + +
+

+
+ """ + let result = try XMLDecoder().decode(Container.self, from: xml.data(using: .utf8)!) + let expected = Container( + paragraphs: [ + Paragraph( + entries: [ + .br(Break()), + .run(Run(id: 1518, text: "I am answering it again.")), + .properties(Properties(id: 431, title: "A Word About Wake Times")), + ] + ), + Paragraph( + entries: [ + .run(Run(id: 1519, text: "I am answering it again.")), + .br(Break()), + ] + ), + ] + ) + XCTAssertEqual(result, expected) + } + func testNestedEnumsRoundTrip() throws { let original = Container( paragraphs: [ diff --git a/XMLCoder.podspec b/XMLCoder.podspec index 167f9db4..4fcc8404 100644 --- a/XMLCoder.podspec +++ b/XMLCoder.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "XMLCoder" - s.version = "0.7.0" + s.version = "0.8.0" s.summary = "XMLEncoder & XMLDecoder using the Codable protocol in Swift" s.description = "XMLCoder allows Swift Codable-conforming objects to be translated to and from XML" s.homepage = "https://github.com/MaxDesiatov/XMLCoder" diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index e6c05c36..cf9b651d 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 07E441BA2340F14B00890F46 /* EmptyElementEmptyStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */; }; + B5EA3BB6230F237800D8D69B /* NestedChoiceArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */; }; OBJ_148 /* BoolBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* BoolBox.swift */; }; OBJ_149 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Box.swift */; }; OBJ_150 /* ChoiceBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* ChoiceBox.swift */; }; @@ -152,6 +154,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyElementEmptyStringTests.swift; sourceTree = ""; }; + B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedChoiceArrayTest.swift; sourceTree = ""; }; OBJ_100 /* DecimalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTests.swift; sourceTree = ""; }; OBJ_101 /* EmptyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTests.swift; sourceTree = ""; }; OBJ_102 /* FloatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatTests.swift; sourceTree = ""; }; @@ -380,7 +384,7 @@ path = Encoder; sourceTree = ""; }; - OBJ_5 /* */ = { + OBJ_5 = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, @@ -400,7 +404,6 @@ OBJ_141 /* docs.sh */, OBJ_142 /* pod.sh */, ); - name = ""; sourceTree = ""; }; OBJ_58 /* Tests */ = { @@ -441,12 +444,14 @@ OBJ_119 /* NoteTest.swift */, OBJ_120 /* PlantCatalog.swift */, OBJ_121 /* PlantTest.swift */, + 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */, OBJ_122 /* RJISample.swift */, OBJ_123 /* RJITest.swift */, OBJ_124 /* RelationshipsTest.swift */, OBJ_125 /* SimpleChoiceTests.swift */, OBJ_126 /* SingleChildTests.swift */, OBJ_127 /* SpacePreserveTest.swift */, + B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, ); name = XMLCoderTests; path = Tests/XMLCoderTests; @@ -595,7 +600,7 @@ English, en, ); - mainGroup = OBJ_5 /* */; + mainGroup = OBJ_5; productRefGroup = OBJ_128 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -717,7 +722,9 @@ OBJ_251 /* KeyedTests.swift in Sources */, OBJ_252 /* NullTests.swift in Sources */, OBJ_253 /* OptionalTests.swift in Sources */, + 07E441BA2340F14B00890F46 /* EmptyElementEmptyStringTests.swift in Sources */, OBJ_254 /* StringTests.swift in Sources */, + B5EA3BB6230F237800D8D69B /* NestedChoiceArrayTest.swift in Sources */, OBJ_255 /* UIntTests.swift in Sources */, OBJ_256 /* URLTests.swift in Sources */, OBJ_257 /* UnkeyedIntTests.swift in Sources */, @@ -898,6 +905,7 @@ CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 0.8.0; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = YES; @@ -925,6 +933,7 @@ CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 0.8.0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_OPTIMIZATION_LEVEL = s; diff --git a/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme b/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme index c117e5d6..59df07f8 100644 --- a/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme +++ b/XMLCoder.xcodeproj/xcshareddata/xcschemes/XMLCoder.xcscheme @@ -39,8 +39,6 @@ - - - -