diff --git a/README.md b/README.md index 6464f697..37b7cb76 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Given: The below will return "123". ```swift -xml["root"]["catalog"]["book"][1].element?.attributes["id"] +xml["root"]["catalog"]["book"][1].element?.attribute(by: "id")?.text ``` Alternatively, you can look up an element with specific attributes. The below will return "John". @@ -290,17 +290,17 @@ Given: ```xml - + Book A 12.5 2015 - + Book B 10 1988 - + Book C 8.33 1990 @@ -317,13 +317,15 @@ struct Book: XMLIndexerDeserializable { let price: Double let year: Int let amount: Int? + let isbn: Int static func deserialize(node: XMLIndexer) throws -> Book { return try Book( title: node["title"].value(), price: node["price"].value(), year: node["year"].value(), - amount: node["amount"].value() + amount: node["amount"].value(), + isbn: node.value(ofAttribute: "isbn") ) } } @@ -337,9 +339,11 @@ let books: [Book] = try xml["root"]["books"]["book"].value() Types Conversion -Built-in, leaf-nodes converters support `Int`, `Double`, `Float`, `Bool`, and `String` values (both non- and -optional variants). Custom converters can be added by implementing `XMLElementDeserializable`. +You can convert any XML to your custom type by implementing `XMLIndexerDeserializable` for any non-leaf node (e.g. `` in the example above). + +For leaf nodes (e.g. `` in the example above), built-in converters support `Int`, `Double`, `Float`, `Bool`, and `String` values (both non- and -optional variants). Custom converters can be added by implementing `XMLElementDeserializable`. -You can convert any XML to your custom type by implementing `XMLIndexerDeserializable`. +For attributes (e.g. `isbn=` in the example above), built-in converters support the same types as above, and additional converters can be added by implementing `XMLAttributeDeserializable`. Types conversion supports error handling, optionals and arrays. For more examples, look into `SWXMLHashTests.swift` or play with types conversion directly in the Swift playground. diff --git a/Source/Info.plist b/Source/Info.plist index 78122054..d2035fbf 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -15,7 +15,7 @@ <key>CFBundlePackageType</key> <string>FMWK</string> <key>CFBundleShortVersionString</key> - <string>1.0</string> + <string>2.0</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> diff --git a/Source/SWXMLHash+TypeConversion.swift b/Source/SWXMLHash+TypeConversion.swift index a4e1beff..0394f866 100644 --- a/Source/SWXMLHash+TypeConversion.swift +++ b/Source/SWXMLHash+TypeConversion.swift @@ -6,6 +6,8 @@ // // +// swiftlint:disable file_length + import Foundation // MARK: - XMLIndexerDeserializable @@ -55,9 +57,135 @@ public extension XMLElementDeserializable { } } +// MARK: - XMLAttributeDeserializable + +/// Provides XMLAttribute deserialization / type transformation support +public protocol XMLAttributeDeserializable { + static func deserialize(attribute: XMLAttribute) throws -> Self +} + +/// Provides XMLAttribute deserialization / type transformation support +public extension XMLAttributeDeserializable { + /** + A default implementation that will throw an error if it is called + + - parameters: + - attribute: The XMLAttribute to be deserialized + - throws: an XMLDeserializationError.ImplementationIsMissing if no implementation is found + - returns: this won't ever return because of the error being thrown + */ + static func deserialize(attribute: XMLAttribute) throws -> Self { + throw XMLDeserializationError.ImplementationIsMissing( + method: "XMLAttributeDeserializable(element: XMLAttribute)") + } +} + +// MARK: - XMLIndexer Extensions public extension XMLIndexer { + // MARK: - XMLAttributeDeserializable + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `T` + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `T` value + */ + func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) throws -> T { + switch self { + case .Element(let element): + return try element.value(ofAttribute: attr) + case .Stream(let opStream): + return try opStream.findElements().value(ofAttribute: attr) + default: + throw XMLDeserializationError.NodeIsInvalid(node: self) + } + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `T?` + + - parameter attr: The attribute to deserialize + - returns: The deserialized `T?` value, or nil if the attribute does not exist + */ + func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) -> T? { + switch self { + case .Element(let element): + return element.value(ofAttribute: attr) + case .Stream(let opStream): + return opStream.findElements().value(ofAttribute: attr) + default: + return nil + } + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T]` + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T]` value + */ + func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) throws -> [T] { + switch self { + case .List(let elements): + return try elements.map { try $0.value(ofAttribute: attr) } + case .Element(let element): + return try [element].map { try $0.value(ofAttribute: attr) } + case .Stream(let opStream): + return try opStream.findElements().value(ofAttribute: attr) + default: + throw XMLDeserializationError.NodeIsInvalid(node: self) + } + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T]?` + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T]?` value + */ + func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) throws -> [T]? { + switch self { + case .List(let elements): + return try elements.map { try $0.value(ofAttribute: attr) } + case .Element(let element): + return try [element].map { try $0.value(ofAttribute: attr) } + case .Stream(let opStream): + return try opStream.findElements().value(ofAttribute: attr) + default: + return nil + } + } + + /** + Attempts to deserialize the value of the specified attribute of the current XMLIndexer + element to `[T?]` + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `[T?]` value + */ + func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) throws -> [T?] { + switch self { + case .List(let elements): + return elements.map { $0.value(ofAttribute: attr) } + case .Element(let element): + return [element].map { $0.value(ofAttribute: attr) } + case .Stream(let opStream): + return try opStream.findElements().value(ofAttribute: attr) + default: + throw XMLDeserializationError.NodeIsInvalid(node: self) + } + } + // MARK: - XMLElementDeserializable /** @@ -71,7 +199,7 @@ public extension XMLIndexer { case .Element(let element): return try T.deserialize(element) case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: throw XMLDeserializationError.NodeIsInvalid(node: self) } @@ -88,7 +216,7 @@ public extension XMLIndexer { case .Element(let element): return try T.deserialize(element) case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: return nil } @@ -107,7 +235,7 @@ public extension XMLIndexer { case .Element(let element): return try [element].map { try T.deserialize($0) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: return [] } @@ -126,7 +254,7 @@ public extension XMLIndexer { case .Element(let element): return try [element].map { try T.deserialize($0) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: return nil } @@ -145,7 +273,7 @@ public extension XMLIndexer { case .Element(let element): return try [element].map { try T.deserialize($0) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: return [] } @@ -165,7 +293,7 @@ public extension XMLIndexer { case .Element: return try T.deserialize(self) case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: throw XMLDeserializationError.NodeIsInvalid(node: self) } @@ -182,7 +310,7 @@ public extension XMLIndexer { case .Element: return try T.deserialize(self) case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: return nil } @@ -201,7 +329,7 @@ public extension XMLIndexer { case .Element(let element): return try [element].map { try T.deserialize( XMLIndexer($0) ) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: throw XMLDeserializationError.NodeIsInvalid(node: self) } @@ -220,7 +348,7 @@ public extension XMLIndexer { case .Element(let element): return try [element].map { try T.deserialize( XMLIndexer($0) ) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: throw XMLDeserializationError.NodeIsInvalid(node: self) } @@ -235,31 +363,73 @@ public extension XMLIndexer { func value<T: XMLIndexerDeserializable>() throws -> [T?] { switch self { case .List(let elements): - return try elements.map { try T.deserialize( XMLIndexer($0) ) } + return try elements.map { try T.deserialize( XMLIndexer($0) ) } case .Element(let element): return try [element].map { try T.deserialize( XMLIndexer($0) ) } case .Stream(let opStream): - return try! opStream.findElements().value() + return try opStream.findElements().value() default: throw XMLDeserializationError.NodeIsInvalid(node: self) } } } -private extension XMLElement { - func nonEmptyTextOrThrow() throws -> String { +// MARK: - XMLElement Extensions + +extension XMLElement { + + /** + Attempts to deserialize the specified attribute of the current XMLElement to `T` + + - parameter attr: The attribute to deserialize + - throws: an XMLDeserializationError if there is a problem with deserialization + - returns: The deserialized `T` value + */ + public func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) throws -> T { + if let attr = self.attribute(by: attr) { + return try T.deserialize(attr) + } else { + throw XMLDeserializationError.AttributeDoesNotExist(element: self, attribute: attr) + } + } + + /** + Attempts to deserialize the specified attribute of the current XMLElement to `T?` + + - parameter attr: The attribute to deserialize + - returns: The deserialized `T?` value, or nil if the attribute does not exist. + */ + public func value<T: XMLAttributeDeserializable>(ofAttribute attr: String) -> T? { + if let attr = self.attribute(by: attr) { + return try? T.deserialize(attr) + } else { + return nil + } + } + + /** + Gets the text associated with this element, or throws an exception if the text is empty + + - throws: XMLDeserializationError.NodeHasNoValue if the element text is empty + - returns: The element text + */ + private func nonEmptyTextOrThrow() throws -> String { if let text = self.text where !text.characters.isEmpty { return text } else { throw XMLDeserializationError.NodeHasNoValue } } } +// MARK: - XMLDeserializationError + /// The error that is thrown if there is a problem with deserialization public enum XMLDeserializationError: ErrorType, CustomStringConvertible { case ImplementationIsMissing(method: String) case NodeIsInvalid(node: XMLIndexer) case NodeHasNoValue case TypeConversionFailed(type: String, element: XMLElement) + case AttributeDoesNotExist(element: XMLElement, attribute: String) + case AttributeDeserializationFailed(type: String, attribute: XMLAttribute) /// The text description for the error thrown public var description: String { @@ -272,6 +442,10 @@ public enum XMLDeserializationError: ErrorType, CustomStringConvertible { return "This node is empty" case .TypeConversionFailed(let type, let node): return "Can't convert node \(node) to value of type \(type)" + case .AttributeDoesNotExist(let element, let attribute): + return "Element \(element) does not contain attribute: \(attribute)" + case .AttributeDeserializationFailed(let type, let attribute): + return "Can't convert attribute \(attribute) to value of type \(type)" } } } @@ -279,7 +453,7 @@ public enum XMLDeserializationError: ErrorType, CustomStringConvertible { // MARK: - Common types deserialization -extension String: XMLElementDeserializable { +extension String: XMLElementDeserializable, XMLAttributeDeserializable { /** Attempts to deserialize XML element content to a String @@ -289,15 +463,24 @@ extension String: XMLElementDeserializable { - returns: the deserialized String value */ public static func deserialize(element: XMLElement) throws -> String { - guard let text = element.text - else { + guard let text = element.text else { throw XMLDeserializationError.TypeConversionFailed(type: "String", element: element) } return text } + + /** + Attempts to deserialize XML Attribute content to a String + + - parameter attribute: the XMLAttribute to be deserialized + - returns: the deserialized String value + */ + public static func deserialize(attribute: XMLAttribute) -> String { + return attribute.text + } } -extension Int: XMLElementDeserializable { +extension Int: XMLElementDeserializable, XMLAttributeDeserializable { /** Attempts to deserialize XML element content to a Int @@ -307,13 +490,30 @@ extension Int: XMLElementDeserializable { - returns: the deserialized Int value */ public static func deserialize(element: XMLElement) throws -> Int { - guard let value = Int(try element.nonEmptyTextOrThrow()) - else { throw XMLDeserializationError.TypeConversionFailed(type: "Int", element: element) } + guard let value = Int(try element.nonEmptyTextOrThrow()) else { + throw XMLDeserializationError.TypeConversionFailed(type: "Int", element: element) + } + return value + } + + /** + Attempts to deserialize XML attribute content to an Int + + - parameter attribute: The XMLAttribute to be deserialized + - throws: an XMLDeserializationError.AttributeDeserializationFailed if the attribute cannot be + deserialized + - returns: the deserialized Int value + */ + public static func deserialize(attribute: XMLAttribute) throws -> Int { + guard let value = Int(attribute.text) else { + throw XMLDeserializationError.AttributeDeserializationFailed( + type: "Int", attribute: attribute) + } return value } } -extension Double: XMLElementDeserializable { +extension Double: XMLElementDeserializable, XMLAttributeDeserializable { /** Attempts to deserialize XML element content to a Double @@ -323,15 +523,30 @@ extension Double: XMLElementDeserializable { - returns: the deserialized Double value */ public static func deserialize(element: XMLElement) throws -> Double { - guard let value = Double(try element.nonEmptyTextOrThrow()) - else { + guard let value = Double(try element.nonEmptyTextOrThrow()) else { throw XMLDeserializationError.TypeConversionFailed(type: "Double", element: element) } return value } + + /** + Attempts to deserialize XML attribute content to a Double + + - parameter attribute: The XMLAttribute to be deserialized + - throws: an XMLDeserializationError.AttributeDeserializationFailed if the attribute cannot be + deserialized + - returns: the deserialized Double value + */ + public static func deserialize(attribute: XMLAttribute) throws -> Double { + guard let value = Double(attribute.text) else { + throw XMLDeserializationError.AttributeDeserializationFailed( + type: "Double", attribute: attribute) + } + return value + } } -extension Float: XMLElementDeserializable { +extension Float: XMLElementDeserializable, XMLAttributeDeserializable { /** Attempts to deserialize XML element content to a Float @@ -341,19 +556,37 @@ extension Float: XMLElementDeserializable { - returns: the deserialized Float value */ public static func deserialize(element: XMLElement) throws -> Float { - guard let value = Float(try element.nonEmptyTextOrThrow()) - else { throw XMLDeserializationError.TypeConversionFailed(type: "Float", element: element) } + guard let value = Float(try element.nonEmptyTextOrThrow()) else { + throw XMLDeserializationError.TypeConversionFailed(type: "Float", element: element) + } + return value + } + + /** + Attempts to deserialize XML attribute content to a Float + + - parameter attribute: The XMLAttribute to be deserialized + - throws: an XMLDeserializationError.AttributeDeserializationFailed if the attribute cannot be + deserialized + - returns: the deserialized Float value + */ + public static func deserialize(attribute: XMLAttribute) throws -> Float { + guard let value = Float(attribute.text) else { + throw XMLDeserializationError.AttributeDeserializationFailed( + type: "Float", attribute: attribute) + } return value } } -extension Bool: XMLElementDeserializable { +extension Bool: XMLElementDeserializable, XMLAttributeDeserializable { + // swiftlint:disable line_length /** - Attempts to deserialize XML element content to a Bool. This uses NSString's 'boolValue' described - [here](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/#//apple_ref/occ/instp/NSString/boolValue) + Attempts to deserialize XML element content to a Bool. This uses NSString's 'boolValue' + described [here](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/#//apple_ref/occ/instp/NSString/boolValue) - parameters: - - element: the XMLElement to be deserialized + - element: the XMLElement to be deserialized - throws: an XMLDeserializationError.TypeConversionFailed if the element cannot be deserialized - returns: the deserialized Bool value */ @@ -361,4 +594,19 @@ extension Bool: XMLElementDeserializable { let value = Bool(NSString(string: try element.nonEmptyTextOrThrow()).boolValue) return value } + + /** + Attempts to deserialize XML attribute content to a Bool. This uses NSString's 'boolValue' + described [here](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/#//apple_ref/occ/instp/NSString/boolValue) + + - parameter attribute: The XMLAttribute to be deserialized + - throws: an XMLDeserializationError.AttributeDeserializationFailed if the attribute cannot be + deserialized + - returns: the deserialized Bool value + */ + public static func deserialize(attribute: XMLAttribute) throws -> Bool { + let value = Bool(NSString(string: attribute.text).boolValue) + return value + } + // swiftlint:enable line_length } diff --git a/Source/SWXMLHash.swift b/Source/SWXMLHash.swift index da75fc99..dd9e4f52 100644 --- a/Source/SWXMLHash.swift +++ b/Source/SWXMLHash.swift @@ -416,18 +416,15 @@ public enum XMLIndexer: SequenceType { let match = opStream.findElements() return try match.withAttr(attr, value) case .List(let list): - if let elem = list.filter({$0.attributes[attr] == value}).first { + if let elem = list.filter({$0.attribute(by: attr)?.text == value}).first { return .Element(elem) } throw Error.AttributeValue(attr: attr, value: value) case .Element(let elem): - if let attr = elem.attributes[attr] { - if attr == value { - return .Element(elem) - } - throw Error.AttributeValue(attr: attr, value: value) + if elem.attribute(by: attr)?.text == value { + return .Element(elem) } - fallthrough + throw Error.AttributeValue(attr: attr, value: value) default: throw Error.Attribute(attr: attr) } @@ -614,23 +611,46 @@ extension XMLIndexer.Error: CustomStringConvertible { } /// Models content for an XML doc, whether it is text or XML -public protocol XMLContent: CustomStringConvertible { -} +public protocol XMLContent: CustomStringConvertible { } /// Models a text element public class TextElement: XMLContent { + /// The underlying text value public let text: String init(text: String) { self.text = text } } +public struct XMLAttribute { + public let name: String + public let text: String + init(name: String, text: String) { + self.name = name + self.text = text + } +} + /// Models an XML element, including name, text and attributes public class XMLElement: XMLContent { /// The name of the element public let name: String + /// The attributes of the element - public var attributes = [String:String]() + @available(*, deprecated, message="See `allAttributes` instead, which introduces the XMLAttribute type over a simple String type") + public var attributes: [String:String] { + var attrMap = [String: String]() + for (name, attr) in allAttributes { + attrMap[name] = attr.text + } + return attrMap + } + + public var allAttributes = [String:XMLAttribute]() + + public func attribute(by name: String) -> XMLAttribute? { + return allAttributes[name] + } /// The inner text of the element, if it exists public var text: String? { @@ -676,8 +696,8 @@ public class XMLElement: XMLContent { for (keyAny, valueAny) in attributes { if let key = keyAny as? String, - let value = valueAny as? String { - element.attributes[key] = value + value = valueAny as? String { + element.allAttributes[key] = XMLAttribute(name: key, text: value) } } @@ -698,17 +718,17 @@ extension TextElement: CustomStringConvertible { } } +extension XMLAttribute: CustomStringConvertible { + /// The textual representation of an `XMLAttribute` instance. + public var description: String { + return "\(name)=\"\(text)\"" + } +} + extension XMLElement: CustomStringConvertible { /// The tag, attributes and content for a `XMLElement` instance (<elem id="foo">content</elem>) public var description: String { - var attributesStringList = [String]() - if !attributes.isEmpty { - for (key, val) in attributes { - attributesStringList.append("\(key)=\"\(val)\"") - } - } - - var attributesString = attributesStringList.joinWithSeparator(" ") + var attributesString = allAttributes.map { $0.1.description }.joinWithSeparator(" ") if !attributesString.isEmpty { attributesString = " " + attributesString } diff --git a/Tests/LazyTypesConversionTests.swift b/Tests/LazyTypesConversionTests.swift index c2012f89..1525fa07 100644 --- a/Tests/LazyTypesConversionTests.swift +++ b/Tests/LazyTypesConversionTests.swift @@ -40,6 +40,7 @@ class LazyTypesConversionTests: XCTestCase { " <name>the name of basic item</name>" + " <price>99.14</price>" + " </basicItem>" + + " <attribute int=\"1\"/>" + "</root>" override func setUp() { @@ -50,4 +51,9 @@ class LazyTypesConversionTests: XCTestCase { let value: String = try! parser!["root"]["string"].value() XCTAssertEqual(value, "the string value") } + + func testShouldConvertAttributeToNonOptional() { + let value: Int = try! parser!["root"]["attribute"].value(ofAttribute: "int") + XCTAssertEqual(value, 1) + } } diff --git a/Tests/LazyXMLParsingTests.swift b/Tests/LazyXMLParsingTests.swift index 8998c12a..6278d459 100644 --- a/Tests/LazyXMLParsingTests.swift +++ b/Tests/LazyXMLParsingTests.swift @@ -49,6 +49,7 @@ class LazyXMLParsingTests: XCTestCase { func testShouldBeAbleToParseAttributes() { XCTAssertEqual(xml!["root"]["catalog"]["book"][1].element?.attributes["id"], "bk102") + XCTAssertEqual(xml!["root"]["catalog"]["book"][1].element?.attribute(by: "id")?.text, "bk102") } func testShouldBeAbleToLookUpElementsByNameAndAttribute() { diff --git a/Tests/TypeConversionArrayOfNonPrimitiveTypesTests.swift b/Tests/TypeConversionArrayOfNonPrimitiveTypesTests.swift index 07877921..8fd3d482 100644 --- a/Tests/TypeConversionArrayOfNonPrimitiveTypesTests.swift +++ b/Tests/TypeConversionArrayOfNonPrimitiveTypesTests.swift @@ -44,7 +44,8 @@ class TypeConversionArrayOfNonPrimitiveTypesTests: XCTestCase { " <name>item 3</name>" + " <price>3</price>" + " </basicItem>" + - "</arrayOfBadBasicItems>" + + "</arrayOfGoodBasicItems>" + + "<arrayOfBadBasicItems>" + " <basicItem>" + " <name>item 1</name>" + " <price>1</price>" + @@ -57,13 +58,29 @@ class TypeConversionArrayOfNonPrimitiveTypesTests: XCTestCase { " <price>3</price>" + " </basicItem>" + "</arrayOfBadBasicItems>" + + "<arrayOfGoodAttributeItems>" + + " <attributeItem name=\"attr 1\" price=\"1.1\"/>" + + " <attributeItem name=\"attr 2\" price=\"2.2\"/>" + + " <attributeItem name=\"attr 3\" price=\"3.3\"/>" + + "</arrayOfGoodAttributeItems>" + + "<arrayOfBadAttributeItems>" + + " <attributeItem name=\"attr 1\" price=\"1.1\"/>" + + " <attributeItem price=\"2.2\"/>" + // it's missing the name attribute + " <attributeItem name=\"attr 3\" price=\"3.3\"/>" + + "</arrayOfBadAttributeItems>" + "</root>" let correctBasicItems = [ BasicItem(name: "item 1", price: 1), BasicItem(name: "item 2", price: 2), - BasicItem(name: "item 3", price: 3), - ] + BasicItem(name: "item 3", price: 3) + ] + + let correctAttributeItems = [ + AttributeItem(name: "attr 1", price: 1.1), + AttributeItem(name: "attr 2", price: 2.2), + AttributeItem(name: "attr 3", price: 3.3) + ] override func setUp() { parser = SWXMLHash.parse(xmlWithArraysOfTypes) @@ -110,4 +127,46 @@ class TypeConversionArrayOfNonPrimitiveTypesTests: XCTestCase { } } } + + func testShouldConvertArrayOfGoodAttributeItemsToNonOptional() { + let value: [AttributeItem] = try! parser!["root"]["arrayOfGoodAttributeItems"]["attributeItem"].value() + XCTAssertEqual(value, correctAttributeItems) + } + + func testShouldConvertArrayOfGoodAttributeItemsToOptional() { + let value: [AttributeItem]? = try! parser!["root"]["arrayOfGoodAttributeItems"]["attributeItem"].value() + XCTAssertEqual(value!, correctAttributeItems) + } + + func testShouldConvertArrayOfGoodAttributeItemsToArrayOfOptionals() { + let value: [AttributeItem?] = try! parser!["root"]["arrayOfGoodAttributeItems"]["attributeItem"].value() + XCTAssertEqual(value.flatMap({ $0 }), correctAttributeItems) + } + + func testShouldThrowWhenConvertingArrayOfBadAttributeItemsToNonOptional() { + XCTAssertThrowsError(try (parser!["root"]["arrayOfBadAttributeItems"]["attributeItem"].value() as [AttributeItem])) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testShouldThrowWhenConvertingArrayOfBadAttributeItemsToOptional() { + XCTAssertThrowsError(try (parser!["root"]["arrayOfBadAttributeItems"]["attributeItem"].value() as [AttributeItem]?)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testShouldThrowWhenConvertingArrayOfBadAttributeItemsToArrayOfOptionals() { + XCTAssertThrowsError(try (parser!["root"]["arrayOfBadAttributeItems"]["attributeItem"].value() as [AttributeItem?])) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } } diff --git a/Tests/TypeConversionBasicTypesTests.swift b/Tests/TypeConversionBasicTypesTests.swift index c8bd6392..226e19d7 100644 --- a/Tests/TypeConversionBasicTypesTests.swift +++ b/Tests/TypeConversionBasicTypesTests.swift @@ -41,6 +41,8 @@ class TypeConversionBasicTypesTests: XCTestCase { " <name>the name of basic item</name>" + " <price>99.14</price>" + " </basicItem>" + + " <attr string=\"stringValue\" int=\"200\" double=\"200.15\" float=\"205.42\" bool1=\"0\" bool2=\"true\"/>" + + " <attributeItem name=\"the name of attribute item\" price=\"19.99\"/>" + "</root>" override func setUp() { @@ -81,6 +83,30 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + func testShouldConvertAttributeToNonOptional() { + let value: String = try! parser!["root"]["attr"].value(ofAttribute: "string") + XCTAssertEqual(value, "stringValue") + } + + func testShouldConvertAttributeToOptional() { + let value: String? = parser!["root"]["attr"].value(ofAttribute: "string") + XCTAssertEqual(value, "stringValue") + } + + func testShouldThrowWhenConvertingMissingAttributeToNonOptional() { + XCTAssertThrowsError(try (parser!["root"]["attr"].value(ofAttribute: "missing") as String)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testShouldConvertMissingAttributeToOptional() { + let value: String? = parser!["root"]["attr"].value(ofAttribute: "missing") + XCTAssertNil(value) + } + func testIntShouldConvertValueToNonOptional() { let value: Int = try! parser!["root"]["int"].value() XCTAssertEqual(value, 100) @@ -123,6 +149,16 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + func testIntShouldConvertAttributeToNonOptional() { + let value: Int = try! parser!["root"]["attr"].value(ofAttribute: "int") + XCTAssertEqual(value, 200) + } + + func testIntShouldConvertAttributeToOptional() { + let value: Int? = parser!["root"]["attr"].value(ofAttribute: "int") + XCTAssertEqual(value, 200) + } + func testDoubleShouldConvertValueToNonOptional() { let value: Double = try! parser!["root"]["double"].value() XCTAssertEqual(value, 100.45) @@ -165,6 +201,16 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + func testDoubleShouldConvertAttributeToNonOptional() { + let value: Double = try! parser!["root"]["attr"].value(ofAttribute: "double") + XCTAssertEqual(value, 200.15) + } + + func testDoubleShouldConvertAttributeToOptional() { + let value: Double? = parser!["root"]["attr"].value(ofAttribute: "double") + XCTAssertEqual(value, 200.15) + } + func testFloatShouldConvertValueToNonOptional() { let value: Float = try! parser!["root"]["float"].value() XCTAssertEqual(value, 44.12) @@ -207,6 +253,16 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + func testFloatShouldConvertAttributeToNonOptional() { + let value: Float = try! parser!["root"]["attr"].value(ofAttribute: "float") + XCTAssertEqual(value, 205.42) + } + + func testFloatShouldConvertAttributeToOptional() { + let value: Float? = parser!["root"]["attr"].value(ofAttribute: "float") + XCTAssertEqual(value, 205.42) + } + func testBoolShouldConvertValueToNonOptional() { let value1: Bool = try! parser!["root"]["bool1"].value() let value2: Bool = try! parser!["root"]["bool2"].value() @@ -253,6 +309,16 @@ class TypeConversionBasicTypesTests: XCTestCase { XCTAssertNil(value) } + func testBoolShouldConvertAttributeToNonOptional() { + let value: Bool = try! parser!["root"]["attr"].value(ofAttribute: "bool1") + XCTAssertEqual(value, false) + } + + func testBoolShouldConvertAttributeToOptional() { + let value: Bool? = parser!["root"]["attr"].value(ofAttribute: "bool2") + XCTAssertEqual(value, true) + } + let correctBasicItem = BasicItem(name: "the name of basic item", price: 99.14) func testBasicItemShouldConvertBasicitemToNonOptional() { @@ -296,6 +362,50 @@ class TypeConversionBasicTypesTests: XCTestCase { let value: BasicItem? = try! parser!["root"]["missing"].value() XCTAssertNil(value) } + + let correctAttributeItem = AttributeItem(name: "the name of attribute item", price: 19.99) + + func testAttributeItemShouldConvertAttributeItemToNonOptional() { + let value: AttributeItem = try! parser!["root"]["attributeItem"].value() + XCTAssertEqual(value, correctAttributeItem) + } + + func testAttributeItemShouldThrowWhenConvertingEmptyToNonOptional() { + XCTAssertThrowsError(try (parser!["root"]["empty"].value() as AttributeItem)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testAttributeItemShouldThrowWhenConvertingMissingToNonOptional() { + XCTAssertThrowsError(try (parser!["root"]["missing"].value() as AttributeItem)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testAttributeItemShouldConvertAttributeItemToOptional() { + let value: AttributeItem? = try! parser!["root"]["attributeItem"].value() + XCTAssertEqual(value, correctAttributeItem) + } + + func testAttributeItemShouldConvertEmptyToOptional() { + XCTAssertThrowsError(try (parser!["root"]["empty"].value() as AttributeItem?)) { error in + guard error is XMLDeserializationError else { + XCTFail("Wrong type of error") + return + } + } + } + + func testAttributeItemShouldConvertMissingToOptional() { + let value: AttributeItem? = try! parser!["root"]["missing"].value() + XCTAssertNil(value) + } } struct BasicItem: XMLIndexerDeserializable { @@ -315,3 +425,21 @@ extension BasicItem: Equatable {} func == (a: BasicItem, b: BasicItem) -> Bool { return a.name == b.name && a.price == b.price } + +struct AttributeItem: XMLElementDeserializable { + let name: String + let price: Double + + static func deserialize(element: XMLElement) throws -> AttributeItem { + return try AttributeItem( + name: element.value(ofAttribute: "name"), + price: element.value(ofAttribute: "price") + ) + } +} + +extension AttributeItem: Equatable {} + +func == (a: AttributeItem, b: AttributeItem) -> Bool { + return a.name == b.name && a.price == b.price +} diff --git a/Tests/TypeConversionComplexTypesTests.swift b/Tests/TypeConversionComplexTypesTests.swift index a4fea34f..9a8c2f78 100644 --- a/Tests/TypeConversionComplexTypesTests.swift +++ b/Tests/TypeConversionComplexTypesTests.swift @@ -47,6 +47,11 @@ class TypeConversionComplexTypesTests: XCTestCase { " <price>3</price>" + " </basicItem>" + " </basicItems>" + + " <attributeItems>" + + " <attributeItem name=\"attr1\" price=\"1.1\"/>" + + " <attributeItem name=\"attr2\" price=\"2.2\"/>" + + " <attributeItem name=\"attr3\" price=\"3.3\"/>" + + " </attributeItems>" + " </complexItem>" + " <empty></empty>" + "</root>" @@ -58,6 +63,11 @@ class TypeConversionComplexTypesTests: XCTestCase { BasicItem(name: "item 1", price: 1), BasicItem(name: "item 2", price: 2), BasicItem(name: "item 3", price: 3), + ], + attrs: [ + AttributeItem(name: "attr1", price: 1.1), + AttributeItem(name: "attr2", price: 2.2), + AttributeItem(name: "attr3", price: 3.3), ] ) @@ -112,12 +122,14 @@ struct ComplexItem: XMLIndexerDeserializable { let name: String let priceOptional: Double? let basics: [BasicItem] + let attrs: [AttributeItem] static func deserialize(node: XMLIndexer) throws -> ComplexItem { return try ComplexItem( name: node["name"].value(), priceOptional: node["price"].value(), - basics: node["basicItems"]["basicItem"].value() + basics: node["basicItems"]["basicItem"].value(), + attrs: node["attributeItems"]["attributeItem"].value() ) } } @@ -125,5 +137,5 @@ struct ComplexItem: XMLIndexerDeserializable { extension ComplexItem: Equatable {} func == (a: ComplexItem, b: ComplexItem) -> Bool { - return a.name == b.name && a.priceOptional == b.priceOptional && a.basics == b.basics + return a.name == b.name && a.priceOptional == b.priceOptional && a.basics == b.basics && a.attrs == b.attrs } diff --git a/Tests/TypeConversionPrimitypeTypesTests.swift b/Tests/TypeConversionPrimitypeTypesTests.swift index f751d22b..566f38a8 100644 --- a/Tests/TypeConversionPrimitypeTypesTests.swift +++ b/Tests/TypeConversionPrimitypeTypesTests.swift @@ -39,6 +39,9 @@ class TypeConversionPrimitypeTypesTests: XCTestCase { "<arrayOfMixedInts>" + " <int>0</int> <int>boom</int> <int>2</int> <int>3</int>" + "</arrayOfMixedInts>" + + "<arrayOfAttributeInts>" + + " <int value=\"0\"/> <int value=\"1\"/> <int value=\"2\"/> <int value=\"3\"/>" + + "</arrayOfAttributeInts>" + "<empty></empty>" + "</root>" @@ -115,6 +118,21 @@ class TypeConversionPrimitypeTypesTests: XCTestCase { } } + func testShouldConvertArrayOfAttributeIntsToNonOptional() { + let value: [Int] = try! parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: "value") + XCTAssertEqual(value, [0, 1, 2, 3]) + } + + func testShouldConvertArrayOfAttributeIntsToOptional() { + let value: [Int]? = try! parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: "value") + XCTAssertEqual(value!, [0, 1, 2, 3]) + } + + func testShouldConvertArrayOfAttributeIntsToArrayOfOptionals() { + let value: [Int?] = try! parser!["root"]["arrayOfAttributeInts"]["int"].value(ofAttribute: "value") + XCTAssertEqual(value.flatMap({ $0 }), [0, 1, 2, 3]) + } + func testShouldConvertEmptyArrayOfIntsToNonOptional() { let value: [Int] = try! parser!["root"]["empty"]["int"].value() XCTAssertEqual(value, []) diff --git a/Tests/XMLParsingTests.swift b/Tests/XMLParsingTests.swift index 274dc38c..a7ce8ee2 100644 --- a/Tests/XMLParsingTests.swift +++ b/Tests/XMLParsingTests.swift @@ -49,6 +49,7 @@ class XMLParsingTests: XCTestCase { func testShouldBeAbleToParseAttributes() { XCTAssertEqual(xml!["root"]["catalog"]["book"][1].element?.attributes["id"], "bk102") + XCTAssertEqual(xml!["root"]["catalog"]["book"][1].element?.attribute(by: "id")?.text, "bk102") } func testShouldBeAbleToLookUpElementsByNameAndAttribute() {