Repository: jpsim/Yams
Pull request #105 (2018-02-17 05:32:40)

Use more effectively that `Node` is an enum
* Change parameter type of `ScalarConstructible.construct(from:)` from `Node` to `Node.Scalar`
* Change `Constructor`:
  * Split `Map` type into `ScalarMap`, `MappingMap` and `SequenceMap`
  * Split `defaultMap` static property into `defaultScalarMap`, `defaultMappingMap` and `defaultSequenceMap`
  * Change `init(_:)` to `init(_:_:_:)`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acf9f2e..be161dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,17 @@
 
 ##### Breaking
 
-* None.
+* Some APIs have changed related to `ScalarConstructible`
+  * Change parameter type of `ScalarConstructible.construct(from:)` from `Node`
+    to `Node.Scalar`  
+  * Change `Constructor`:
+    * Split `Map` into `ScalarMap`, `MappingMap` and `SequenceMap`
+    * Split `defaultMap` into `defaultScalarMap`, `defaultMappingMap` and 
+      `defaultSequenceMap`
+    * Change `init(_:)` to `init(_:_:_:)`
+
+  [Norio Nomura](https://github.com/norio-nomura)
+  [#105](https://github.com/jpsim/Yams/issues/105)
 
 ##### Enhancements
 
diff --git a/Sources/Yams/Constructor.swift b/Sources/Yams/Constructor.swift
index f2ea4a9..d9a0d4b 100644
--- a/Sources/Yams/Constructor.swift
+++ b/Sources/Yams/Constructor.swift
@@ -9,38 +9,50 @@
 import Foundation
 
 public final class Constructor {
-    public typealias Map = [Tag.Name: (Node) -> Any?]
-
-    public init(_ map: Map) {
-        methodMap = map
+    public typealias ScalarMap = [Tag.Name: (Node.Scalar) -> Any?]
+    public typealias MappingMap = [Tag.Name: (Node.Mapping) -> Any?]
+    public typealias SequenceMap = [Tag.Name: (Node.Sequence) -> Any?]
+
+    public init(_ scalarMap: ScalarMap = defaultScalarMap,
+                _ mappingMap: MappingMap = defaultMappingMap,
+                _ sequenceMap: SequenceMap = defaultSequenceMap) {
+        self.scalarMap = scalarMap
+        self.mappingMap = mappingMap
+        self.sequenceMap = sequenceMap
     }
 
     public func any(from node: Node) -> Any {
-        if let method = methodMap[node.tag.name], let result = method(node) {
-            return result
-        }
         switch node {
-        case .scalar:
-            return String.construct(from: node)!
-        case .mapping:
-            return [AnyHashable: Any]._construct_mapping(from: node)
-        case .sequence:
-            return [Any].construct_seq(from: node)
+        case .scalar(let scalar):
+            if let method = scalarMap[node.tag.name], let result = method(scalar) {
+                return result
+            }
+            return String.construct(from: scalar)!
+        case .mapping(let mapping):
+            if let method = mappingMap[node.tag.name], let result = method(mapping) {
+                return result
+            }
+            return [AnyHashable: Any]._construct_mapping(from: mapping)
+        case .sequence(let sequence):
+            if let method = sequenceMap[node.tag.name], let result = method(sequence) {
+                return result
+            }
+            return [Any].construct_seq(from: sequence)
         }
     }
 
-    private let methodMap: Map
+    private let scalarMap: ScalarMap
+    private let mappingMap: MappingMap
+    private let sequenceMap: SequenceMap
 }
 
 extension Constructor {
-    public static let `default` = Constructor(defaultMap)
+    public static let `default` = Constructor()
 
     // We can not write extension of map because that is alias of specialized dictionary
-    public static let defaultMap: Map = [
+    public static let defaultScalarMap: ScalarMap = [
         // Failsafe Schema
-        .map: [AnyHashable: Any].construct_mapping,
         .str: String.construct,
-        .seq: [Any].construct_seq,
         // JSON Schema
         .bool: Bool.construct,
         .float: Double.construct,
@@ -48,26 +60,35 @@ extension Constructor {
         .int: Int.construct,
         // http://yaml.org/type/index.html
         .binary: Data.construct,
-        // .merge is supported in `[AnyHashable: Any].construct`.
-        .omap: [Any].construct_omap,
-        .pairs: [Any].construct_pairs,
-        .set: Set<AnyHashable>.construct_set,
         .timestamp: Date.construct
-        // .value is supported in `String.construct` and `[AnyHashable: Any].construct`.
+    ]
+
+    public static let defaultMappingMap: MappingMap = [
+        .map: [AnyHashable: Any].construct_mapping,
+        // http://yaml.org/type/index.html
+        .set: Set<AnyHashable>.construct_set
+        // .merge is supported in `[AnyHashable: Any].construct_mapping`.
+        // .value is supported in `String.construct` and `[AnyHashable: Any].construct_mapping`.
+    ]
+
+    public static let defaultSequenceMap: SequenceMap = [
+        .seq: [Any].construct_seq,
+        // http://yaml.org/type/index.html
+        .omap: [Any].construct_omap,
+        .pairs: [Any].construct_pairs
     ]
 }
 
 // MARK: - ScalarConstructible
 public protocol ScalarConstructible {
-    // We don't use overloading `init?(_ node: Node)`
+    // We don't use overloading `init?(_ scalar: Node.Scalar)`
     // because that causes difficulties on using `init` as closure
-    static func construct(from node: Node) -> Self?
+    static func construct(from scalar: Node.Scalar) -> Self?
 }
 
 extension Bool: ScalarConstructible {
-    public static func construct(from node: Node) -> Bool? {
-        guard let string = node.scalar?.string else { return nil }
-        switch string.lowercased() {
+    public static func construct(from scalar: Node.Scalar) -> Bool? {
+        switch scalar.string.lowercased() {
         case "true", "yes", "on":
             return true
         case "false", "no", "off":
@@ -79,28 +100,25 @@ extension Bool: ScalarConstructible {
 }
 
 extension Data: ScalarConstructible {
-    public static func construct(from node: Node) -> Data? {
-        guard let string = node.scalar?.string else { return nil }
-        return Data(base64Encoded: string, options: .ignoreUnknownCharacters)
+    public static func construct(from scalar: Node.Scalar) -> Data? {
+        return Data(base64Encoded: scalar.string, options: .ignoreUnknownCharacters)
     }
 }
 
 extension Date: ScalarConstructible {
-    public static func construct(from node: Node) -> Date? {
-        guard let string = node.scalar?.string else { return nil }
-
-        let range = NSRange(location: 0, length: string.utf16.count)
-        guard let result = timestampPattern.firstMatch(in: string, options: [], range: range),
+    public static func construct(from scalar: Node.Scalar) -> Date? {
+        let range = NSRange(location: 0, length: scalar.string.utf16.count)
+        guard let result = timestampPattern.firstMatch(in: scalar.string, options: [], range: range),
             result.range.location != NSNotFound else {
                 return nil
         }
         #if os(Linux) || swift(>=4.0)
             let components = (1..<result.numberOfRanges).map {
-                string.substring(with: result.range(at: $0))
+                scalar.string.substring(with: result.range(at: $0))
             }
         #else
             let components = (1..<result.numberOfRanges).map {
-                string.substring(with: result.rangeAt($0))
+                scalar.string.substring(with: result.rangeAt($0))
             }
         #endif
 
@@ -158,9 +176,8 @@ extension Double: ScalarConstructible {}
 extension Float: ScalarConstructible {}
 
 extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible {
-    public static func construct(from node: Node) -> Self? {
-        guard var string = node.scalar?.string else { return nil }
-        switch string {
+    public static func construct(from scalar: Node.Scalar) -> Self? {
+        switch scalar.string {
         case ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF":
             return .infinity
         case "-.inf", "-.Inf", "-.INF":
@@ -168,7 +185,7 @@ extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible
         case ".nan", ".NaN", ".NAN":
             return .nan
         default:
-            string = string.replacingOccurrences(of: "_", with: "")
+            let string = scalar.string.replacingOccurrences(of: "_", with: "")
             if string.contains(":") {
                 return Self(sexagesimal: string)
             }
@@ -178,10 +195,8 @@ extension ScalarConstructible where Self: FloatingPoint & SexagesimalConvertible
 }
 
 extension FixedWidthInteger where Self: SexagesimalConvertible {
-    fileprivate static func _construct(from node: Node) -> Self? {
-        guard let string = node.scalar?.string else { return nil }
-
-        let scalarWithSign = string.replacingOccurrences(of: "_", with: "")
+    fileprivate static func _construct(from scalar: Node.Scalar) -> Self? {
+        let scalarWithSign = scalar.string.replacingOccurrences(of: "_", with: "")
 
         if scalarWithSign == "0" {
             return 0
@@ -213,18 +228,22 @@ extension FixedWidthInteger where Self: SexagesimalConvertible {
 }
 
 extension Int: ScalarConstructible {
-    public static func construct(from node: Node) -> Int? {
-        return _construct(from: node)
+    public static func construct(from scalar: Node.Scalar) -> Int? {
+        return _construct(from: scalar)
     }
 }
 
 extension UInt: ScalarConstructible {
-    public static func construct(from node: Node) -> UInt? {
-        return _construct(from: node)
+    public static func construct(from scalar: Node.Scalar) -> UInt? {
+        return _construct(from: scalar)
     }
 }
 
 extension String: ScalarConstructible {
+    public static func construct(from scalar: Node.Scalar) -> String? {
+        return scalar.string
+    }
+
     public static func construct(from node: Node) -> String? {
         // This will happen while `Dictionary.flatten_mapping()` if `node.tag.name` was `.value`
         if case let .mapping(mapping) = node {
@@ -240,9 +259,8 @@ extension String: ScalarConstructible {
 
 // MARK: - Types that can't conform to ScalarConstructible
 extension NSNull/*: ScalarConstructible*/ {
-    public static func construct(from node: Node) -> NSNull? {
-        guard let string = node.scalar?.string else { return nil }
-        switch string {
+    public static func construct(from scalar: Node.Scalar) -> NSNull? {
+        switch scalar.string {
         case "", "~", "null", "Null", "NULL":
             return NSNull()
         default:
@@ -253,24 +271,21 @@ extension NSNull/*: ScalarConstructible*/ {
 
 // MARK: mapping
 extension Dictionary {
-    public static func construct_mapping(from node: Node) -> [AnyHashable: Any]? {
-        return _construct_mapping(from: node)
+    public static func construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any]? {
+        return _construct_mapping(from: mapping)
     }
 
-    fileprivate static func _construct_mapping(from node: Node) -> [AnyHashable: Any] {
-        assert(node.isMapping) // swiftlint:disable:next force_unwrapping
-        let mapping = flatten_mapping(node).mapping!
+    fileprivate static func _construct_mapping(from mapping: Node.Mapping) -> [AnyHashable: Any] {
+        let mapping = flatten_mapping(mapping)
         var dictionary = [AnyHashable: Any](minimumCapacity: mapping.count)
         mapping.forEach {
             // TODO: YAML supports keys other than str.
-            dictionary[String.construct(from: $0.key)!] = node.tag.constructor.any(from: $0.value)
+            dictionary[String.construct(from: $0.key)!] = mapping.tag.constructor.any(from: $0.value)
         }
         return dictionary
     }
 
-    private static func flatten_mapping(_ node: Node) -> Node {
-        assert(node.isMapping) // swiftlint:disable:next force_unwrapping
-        let mapping = node.mapping!
+    private static func flatten_mapping(_ mapping: Node.Mapping) -> Node.Mapping {
         var pairs = Array(mapping)
         var merge = [(key: Node, value: Node)]()
         var index = pairs.startIndex
@@ -279,15 +294,11 @@ extension Dictionary {
             if pair.key.tag.name == .merge {
                 pairs.remove(at: index)
                 switch pair.value {
-                case .mapping:
-                    let flattened_node = flatten_mapping(pair.value)
-                    if let mapping = flattened_node.mapping {
-                        merge.append(contentsOf: mapping)
-                    }
+                case .mapping(let mapping):
+                    merge.append(contentsOf: flatten_mapping(mapping))
                 case let .sequence(sequence):
                     let submerge = sequence
-                        .filter { $0.isMapping } // TODO: Should raise error on other than mapping
-                        .compactMap { flatten_mapping($0).mapping }
+                        .compactMap { $0.mapping.map(flatten_mapping) }
                         .reversed()
                     submerge.forEach {
                         merge.append(contentsOf: $0)
@@ -302,15 +313,14 @@ extension Dictionary {
                 index += 1
             }
         }
-        return Node(merge + pairs, node.tag, mapping.style)
+        return Node.Mapping(merge + pairs, mapping.tag, mapping.style)
     }
 }
 
 extension Set {
-    public static func construct_set(from node: Node) -> Set<AnyHashable>? {
+    public static func construct_set(from mapping: Node.Mapping) -> Set<AnyHashable>? {
         // TODO: YAML supports Hashable elements other than str.
-        assert(node.isMapping) // swiftlint:disable:next force_unwrapping
-        return Set<AnyHashable>(node.mapping!.map({ String.construct(from: $0.key)! as AnyHashable }))
+        return Set<AnyHashable>(mapping.map({ String.construct(from: $0.key)! as AnyHashable }))
         // Explicitly declaring the generic parameter as `<AnyHashable>` above is required,
         // because this is inside extension of `Set` and Swift 3.0.2 can't infer the type without that.
     }
@@ -318,28 +328,25 @@ extension Set {
 
 // MARK: sequence
 extension Array {
-    public static func construct_seq(from node: Node) -> [Any] {
-        // swiftlint:disable:next force_unwrapping
-        return node.sequence!.map(node.tag.constructor.any)
+    public static func construct_seq(from sequence: Node.Sequence) -> [Any] {
+        return sequence.map(sequence.tag.constructor.any)
     }
 
-    public static func construct_omap(from node: Node) -> [(Any, Any)] {
+    public static func construct_omap(from sequence: Node.Sequence) -> [(Any, Any)] {
         // Note: we do not check for duplicate keys.
-        assert(node.isSequence) // swiftlint:disable:next force_unwrapping
-        return node.sequence!.compactMap { subnode -> (Any, Any)? in
+        return sequence.compactMap { subnode -> (Any, Any)? in
             // TODO: Should raise error if subnode is not mapping or mapping.count != 1
             guard let (key, value) = subnode.mapping?.first else { return nil }
-            return (node.tag.constructor.any(from: key), node.tag.constructor.any(from: value))
+            return (sequence.tag.constructor.any(from: key), sequence.tag.constructor.any(from: value))
         }
     }
 
-    public static func construct_pairs(from node: Node) -> [(Any, Any)] {
+    public static func construct_pairs(from sequence: Node.Sequence) -> [(Any, Any)] {
         // Note: we do not check for duplicate keys.
-        assert(node.isSequence) // swiftlint:disable:next force_unwrapping
-        return node.sequence!.compactMap { subnode -> (Any, Any)? in
+        return sequence.compactMap { subnode -> (Any, Any)? in
             // TODO: Should raise error if subnode is not mapping or mapping.count != 1
             guard let (key, value) = subnode.mapping?.first else { return nil }
-            return (node.tag.constructor.any(from: key), node.tag.constructor.any(from: value))
+            return (sequence.tag.constructor.any(from: key), sequence.tag.constructor.any(from: value))
         }
     }
 }
@@ -437,4 +444,19 @@ fileprivate extension Substring {
     }
 }
 
+// MARK: - Unavailable
+
+extension Constructor {
+    @available(*, unavailable, message: "Use `Constructor.ScalarMap` instead")
+    public typealias Map = [Tag.Name: (Node) -> Any?]
+    
+    @available(*, unavailable, message: "Use `Constructor.defaultScalarMap` instead")
+    public static let defaultMap: ScalarMap = [:]
+}
+
+extension ScalarConstructible {
+    @available(*, unavailable, message: "Use `construct(from scalar: Node.Scalar)` instead")
+    static func construct(from scalar: Node) -> Self? { return nil }
+}
+
 // swiftlint:disable:this file_length
diff --git a/Sources/Yams/Decoder.swift b/Sources/Yams/Decoder.swift
index c319002..58b0d37 100644
--- a/Sources/Yams/Decoder.swift
+++ b/Sources/Yams/Decoder.swift
@@ -63,8 +63,9 @@ struct _Decoder: Decoder { // swiftlint:disable:this type_name
 
     /// constuct `T` from `node`
     fileprivate func construct<T: ScalarConstructible>() throws -> T {
-        guard let constructed = T.construct(from: node) else {
-            throw _typeMismatch(at: codingPath, expectation: T.self, reality: node)
+        let scalar = try self.scalar()
+        guard let constructed = T.construct(from: scalar) else {
+            throw _typeMismatch(at: codingPath, expectation: T.self, reality: scalar)
         }
         return constructed
     }
@@ -73,6 +74,18 @@ struct _Decoder: Decoder { // swiftlint:disable:this type_name
     fileprivate func decoder(referencing node: Node, `as` key: CodingKey) -> _Decoder {
         return .init(referencing: node, userInfo: userInfo, codingPath: codingPath + [key])
     }
+
+    /// returns `Node.Scalar` or throws `DecodingError.typeMismatch`
+    fileprivate func scalar() throws -> Node.Scalar {
+        switch node {
+        case .scalar(let scalar):
+            return scalar
+        case .mapping(let mapping):
+            throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: mapping)
+        case .sequence(let sequence):
+            throw _typeMismatch(at: codingPath, expectation: Node.Scalar.self, reality: sequence)
+        }
+    }
 }
 
 struct _KeyedDecodingContainer<K: CodingKey> : KeyedDecodingContainerProtocol {
@@ -258,8 +271,9 @@ extension _Decoder: SingleValueDecodingContainer {
         guard let constructibleType = T.self as? ScalarConstructible.Type else {
             return nil
         }
-        guard let value = constructibleType.construct(from: node) else {
-            throw _valueNotFound(at: codingPath, T.self, "Expected \(T.self) value but found null instead.")
+        let scalar = try self.scalar()
+        guard let value = constructibleType.construct(from: scalar) else {
+            throw _valueNotFound(at: codingPath, T.self, "Expected \(T.self) value but found \(scalar) instead.")
         }
         return value as? T
     }
@@ -284,16 +298,14 @@ private func _typeMismatch(at codingPath: [CodingKey], expectation: Any.Type, re
 }
 
 extension FixedWidthInteger where Self: SignedInteger {
-    public static func construct(from node: Node) -> Self? {
-        guard let int = Int.construct(from: node) else { return nil }
-        return Self.init(exactly: int)
+    public static func construct(from scalar: Node.Scalar) -> Self? {
+        return Int.construct(from: scalar).flatMap(Self.init(exactly:))
     }
 }
 
 extension FixedWidthInteger where Self: UnsignedInteger {
-    public static func construct(from node: Node) -> Self? {
-        guard let int = UInt.construct(from: node) else { return nil }
-        return Self.init(exactly: int)
+    public static func construct(from scalar: Node.Scalar) -> Self? {
+        return UInt.construct(from: scalar).flatMap(Self.init(exactly:))
     }
 }
 
@@ -307,15 +319,13 @@ extension UInt64: ScalarConstructible {}
 extension UInt8: ScalarConstructible {}
 
 extension Decimal: ScalarConstructible {
-    public static func construct(from node: Node) -> Decimal? {
-        guard let string = node.scalar?.string else { return nil }
-        return Decimal(string: string)
+    public static func construct(from scalar: Node.Scalar) -> Decimal? {
+        return Decimal(string: scalar.string)
     }
 }
 
 extension URL: ScalarConstructible {
-    public static func construct(from node: Node) -> URL? {
-        guard let string = node.scalar?.string else { return nil }
-        return URL(string: string)
+    public static func construct(from scalar: Node.Scalar) -> URL? {
+        return URL(string: scalar.string)
     }
 }
diff --git a/Sources/Yams/Node.swift b/Sources/Yams/Node.swift
index 8fccf31..ce44e6f 100644
--- a/Sources/Yams/Node.swift
+++ b/Sources/Yams/Node.swift
@@ -56,27 +56,27 @@ extension Node {
     }
 
     public var bool: Bool? {
-        return Bool.construct(from: self)
+        return scalar.flatMap(Bool.construct)
     }
 
     public var float: Double? {
-        return Double.construct(from: self)
+        return scalar.flatMap(Double.construct)
     }
 
     public var null: NSNull? {
-        return NSNull.construct(from: self)
+        return scalar.flatMap(NSNull.construct)
     }
 
     public var int: Int? {
-        return Int.construct(from: self)
+        return scalar.flatMap(Int.construct)
     }
 
     public var binary: Data? {
-        return Data.construct(from: self)
+        return scalar.flatMap(Data.construct)
     }
 
     public var timestamp: Date? {
-        return Date.construct(from: self)
+        return scalar.flatMap(Date.construct)
     }
 
     // MARK: Typed accessor methods
@@ -91,7 +91,7 @@ extension Node {
     /// - Parameter type: Type conforms to ScalarConstructible
     /// - Returns: Array of `Type`
     public func array<Type: ScalarConstructible>(of type: Type.Type = Type.self) -> [Type] {
-        return sequence?.compactMap(Type.construct) ?? []
+        return sequence?.compactMap { $0.scalar.flatMap(Type.construct) } ?? []
     }
 
     public subscript(node: Node) -> Node? {


Test patch:
diff --git a/Tests/YamsTests/ConstructorTests.swift b/Tests/YamsTests/ConstructorTests.swift
index e2a518b..143155f 100644
--- a/Tests/YamsTests/ConstructorTests.swift
+++ b/Tests/YamsTests/ConstructorTests.swift
@@ -42,8 +42,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             "description": "The binary value above is a tiny arrow encoded as a gif image."
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(Data.construct(from: [Node("1"), Node("2")]))
     }
 
     func testBool() throws {
@@ -69,8 +67,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             ]
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(Bool.construct(from: [Node("1"), Node("2")]))
     }
 
     func testFloat() throws {
@@ -93,8 +89,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             "not a number": Double.nan
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(Float.construct(from: [Node("1"), Node("2")]))
     }
 
     func testInt() throws {
@@ -133,8 +127,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             "canonicalMax": 9223372036854775807
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(Int.construct(from: [Node("1"), Node("2")]))
     }
 
     func testMap() throws {
@@ -245,8 +237,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             ]
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(NSNull.construct(from: [Node("1"), Node("2")]))
     }
 
     func testOmap() throws {
@@ -372,8 +362,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
             "date (00:00:00Z)": timestamp( 0, 2002, 12, 14)
         ]
         YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(Date.construct(from: [Node("1"), Node("2")]))
     }
 
     func testTimestampWithNanosecond() throws {
@@ -427,32 +415,6 @@ class ConstructorTests: XCTestCase { // swiftlint:disable:this type_body_length
         XCTAssertEqual(nodes[1]["link with"]?[1]?["="], "library2.dll")
         XCTAssertEqual(nodes[1]["link with"]?[1]?["version"], "2.3")
     }
-
-    func testString() throws {
-        let example = """
-            one: two
-            three: four
-            """
-        let objects = try Yams.load(yaml: example)
-        let expected: [String: Any] = [
-            "one": "two",
-            "three": "four"
-        ]
-        YamsAssertEqual(objects, expected)
-
-        XCTAssertNil(String.construct(from: [Node("1"), Node("2")]))
-    }
-
-    func testDecimal() throws {
-        XCTAssertEqual(Decimal.construct(from: Node("1.2")), Decimal(string: "1.2")!)
-        XCTAssertNil(Decimal.construct(from: [Node("1"), Node("2")]))
-    }
-
-    func testURL() throws {
-        XCTAssertEqual(URL.construct(from: Node("http://www.google.com")), URL(string: "http://www.google.com")!)
-        XCTAssertEqual(URL.construct(from: Node("~/file.txt")), URL(string: "~/file.txt")!)
-        XCTAssertNil(URL.construct(from: [Node("1"), Node("2")]))
-    }
 }
 
 extension ConstructorTests {
@@ -471,10 +433,7 @@ extension ConstructorTests {
             ("testSeq", testSeq),
             ("testTimestamp", testTimestamp),
             ("testTimestampWithNanosecond", testTimestampWithNanosecond),
-            ("testValue", testValue),
-            ("testString", testString),
-            ("testDecimal", testDecimal),
-            ("testURL", testURL)
+            ("testValue", testValue)
         ]
     }
 } // swiftlint:disable:this file_length
diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift
index a13de01..0c8e28e 100644
--- a/Tests/YamsTests/EncoderTests.swift
+++ b/Tests/YamsTests/EncoderTests.swift
@@ -239,6 +239,59 @@ class EncoderTests: XCTestCase {
         XCTAssertEqual(decodedYaml, ["200": "ok"])
     }
 
+    func testNodeTypeMismatch() throws {
+        // https://github.com/jpsim/Yams/pull/95
+        struct Sample: Decodable { // swiftlint:disable:this nesting
+            let values: [String]
+        }
+
+        let validYaml = """
+            values:
+            - hello
+            """
+        XCTAssertNoThrow(try YAMLDecoder().decode(Sample.self, from: validYaml))
+
+        let invalidYamls = [
+        // expecting scalar,
+            // but mapping instead
+            """
+            values:
+            - hello:
+            """,
+            // but sequence instead
+            """
+            values:
+            - [hello1, hello2]
+            """,
+        // expecting mapping,
+            // but scalar instead
+            """
+            hello
+            """,
+            // but sequence instead
+            """
+            - hello
+            """,
+        // expecting sequence,
+            // but scalar instead
+            """
+            values: hello
+            """,
+            // but mapping instead
+            """
+            values:
+              hello:
+            """
+        ]
+        for invalidYaml in invalidYamls {
+            XCTAssertThrowsError(try YAMLDecoder().decode(Sample.self, from: invalidYaml)) { error in
+                if case DecodingError.typeMismatch = error {} else {
+                    XCTFail("unexpected error: \(error)")
+                }
+            }
+        }
+    }
+
     // MARK: - Helper Functions
     private func _testEncodeFailure<T: Encodable>(of value: T) {
         do {
@@ -1007,7 +1060,8 @@ extension EncoderTests {
             ("testValuesInSingleValueContainer", testValuesInSingleValueContainer),
             ("testValuesInKeyedContainer", testValuesInKeyedContainer),
             ("testValuesInUnkeyedContainer", testValuesInUnkeyedContainer),
-            ("testDictionary", testDictionary)
+            ("testDictionary", testDictionary),
+            ("testNodeTypeMismatch", testNodeTypeMismatch)
         ]
     }
 }