diff --git a/Source/SWXMLHash.swift b/Source/SWXMLHash.swift index 0259835d..b812dbc7 100644 --- a/Source/SWXMLHash.swift +++ b/Source/SWXMLHash.swift @@ -183,7 +183,6 @@ protocol SimpleXmlParser { #if os(Linux) extension XMLParserDelegate { - func parserDidStartDocument(_ parser: Foundation.XMLParser) { } func parserDidEndDocument(_ parser: Foundation.XMLParser) { } @@ -303,7 +302,6 @@ class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate { namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String]) { - elementStack.push(elementName) if !onMatch() { @@ -342,7 +340,6 @@ class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate { didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { - let match = onMatch() elementStack.drop() @@ -400,7 +397,6 @@ class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate { namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String]) { - let currentNode = parentStack .top() .addElement(elementName, withAttributes: attributeDict, caseInsensitive: self.options.caseInsensitive) @@ -418,7 +414,6 @@ class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate { didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { - parentStack.drop() } @@ -1043,3 +1038,72 @@ fileprivate extension String { return str1 == str2 } } + +// MARK: - XMLIndexer String RawRepresentables + +/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +extension XMLIndexer { + /** + Allows for element lookup by matching attribute values + using a String backed RawRepresentables (E.g. `String` backed `enum` cases) + + - Note: + Convenience for withAttribute(String, String) + + - parameters: + - attr: should the name of the attribute to match on + - value: should be the value of the attribute to match on + - throws: an XMLIndexer.XMLError if an element with the specified attribute isn't found + - returns: instance of XMLIndexer + */ + public func withAttribute(_ attr: A, _ value: V) throws -> XMLIndexer + where A.RawValue == String, V.RawValue == String { + return try withAttribute(attr.rawValue, value.rawValue) + } + + /** + Find an XML element at the current level by element name + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for byKey(String) + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by key + - throws: Throws an XMLIndexingError.Key if no element was found + */ + public func byKey(_ key: K) throws -> XMLIndexer where K.RawValue == String { + return try byKey(key.rawValue) + } + + /** + Find an XML element at the current level by element name + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for self[String] + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by + */ + public subscript(key: K) -> XMLIndexer where K.RawValue == String { + return self[key.rawValue] + } +} + +// MARK: - XMLElement String RawRepresentables + +/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +extension XMLElement { + /** + Find an attribute by name using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for self[String] + */ + public func attribute(by name: N) -> XMLAttribute? where N.RawValue == String { + return attribute(by: name.rawValue) + } +} diff --git a/Source/XMLIndexer+XMLIndexerDeserializable.swift b/Source/XMLIndexer+XMLIndexerDeserializable.swift index ec4b59e0..508a1a43 100644 --- a/Source/XMLIndexer+XMLIndexerDeserializable.swift +++ b/Source/XMLIndexer+XMLIndexerDeserializable.swift @@ -102,7 +102,6 @@ public extension XMLAttributeDeserializable { // MARK: - XMLIndexer Extensions public extension XMLIndexer { - // MARK: - XMLAttributeDeserializable /** @@ -392,10 +391,89 @@ public extension XMLIndexer { } } +// MARK: - XMLAttributeDeserializable String RawRepresentable + +/*: Provides XMLIndexer XMLAttributeDeserializable deserialization from String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +public extension XMLIndexer { + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `T` using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `T` value + */ + func value(ofAttribute attr: A) throws -> T where A.RawValue == String { + return try value(ofAttribute: attr.rawValue) + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `T?` using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - returns: The deserialized `T?` value, or nil if the attribute does not exist + */ + func value(ofAttribute attr: A) -> T? where A.RawValue == String { + return value(ofAttribute: attr.rawValue) + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T]` using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T]` value + */ + func value(ofAttribute attr: A) throws -> [T] where A.RawValue == String { + return try value(ofAttribute: attr.rawValue) + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T]?` using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T]?` value + */ + func value(ofAttribute attr: A) throws -> [T]? where A.RawValue == String { + return try value(ofAttribute: attr.rawValue) + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T?]` using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T?]` value + */ + func value(ofAttribute attr: A) throws -> [T?] where A.RawValue == String { + return try value(ofAttribute: attr.rawValue) + } +} + // MARK: - XMLElement Extensions extension XMLElement { - /** Attempts to deserialize the specified attribute of the current XMLElement to `T` @@ -441,6 +519,41 @@ extension XMLElement { } } +// MARK: String RawRepresentable + +/*: Provides XMLElement XMLAttributeDeserializable deserialization from String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +public extension XMLElement { + /** + Attempts to deserialize the specified attribute of the current XMLElement to `T` + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `T` value + */ + public func value(ofAttribute attr: A) throws -> T where A.RawValue == String { + return try value(ofAttribute: attr.rawValue) + } + + /** + Attempts to deserialize the specified attribute of the current XMLElement to `T?` + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for value(ofAttribute: String) + + - parameter attr: The attribute to deserialize + - returns: The deserialized `T?` value, or nil if the attribute does not exist. + */ + public func value(ofAttribute attr: A) -> T? where A.RawValue == String { + return value(ofAttribute: attr.rawValue) + } +} + // MARK: - XMLDeserializationError /// The error that is thrown if there is a problem with deserialization diff --git a/Tests/SWXMLHashTests/TypeConversionBasicTypesTests.swift b/Tests/SWXMLHashTests/TypeConversionBasicTypesTests.swift index ad0598db..7bbac955 100644 --- a/Tests/SWXMLHashTests/TypeConversionBasicTypesTests.swift +++ b/Tests/SWXMLHashTests/TypeConversionBasicTypesTests.swift @@ -161,6 +161,48 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + // swiftlint:disable nesting + func testShouldConvertAttributeToNonOptionalWithStringRawRepresentable() { + enum Keys: String { + case string + } + do { + let value: String = try parser!["root"]["attr"].value(ofAttribute: Keys.string) + XCTAssertEqual(value, "stringValue") + } catch { + XCTFail("\(error)") + } + } + + func testShouldConvertAttributeToOptionalWithStringRawRepresentable() { + enum Keys: String { + case string + } + let value: String? = parser!["root"]["attr"].value(ofAttribute: Keys.string) + XCTAssertEqual(value, "stringValue") + } + + func testShouldThrowWhenConvertingMissingAttributeToNonOptionalWithStringRawRepresentable() { + enum Keys: String { + case missing + } + XCTAssertThrowsError(try (parser!["root"]["attr"].value(ofAttribute: Keys.missing) as String)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testShouldConvertMissingAttributeToOptionalWithStringRawRepresentable() { + enum Keys: String { + case missing + } + let value: String? = parser!["root"]["attr"].value(ofAttribute: Keys.missing) + XCTAssertNil(value) + } + // swiftlint:enable nesting + func testIntShouldConvertValueToNonOptional() { do { let value: Int = try parser!["root"]["int"].value() @@ -494,6 +536,7 @@ class TypeConversionBasicTypesTests: XCTestCase { } let correctAttributeItem = AttributeItem(name: "the name of attribute item", price: 19.99) + let correctAttributeItemStringRawRepresentable = AttributeItemStringRawRepresentable(name: "the name of attribute item", price: 19.99) func testAttributeItemShouldConvertAttributeItemToNonOptional() { do { @@ -504,6 +547,15 @@ class TypeConversionBasicTypesTests: XCTestCase { } } + func testAttributeItemStringRawRepresentableShouldConvertAttributeItemToNonOptional() { + do { + let value: AttributeItemStringRawRepresentable = try parser!["root"]["attributeItem"].value() + XCTAssertEqual(value, correctAttributeItemStringRawRepresentable) + } catch { + XCTFail("\(error)") + } + } + func testAttributeItemShouldThrowWhenConvertingEmptyToNonOptional() { XCTAssertThrowsError(try (parser!["root"]["empty"].value() as AttributeItem)) { error in guard error is XMLDeserializationError else { @@ -609,6 +661,30 @@ extension AttributeItem: Equatable { } } +struct AttributeItemStringRawRepresentable: XMLElementDeserializable { + private enum Keys: String { + case name + case price + } + + let name: String + let price: Double + + static func deserialize(_ element: SWXMLHash.XMLElement) throws -> AttributeItemStringRawRepresentable { + print("my deserialize") + return try AttributeItemStringRawRepresentable( + name: element.value(ofAttribute: Keys.name), + price: element.value(ofAttribute: Keys.price) + ) + } +} + +extension AttributeItemStringRawRepresentable: Equatable { + static func == (a: AttributeItemStringRawRepresentable, b: AttributeItemStringRawRepresentable) -> Bool { + return a.name == b.name && a.price == b.price + } +} + extension TypeConversionBasicTypesTests { static var allTests: [(String, (TypeConversionBasicTypesTests) -> () throws -> Void)] { return [ @@ -622,6 +698,10 @@ extension TypeConversionBasicTypesTests { ("testShouldConvertAttributeToOptional", testShouldConvertAttributeToOptional), ("testShouldThrowWhenConvertingMissingAttributeToNonOptional", testShouldThrowWhenConvertingMissingAttributeToNonOptional), ("testShouldConvertMissingAttributeToOptional", testShouldConvertMissingAttributeToOptional), + ("testShouldConvertAttributeToNonOptionalWithStringRawRepresentable", testShouldConvertAttributeToNonOptionalWithStringRawRepresentable), + ("testShouldConvertAttributeToOptionalWithStringRawRepresentable", testShouldConvertAttributeToOptionalWithStringRawRepresentable), + ("testShouldThrowWhenConvertingMissingAttributeToNonOptionalWithStringRawRepresentable", testShouldThrowWhenConvertingMissingAttributeToNonOptionalWithStringRawRepresentable), + ("testShouldConvertMissingAttributeToOptionalWithStringRawRepresentable", testShouldConvertMissingAttributeToOptionalWithStringRawRepresentable), ("testIntShouldConvertValueToNonOptional", testIntShouldConvertValueToNonOptional), ("testIntShouldThrowWhenConvertingEmptyToNonOptional", testIntShouldThrowWhenConvertingEmptyToNonOptional), ("testIntShouldThrowWhenConvertingMissingToNonOptional", testIntShouldThrowWhenConvertingMissingToNonOptional), @@ -661,6 +741,7 @@ extension TypeConversionBasicTypesTests { ("testBasicItemShouldConvertEmptyToOptional", testBasicItemShouldConvertEmptyToOptional), ("testBasicItemShouldConvertMissingToOptional", testBasicItemShouldConvertMissingToOptional), ("testAttributeItemShouldConvertAttributeItemToNonOptional", testAttributeItemShouldConvertAttributeItemToNonOptional), + ("testAttributeItemStringRawRepresentableShouldConvertAttributeItemToNonOptional", testAttributeItemStringRawRepresentableShouldConvertAttributeItemToNonOptional), ("testAttributeItemShouldThrowWhenConvertingEmptyToNonOptional", testAttributeItemShouldThrowWhenConvertingEmptyToNonOptional), ("testAttributeItemShouldThrowWhenConvertingMissingToNonOptional", testAttributeItemShouldThrowWhenConvertingMissingToNonOptional), ("testAttributeItemShouldConvertAttributeItemToOptional", testAttributeItemShouldConvertAttributeItemToOptional), diff --git a/Tests/SWXMLHashTests/TypeConversionPrimitypeTypesTests.swift b/Tests/SWXMLHashTests/TypeConversionPrimitypeTypesTests.swift index 919f9738..2db60312 100644 --- a/Tests/SWXMLHashTests/TypeConversionPrimitypeTypesTests.swift +++ b/Tests/SWXMLHashTests/TypeConversionPrimitypeTypesTests.swift @@ -167,6 +167,47 @@ class TypeConversionPrimitypeTypesTests: XCTestCase { } } + // swiftlint:disable nesting + func testShouldConvertArrayOfAttributeIntsToNonOptionalWithStringRawRepresentable() { + enum Keys: String { + case value + } + do { + let value: [Int] = try parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: Keys.value) + XCTAssertEqual(value, [0, 1, 2, 3]) + } catch { + XCTFail("\(error)") + } + } + + func testShouldConvertArrayOfAttributeIntsToOptionalWithStringRawRepresentable() { + enum Keys: String { + case value + } + do { + let value: [Int]? = try parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: Keys.value) + XCTAssertNotNil(value) + if let value = value { + XCTAssertEqual(value, [0, 1, 2, 3]) + } + } catch { + XCTFail("\(error)") + } + } + + func testShouldConvertArrayOfAttributeIntsToArrayOfOptionalsWithStringRawRepresentable() { + enum Keys: String { + case value + } + do { + let value: [Int?] = try parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: Keys.value) + XCTAssertEqual(value.compactMap({ $0 }), [0, 1, 2, 3]) + } catch { + XCTFail("\(error)") + } + } + // swiftlint:enable nesting + func testShouldConvertEmptyArrayOfIntsToNonOptional() { do { let value: [Int] = try parser!["root"]["empty"]["int"].value() @@ -210,6 +251,9 @@ extension TypeConversionPrimitypeTypesTests { ("testShouldConvertArrayOfAttributeIntsToNonOptional", testShouldConvertArrayOfAttributeIntsToNonOptional), ("testShouldConvertArrayOfAttributeIntsToOptional", testShouldConvertArrayOfAttributeIntsToOptional), ("testShouldConvertArrayOfAttributeIntsToArrayOfOptionals", testShouldConvertArrayOfAttributeIntsToArrayOfOptionals), + ("testShouldConvertArrayOfAttributeIntsToNonOptionalWithStringRawRepresentable", testShouldConvertArrayOfAttributeIntsToNonOptionalWithStringRawRepresentable), + ("testShouldConvertArrayOfAttributeIntsToOptionalWithStringRawRepresentable", testShouldConvertArrayOfAttributeIntsToOptionalWithStringRawRepresentable), + ("testShouldConvertArrayOfAttributeIntsToArrayOfOptionalsWithStringRawRepresentable", testShouldConvertArrayOfAttributeIntsToArrayOfOptionalsWithStringRawRepresentable), ("testShouldConvertEmptyArrayOfIntsToNonOptional", testShouldConvertEmptyArrayOfIntsToNonOptional), ("testShouldConvertEmptyArrayOfIntsToOptional", testShouldConvertEmptyArrayOfIntsToOptional), ("testShouldConvertEmptyArrayOfIntsToArrayOfOptionals", testShouldConvertEmptyArrayOfIntsToArrayOfOptionals) diff --git a/Tests/SWXMLHashTests/XMLParsingTests.swift b/Tests/SWXMLHashTests/XMLParsingTests.swift index 2346e688..97a70805 100644 --- a/Tests/SWXMLHashTests/XMLParsingTests.swift +++ b/Tests/SWXMLHashTests/XMLParsingTests.swift @@ -27,6 +27,7 @@ import SWXMLHash import XCTest // swiftlint:disable line_length +// swiftlint:disable file_length // swiftlint:disable type_body_length class XMLParsingTests: XCTestCase { @@ -74,6 +75,15 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual(xml!["root"]["header"]["title"].element?.text, "Test Title Header") } + // swiftlint:disable nesting + func testShouldBeAbleToParseIndividualElementsWithStringRawRepresentable() { + enum Keys: String { + case root; case header; case title + } + XCTAssertEqual(xml![Keys.root][Keys.header][Keys.title].element?.text, "Test Title Header") + } + // swiftlint:enable nesting + func testShouldBeAbleToParseElementGroups() { XCTAssertEqual(xml!["root"]["catalog"]["book"][1]["author"].element?.text, "Ralls, Kim") } @@ -90,6 +100,15 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual(xml!["root"]["catalog"]["book"][1].element?.attribute(by: "id")?.text, "bk102") } + // swiftlint:disable nesting + // swiftlint:disable identifier_name + func testShouldBeAbleToParseAttributesWithStringRawRepresentable() { + enum Keys: String { + case root; case catalog; case book; case id + } + XCTAssertEqual(xml![Keys.root][Keys.catalog][Keys.book][1].element?.attribute(by: Keys.id)?.text, "bk102") + } + func testShouldBeAbleToLookUpElementsByNameAndAttribute() { do { let value = try xml!["root"]["catalog"]["book"].withAttribute("id", "bk102")["author"].element?.text @@ -99,6 +118,21 @@ class XMLParsingTests: XCTestCase { } } + func testShouldBeAbleToLookUpElementsByNameAndAttributeWithStringRawRepresentable() { + enum Keys: String { + case root; case catalog; case book; case id; case bk102; case author + } + do { + let value = try xml![Keys.root][Keys.catalog][Keys.book].withAttribute(Keys.id, Keys.bk102)[Keys.author].element?.text + XCTAssertEqual(value, "Ralls, Kim") + } catch { + XCTFail("\(error)") + } + } + + // swiftlint:enable nesting + // swiftlint:enable identifier_name + func testShouldBeAbleToLookUpElementsByNameAndAttributeCaseInsensitive() { do { let xmlInsensitive = SWXMLHash.config({ config in @@ -230,6 +264,27 @@ class XMLParsingTests: XCTestCase { } catch { err = nil } } + // swiftlint:disable nesting + /** + Added Only test coverage for: + `byKey(_ key: K) throws -> XMLIndexer where K.RawValue == String` + */ + func testShouldProvideAnErrorObjectWhenKeysDontMatchWithStringRawRepresentable() { + enum Keys: String { + case root; case what; case header; case foo + } + var err: IndexingError? + defer { + XCTAssertNotNil(err) + } + do { + _ = try xml!.byKey(Keys.root).byKey(Keys.what).byKey(Keys.header).byKey(Keys.foo) + } catch let error as IndexingError { + err = error + } catch { err = nil } + } + // swiftlint:enable nesting + func testShouldProvideAnErrorElementWhenIndexersDontMatch() { var err: IndexingError? defer { @@ -339,11 +394,14 @@ extension XMLParsingTests { static var allTests: [(String, (XMLParsingTests) -> () throws -> Void)] { return [ ("testShouldBeAbleToParseIndividualElements", testShouldBeAbleToParseIndividualElements), + ("testShouldBeAbleToParseIndividualElementsWithStringRawRepresentable", testShouldBeAbleToParseIndividualElementsWithStringRawRepresentable), ("testShouldBeAbleToParseElementGroups", testShouldBeAbleToParseElementGroups), ("testShouldBeAbleToParseElementGroupsByIndex", testShouldBeAbleToParseElementGroupsByIndex), ("testShouldBeAbleToByIndexWithoutGoingOutOfBounds", testShouldBeAbleToByIndexWithoutGoingOutOfBounds), ("testShouldBeAbleToParseAttributes", testShouldBeAbleToParseAttributes), + ("testShouldBeAbleToParseAttributesWithStringRawRepresentable", testShouldBeAbleToParseAttributesWithStringRawRepresentable), ("testShouldBeAbleToLookUpElementsByNameAndAttribute", testShouldBeAbleToLookUpElementsByNameAndAttribute), + ("testShouldBeAbleToLookUpElementsByNameAndAttributeWithStringRawRepresentable", testShouldBeAbleToLookUpElementsByNameAndAttributeWithStringRawRepresentable), ("testShouldBeAbleToLookUpElementsByNameAndAttributeCaseInsensitive", testShouldBeAbleToLookUpElementsByNameAndAttributeCaseInsensitive), ("testShouldBeAbleToIterateElementGroups", testShouldBeAbleToIterateElementGroups), ("testShouldBeAbleToIterateElementGroupsEvenIfOnlyOneElementIsFound", testShouldBeAbleToIterateElementGroupsEvenIfOnlyOneElementIsFound), @@ -357,6 +415,7 @@ extension XMLParsingTests { ("testShouldBeAbleToProvideADescriptionForTheDocument", testShouldBeAbleToProvideADescriptionForTheDocument), ("testShouldReturnNilWhenKeysDontMatch", testShouldReturnNilWhenKeysDontMatch), ("testShouldProvideAnErrorObjectWhenKeysDontMatch", testShouldProvideAnErrorObjectWhenKeysDontMatch), + ("testShouldProvideAnErrorObjectWhenKeysDontMatchWithStringRawRepresentable", testShouldProvideAnErrorObjectWhenKeysDontMatchWithStringRawRepresentable), ("testShouldProvideAnErrorElementWhenIndexersDontMatch", testShouldProvideAnErrorElementWhenIndexersDontMatch), ("testShouldStillReturnErrorsWhenAccessingViaSubscripting", testShouldStillReturnErrorsWhenAccessingViaSubscripting), ("testShouldBeAbleToCreateASubIndexerFromFilter", testShouldBeAbleToCreateASubIndexerFromFilter),