diff --git a/Sources/Yams/Decoder.swift b/Sources/Yams/Decoder.swift new file mode 100644 index 0000000..5ec886f --- /dev/null +++ b/Sources/Yams/Decoder.swift @@ -0,0 +1,454 @@ +// +// Decoder.swift +// Yams +// +// Created by Norio Nomura on 5/6/17. +// Copyright (c) 2017 Yams. All rights reserved. +// + +#if swift(>=4.0) + + import Foundation + + public class YAMLDecoder { + public init() {} + public func decode(_ type: T.Type, from data: Data) throws -> T { + // TODO: Detect string encoding + let yaml = String(data: data, encoding: .utf8)! // swiftlint:disable:this force_unwrapping + let node = try Yams.compose(yaml: yaml) ?? "" + let decoder = _YAMLDecoder(referencing: node) + return try T(from: decoder) + } + } + + fileprivate class _YAMLDecoder: Decoder { + + let node: Node + + init(referencing node: Node, codingPath: [CodingKey?] = []) { + self.node = node + self.codingPath = codingPath + } + + // MARK: - Swift.Decoder Methods + + /// The path to the current point in encoding. + var codingPath: [CodingKey?] + + /// Contextual user-provided information for use during encoding. + var userInfo: [CodingUserInfoKey : Any] = [:] + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + guard let mapping = node.mapping else { + // FIXME: Should throw type mismatch error + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead." + ) + ) + } + let wrapper = _YAMLKeyedDecodingContainer(decoder: self, wrapping: mapping) + return KeyedDecodingContainer(wrapper) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard let sequence = node.sequence else { + // FIXME: Should throw type mismatch error + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null value instead." + ) + ) + } + return _YAMLUnkeyedDecodingContainer(decoder: self, wrapping: sequence) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + guard node.isScalar else { + // FIXME: Should throw type mismatch error + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null value instead." + ) + ) + } + return self + } + + // MARK: Utility + + /// Performs the given closure with the given key pushed onto the end of the current coding path. + /// + /// - parameter key: The key to push. May be nil for unkeyed containers. + /// - parameter work: The work to perform with the key in the path. + func with(pushedKey key: CodingKey?, _ work: () throws -> T) rethrows -> T { + self.codingPath.append(key) + let ret: T = try work() + self.codingPath.removeLast() + return ret + } + } + + fileprivate struct _YAMLKeyedDecodingContainer : KeyedDecodingContainerProtocol { + + typealias Key = K + + let decoder: _YAMLDecoder + let mapping: Node.Mapping + + init(decoder: _YAMLDecoder, wrapping mapping: Node.Mapping) { + self.decoder = decoder + self.mapping = mapping + } + + // MARK: - KeyedDecodingContainerProtocol + + var codingPath: [CodingKey?] { + return decoder.codingPath + } + + var allKeys: [Key] { + return mapping.keys.flatMap { $0.string.flatMap(Key.init(stringValue:)) } + } + + func contains(_ key: K) -> Bool { + if mapping[key.stringValue] != nil { + return true + } + return false + } + + func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? { return try construct(for: key) } + func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? { return try construct(for: key) } + func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? { return try construct(for: key) } + func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? { return try construct(for: key) } + func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? { return try construct(for: key) } + func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? { return try construct(for: key) } + func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? { return try construct(for: key) } + func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? { return try construct(for: key) } + func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? { return try construct(for: key) } + func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? { return try construct(for: key) } + func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? { return try construct(for: key) } + func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? { return try construct(for: key) } + func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? { return try construct(for: key) } + func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? { return try construct(for: key) } + + func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable { + return try decoder.with(pushedKey: key) { + guard let node = mapping[key.stringValue] else { return nil } + if T.self == Data.self { + return Data.construct(from: node) as? T + } else if T.self == Date.self { + return Date.construct(from: node) as? T + } + + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + return try T(from: decoder) + } + } + + func nestedContainer(keyedBy type: NestedKey.Type, + forKey key: Key) throws -> KeyedDecodingContainer { + return try decoder.with(pushedKey: key) { + guard let node = mapping[key.stringValue] else { + // FIXME: Should throw type mismatch error + throw DecodingError.keyNotFound( + key, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get \(KeyedDecodingContainer.self) -- no value found for key \"\(key.stringValue)\"" + ) + ) + } + guard let mapping = node.mapping else { + fatalError("should throw type mismatch error") + } + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + let wrapping = _YAMLKeyedDecodingContainer(decoder: decoder, wrapping: mapping) + return KeyedDecodingContainer(wrapping) + } + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + return try decoder.with(pushedKey: key) { + guard let node = mapping[key.stringValue], let sequence = node.sequence else { + // FIXME: Should throw type mismatch error + throw DecodingError.keyNotFound( + key, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get UnkeyedDecodingContainer -- no value found for key \"\(key.stringValue)\"" + ) + ) + } + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + return _YAMLUnkeyedDecodingContainer(decoder: decoder, wrapping: sequence) + } + } + + private func _superDecoder(forKey key: CodingKey) throws -> Decoder { + return try self.decoder.with(pushedKey: key) { + guard let node = mapping[key.stringValue] else { + throw DecodingError.keyNotFound( + key, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get superDecoder() -- no value found for key \"\(key.stringValue)\"" + ) + ) + } + + return _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + } + } + + func superDecoder() throws -> Decoder { + return try _superDecoder(forKey: _YAMLDecodingSuperKey()) + } + + func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } + + // MARK: Utility + + /// Encode ScalarConstructible + func construct(for key: Key) throws -> T? { + return decoder.with(pushedKey: key) { + guard let node = mapping[key.stringValue] else { return nil } + return T.construct(from: node) + } + } + } + + fileprivate struct _YAMLDecodingSuperKey: CodingKey { + init() {} + + var stringValue: String { return "super" } + init?(stringValue: String) { + guard stringValue == "super" else { return nil } + } + + var intValue: Int? { return nil } + init?(intValue: Int) { + return nil + } + } + + fileprivate struct _YAMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { + + let decoder: _YAMLDecoder + let sequence: Node.Sequence + + /// The index of the element we're about to decode. + var currentIndex: Int + + init(decoder: _YAMLDecoder, wrapping sequence: Node.Sequence) { + self.decoder = decoder + self.sequence = sequence + self.currentIndex = 0 + } + + // MARK: - UnkeyedDecodingContainer + var codingPath: [CodingKey?] { + return decoder.codingPath + } + + var count: Int? { + return sequence.count + } + + var isAtEnd: Bool { + return self.currentIndex >= sequence.count + } + + mutating func decodeIfPresent(_ type: Bool.Type) throws -> Bool? { return try construct() } + mutating func decodeIfPresent(_ type: Int.Type) throws -> Int? { return try construct() } + mutating func decodeIfPresent(_ type: Int8.Type) throws -> Int8? { return try construct() } + mutating func decodeIfPresent(_ type: Int16.Type) throws -> Int16? { return try construct() } + mutating func decodeIfPresent(_ type: Int32.Type) throws -> Int32? { return try construct() } + mutating func decodeIfPresent(_ type: Int64.Type) throws -> Int64? { return try construct() } + mutating func decodeIfPresent(_ type: UInt.Type) throws -> UInt? { return try construct() } + mutating func decodeIfPresent(_ type: UInt8.Type) throws -> UInt8? { return try construct() } + mutating func decodeIfPresent(_ type: UInt16.Type) throws -> UInt16? { return try construct() } + mutating func decodeIfPresent(_ type: UInt32.Type) throws -> UInt32? { return try construct() } + mutating func decodeIfPresent(_ type: UInt64.Type) throws -> UInt64? { return try construct() } + mutating func decodeIfPresent(_ type: Float.Type) throws -> Float? { return try construct() } + mutating func decodeIfPresent(_ type: Double.Type) throws -> Double? { return try construct() } + mutating func decodeIfPresent(_ type: String.Type) throws -> String? { return try construct() } + + mutating func decodeIfPresent(_ type: T.Type) throws -> T? where T : Decodable { + guard !self.isAtEnd else { return nil } + + let decoded: T? = try decoder.with(pushedKey: nil) { + let node = sequence[currentIndex] + if T.self == Data.self { + return Data.construct(from: node) as? T + } else if T.self == Date.self { + return Date.construct(from: node) as? T + } + + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + return try T(from: decoder) + } + currentIndex += 1 + return decoded + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { + return try decoder.with(pushedKey: nil) { + guard !self.isAtEnd else { + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." + ) + ) + } + let node = sequence[currentIndex] + guard let mapping = node.mapping else { + // FIXME: Should throw type mismatch error + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get \(KeyedDecodingContainer.self) -- no value found at index \"\(currentIndex)\"" + ) + ) + } + + currentIndex += 1 + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + let wrapping = _YAMLKeyedDecodingContainer(decoder: decoder, wrapping: mapping) + return KeyedDecodingContainer(wrapping) + } + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + return try decoder.with(pushedKey: nil) { + guard !self.isAtEnd else { + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get UnkeyedDecodingContainer -- unkeyed container is at end." + ) + ) + } + let node = sequence[currentIndex] + guard let sequence = node.sequence else { + // FIXME: Should throw type mismatch error + throw DecodingError.typeMismatch( + type(of: node), + DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get UnkeyedDecodingContainer -- no value found at index \"\(currentIndex)\"" + ) + ) + } + let decoder = _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + return _YAMLUnkeyedDecodingContainer(decoder: decoder, wrapping: sequence) + } + } + + mutating func superDecoder() throws -> Decoder { + return try decoder.with(pushedKey: nil) { + guard !self.isAtEnd else { + throw DecodingError.valueNotFound( + Decoder.self, + DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get superDecoder() -- unkeyed container is at end." + ) + ) + } + + let node = sequence[currentIndex] + self.currentIndex += 1 + return _YAMLDecoder(referencing: node, codingPath: self.decoder.codingPath) + } + } + + // MARK: Utility + + /// Encode ScalarConstructible + mutating func construct() throws -> T? { + guard !self.isAtEnd else { return nil } + + return decoder.with(pushedKey: nil) { + let node = sequence[currentIndex] + currentIndex += 1 + return T.construct(from: node) + } + } + } + + extension _YAMLDecoder : SingleValueDecodingContainer { + + // MARK: SingleValueDecodingContainer Methods + + func decode(_ type: Bool.Type) throws -> Bool { return try construct() } + func decode(_ type: Int.Type) throws -> Int { return try construct() } + func decode(_ type: Int8.Type) throws -> Int8 { return try construct() } + func decode(_ type: Int16.Type) throws -> Int16 { return try construct() } + func decode(_ type: Int32.Type) throws -> Int32 { return try construct() } + func decode(_ type: Int64.Type) throws -> Int64 { return try construct() } + func decode(_ type: UInt.Type) throws -> UInt { return try construct() } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try construct() } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try construct() } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try construct() } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try construct() } + func decode(_ type: Float.Type) throws -> Float { return try construct() } + func decode(_ type: Double.Type) throws -> Double { return try construct() } + func decode(_ type: String.Type) throws -> String { return try construct() } + func decode(_ type: Data.Type) throws -> Data { return try construct() } + + // MARK: Utility + + /// Encode ScalarConstructible + func construct() throws -> T { + return try with(pushedKey: nil) { + guard let decoded = T.construct(from: node) else { + // FIXME: Should throw type mismatch error + throw DecodingError.typeMismatch( + T.self, + DecodingError.Context( + codingPath: codingPath, debugDescription: "" + ) + ) + } + return decoded + } + } + } + + extension BinaryInteger { + public static func construct(from node: Node) -> Self? { + return Int.construct(from: node) as? Self + } + } + + extension Int16: ScalarConstructible {} + extension Int32: ScalarConstructible {} + extension Int64: ScalarConstructible {} + extension Int8: ScalarConstructible {} + extension UInt: ScalarConstructible {} + extension UInt16: ScalarConstructible {} + extension UInt32: ScalarConstructible {} + extension UInt64: ScalarConstructible {} + extension UInt8: ScalarConstructible {} + + extension Float: ScalarConstructible { + public static func construct(from node: Node) -> Float? { + return Double.construct(from: node) as? Float + } + } + +#endif // swiftlint:disable:this file_length diff --git a/Sources/Yams/Encoder.swift b/Sources/Yams/Encoder.swift new file mode 100644 index 0000000..a3b078d --- /dev/null +++ b/Sources/Yams/Encoder.swift @@ -0,0 +1,324 @@ +// +// Encoder.swift +// Yams +// +// Created by Norio Nomura on 5/2/17. +// Copyright (c) 2017 Yams. All rights reserved. +// + +#if swift(>=4.0) + + import Foundation + + public class YAMLEncoder { + public init() {} + public func encode(_ value: T) throws -> Data { + let encoder = _YAMLEncoder() + try value.encode(to: encoder) + return try serialize(node: encoder.node).data(using: .utf8, allowLossyConversion: false) ?? Data() + } + } + + fileprivate class _YAMLEncoder: Swift.Encoder { + + var node: Node = "" + + init(codingPath: [CodingKey?] = []) { + self.codingPath = codingPath + } + + // MARK: - Swift.Encoder Methods + + /// The path to the current point in encoding. + var codingPath: [CodingKey?] + + /// Contextual user-provided information for use during encoding. + var userInfo: [CodingUserInfoKey : Any] = [:] + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + assertCanRequestNewContainer() + node = [:] + let wrapper = _KeyedEncodingContainer(referencing: self) + return KeyedEncodingContainer(wrapper) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + assertCanRequestNewContainer() + node = [] + return _UnkeyedEncodingContainer(referencing: self) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + assertCanRequestNewContainer() + return self + } + + // MARK: Utility + + /// Performs the given closure with the given key pushed onto the end of the current coding path. + /// + /// - parameter key: The key to push. May be nil for unkeyed containers. + /// - parameter work: The work to perform with the key in the path. + func with(pushedKey key: CodingKey?, _ work: () throws -> Void) rethrows { + self.codingPath.append(key) + try work() + self.codingPath.removeLast() + } + + /// Asserts that a new container can be requested at this coding path. + /// `preconditionFailure()`s if one cannot be requested. + func assertCanRequestNewContainer() { + guard node == "" else { + let previousContainerType: String + switch node { + case .mapping: + previousContainerType = "keyed" + case .sequence: + previousContainerType = "unkeyed" + case .scalar: + previousContainerType = "single value" + } + preconditionFailure( + "Attempt to encode with new container when already encoded with \(previousContainerType) container." + ) + } + } + } + + fileprivate class _YAMLReferencingEncoder: _YAMLEncoder { + enum Reference { + case sequence(Int) + case mapping(String) + } + let encoder: _YAMLEncoder + let reference: Reference + + init(referencing encoder: _YAMLEncoder, at index: Int) { + self.encoder = encoder + reference = .sequence(index) + super.init(codingPath: encoder.codingPath) + } + + init(referencing encoder: _YAMLEncoder, key: String) { + self.encoder = encoder + reference = .mapping(key) + super.init(codingPath: encoder.codingPath) + } + + deinit { + switch reference { + case .sequence(let index): + encoder.node[index] = node + case .mapping(let key): + encoder.node[key] = node + } + } + } + + fileprivate struct _KeyedEncodingContainer : KeyedEncodingContainerProtocol { + typealias Key = K + + let encoder: _YAMLEncoder + + init(referencing encoder: _YAMLEncoder) { + self.encoder = encoder + } + + // MARK: - KeyedEncodingContainerProtocol + + var codingPath: [CodingKey?] { + return encoder.codingPath + } + + // assumes following methods never throws + func encode(_ value: Bool?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Int?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Int8?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Int16?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Int32?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Int64?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: UInt?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: UInt8?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: UInt16?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: UInt32?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: UInt64?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Float?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: Double?, forKey key: Key) throws { try represent(value, for: key) } + func encode(_ value: String?, forKey key: Key) throws { try represent(value, for: key) } + + func encode(_ value: T?, forKey key: Key) throws where T : Encodable { + try encoder.with(pushedKey: key) { + if let value = value { + if let data = value as? Data { + encoder.node.sequence?.append(try data.represented()) + } else if let date = value as? Date { + encoder.node.sequence?.append(try date.represented()) + } else { + try value.encode(to: referencingEncoder(for: key.stringValue)) + } + } else { + encoder.node.sequence?.append(Node("null", Tag(.null))) + } + } + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, + forKey key: Key) -> KeyedEncodingContainer { + let wrapper = _KeyedEncodingContainer(referencing: referencingEncoder(for: key.stringValue)) + return KeyedEncodingContainer(wrapper) + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + return _UnkeyedEncodingContainer(referencing: referencingEncoder(for: key.stringValue)) + } + + func superEncoder() -> Encoder { + return referencingEncoder(for: "super") + } + + func superEncoder(forKey key: Key) -> Encoder { + return referencingEncoder(for: key.stringValue) + } + + // MARK: Utility + + /// Encode NodeRepresentable + private func represent(_ value: T, for key: Key) throws { + // assumes this function is used for types that never throws. + encoder.node.mapping?[key.stringValue] = try Node(value) + } + + private func referencingEncoder(for key: String) -> _YAMLReferencingEncoder { + return _YAMLReferencingEncoder(referencing: self.encoder, key: key) + } + } + + fileprivate struct _UnkeyedEncodingContainer: UnkeyedEncodingContainer { + + let encoder: _YAMLEncoder + + init(referencing encoder: _YAMLEncoder) { + self.encoder = encoder + } + + // MARK: - UnkeyedEncodingContainer + + var codingPath: [CodingKey?] { + return encoder.codingPath + } + + func encode(_ value: Bool?) throws { try represent(value) } + func encode(_ value: Int?) throws { try represent(value) } + func encode(_ value: Int8?) throws { try represent(value) } + func encode(_ value: Int16?) throws { try represent(value) } + func encode(_ value: Int32?) throws { try represent(value) } + func encode(_ value: Int64?) throws { try represent(value) } + func encode(_ value: UInt?) throws { try represent(value) } + func encode(_ value: UInt8?) throws { try represent(value) } + func encode(_ value: UInt16?) throws { try represent(value) } + func encode(_ value: UInt32?) throws { try represent(value) } + func encode(_ value: UInt64?) throws { try represent(value) } + func encode(_ value: Float?) throws { try represent(value) } + func encode(_ value: Double?) throws { try represent(value) } + func encode(_ value: String?) throws { try represent(value) } + + func encode(_ value: T?) throws where T : Encodable { + // Since generic types may throw, the coding path needs to contain this key. + try encoder.with(pushedKey: nil) { + if let value = value { + if let data = value as? Data { + encoder.node.sequence?.append(try data.represented()) + } else if let date = value as? Date { + encoder.node.sequence?.append(try date.represented()) + } else { + try value.encode(to: referencingEncoder()) + } + } else { + encoder.node.sequence?.append(Node("null", Tag(.null))) + } + } + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + let wrapper = _KeyedEncodingContainer(referencing: referencingEncoder()) + return KeyedEncodingContainer(wrapper) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + return _UnkeyedEncodingContainer(referencing: referencingEncoder()) + } + + func superEncoder() -> Encoder { + return referencingEncoder() + } + + // MARK: Utility + + /// Encode NodeRepresentable + private func represent(_ value: T) throws { + // assumes this function is used for types that never throws. + encoder.node.sequence?.append(try Node(value)) + } + + private func referencingEncoder() -> _YAMLReferencingEncoder { + let index: Int = encoder.node.sequence?.count ?? 0 + encoder.node.sequence?.append("") + return _YAMLReferencingEncoder(referencing: self.encoder, at: index) + } + } + + extension _YAMLEncoder: SingleValueEncodingContainer { + + // MARK: - SingleValueEncodingContainer Methods + + func encode(_ value: Bool) throws { try represent(value) } + func encode(_ value: Int) throws { try represent(value) } + func encode(_ value: Int8) throws { try represent(value) } + func encode(_ value: Int16) throws { try represent(value) } + func encode(_ value: Int32) throws { try represent(value) } + func encode(_ value: Int64) throws { try represent(value) } + func encode(_ value: UInt) throws { try represent(value) } + func encode(_ value: UInt8) throws { try represent(value) } + func encode(_ value: UInt16) throws { try represent(value) } + func encode(_ value: UInt32) throws { try represent(value) } + func encode(_ value: UInt64) throws { try represent(value) } + func encode(_ value: Float) throws { try represent(value) } + func encode(_ value: Double) throws { try represent(value) } + + func encode(_ value: String) throws { + assertCanEncodeSingleValue() + node = Node(value) + } + + // MARK: Utility + + /// Asserts that a single value can be encoded at the current coding path + /// (i.e. that one has not already been encoded through this container). + /// `preconditionFailure()`s if one cannot be encoded. + /// + /// This is similar to assertCanRequestNewContainer above. + func assertCanEncodeSingleValue() { + guard node == "" else { + let previousContainerType: String + switch node { + case .mapping: + previousContainerType = "keyed" + case .sequence: + previousContainerType = "unkeyed" + case .scalar: + preconditionFailure("Attempt to encode multiple values in a single value container.") + } + preconditionFailure( + "Attempt to encode with new container when already encoded with \(previousContainerType) container." + ) + } + } + + /// Encode NodeRepresentable + func represent(_ value: T) throws { + assertCanEncodeSingleValue() + node = try Node(value) + } + } + +#endif diff --git a/Tests/YamsTests/EncoderTests.swift b/Tests/YamsTests/EncoderTests.swift new file mode 100644 index 0000000..fdb0ea9 --- /dev/null +++ b/Tests/YamsTests/EncoderTests.swift @@ -0,0 +1,289 @@ +// +// EncoderTests.swift +// Yams +// +// Created by Norio Nomura on 5/2/17. +// Copyright (c) 2017 Yams. All rights reserved. +// + +import XCTest +import Yams + +#if swift(>=4.0) + + /// Tests are copied from https://github.com/apple/swift/blob/master/test/stdlib/TestJSONEncoder.swift + class EncoderTests: XCTestCase { + // MARK: - Encoding Top-Level Empty Types + func testEncodingTopLevelEmptyStruct() { + let empty = EmptyStruct() + _testRoundTrip(of: empty, expectedYAML: "{}\n") + } + + func testEncodingTopLevelEmptyClass() { + let empty = EmptyClass() + _testRoundTrip(of: empty, expectedYAML: "{}\n") + } + + // MARK: - Encoding Top-Level Single-Value Types + func testEncodingTopLevelSingleValueEnum() { + _testRoundTrip(of: Switch.off, expectedYAML: "false\n...\n") + _testRoundTrip(of: Switch.on, expectedYAML: "true\n...\n") + } + + func testEncodingTopLevelSingleValueStruct() { + _testRoundTrip(of: Timestamp(3141592653), expectedYAML: "3.141592653e+9\n...\n") + } + + func testEncodingTopLevelSingleValueClass() { + _testRoundTrip(of: Counter()) + } + + // MARK: - Encoding Top-Level Structured Types + func testEncodingTopLevelStructuredStruct() { + // Address is a struct type with multiple fields. + let address = Address.testValue + _testRoundTrip(of: address) + } + + func testEncodingTopLevelStructuredClass() { + // Person is a class with multiple fields. + let person = Person.testValue + _testRoundTrip(of: person) + } + + func testEncodingTopLevelDeepStructuredType() { + // Company is a type with fields which are Codable themselves. + let company = Company.testValue + _testRoundTrip(of: company) + } + + // MARK: - Date Strategy Tests + func testEncodingDate() { + // We can't encode a top-level Date, so it'll be wrapped in an array. + _testRoundTrip(of: TopLevelWrapper(Date())) + } + + func testEncodingDateMillisecondsSince1970() { + // Cannot encode an arbitrary number of seconds since we've lost precision since 1970. + let seconds = 1000.0 + let expectedYAML = "- 1970-01-01T00:16:40Z\n" + + // We can't encode a top-level Date, so it'll be wrapped in an array. + _testRoundTrip(of: TopLevelWrapper(Date(timeIntervalSince1970: seconds)), + expectedYAML: expectedYAML) + } + + // MARK: - Data Tests + func testEncodingBase64Data() { + let data = Data(bytes: [0xDE, 0xAD, 0xBE, 0xEF]) + + // We can't encode a top-level Data, so it'll be wrapped in an array. + let expectedYAML = "- 3q2+7w==\n" + _testRoundTrip(of: TopLevelWrapper(data), expectedYAML: expectedYAML) + } + + // MARK: - Helper Functions + private func _testRoundTrip(of value: T, + expectedYAML yamlString: String? = nil) where T : Codable, T : Equatable { + let yaml = yamlString?.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + var payload: Data! = nil + do { + let encoder = YAMLEncoder() + payload = try encoder.encode(value) + } catch { + XCTFail("Failed to encode \(T.self) to YAML.") + } + + if let expectedYAML = yaml { + XCTAssertEqual(expectedYAML, payload, "Produced YAML not identical to expected YAML.") + } + + do { + let decoder = YAMLDecoder() + let decoded = try decoder.decode(T.self, from: payload) + XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.") + } catch { + XCTFail("Failed to decode \(T.self) from YAML by error: \(error)") + } + } + } + + // MARK: - Empty Types + fileprivate struct EmptyStruct: Codable, Equatable { + static func == (_ lhs: EmptyStruct, _ rhs: EmptyStruct) -> Bool { + return true + } + } + + fileprivate class EmptyClass: Codable, Equatable { + static func == (_ lhs: EmptyClass, _ rhs: EmptyClass) -> Bool { + return true + } + } + + // MARK: - Single-Value Types + /// A simple on-off switch type that encodes as a single Bool value. + fileprivate enum Switch: Codable { + case off + case on // swiftlint:disable:this identifier_name + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + switch try container.decode(Bool.self) { + case false: self = .off + case true: self = .on + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .off: try container.encode(false) + case .on: try container.encode(true) + } + } + } + + /// A simple timestamp type that encodes as a single Double value. + fileprivate struct Timestamp: Codable, Equatable { + let value: Double + + init(_ value: Double) { + self.value = value + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try container.decode(Double.self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.value) + } + + static func == (lhs: Timestamp, rhs: Timestamp) -> Bool { + return lhs.value == rhs.value + } + } + + /// A simple referential counter type that encodes as a single Int value. + fileprivate final class Counter: Codable, Equatable { + var count: Int = 0 + + init() {} + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + count = try container.decode(Int.self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.count) + } + + static func == (lhs: Counter, rhs: Counter) -> Bool { + return lhs.count == rhs.count + } + } + + // MARK: - Structured Types + /// A simple address type that encodes as a dictionary of values. + fileprivate struct Address: Codable, Equatable { + let street: String + let city: String + let state: String + let zipCode: Int + let country: String + + init(street: String, city: String, state: String, zipCode: Int, country: String) { + self.street = street + self.city = city + self.state = state + self.zipCode = zipCode + self.country = country + } + + static func == (_ lhs: Address, _ rhs: Address) -> Bool { + return lhs.street == rhs.street && + lhs.city == rhs.city && + lhs.state == rhs.state && + lhs.zipCode == rhs.zipCode && + lhs.country == rhs.country + } + + static var testValue: Address { + return Address(street: "1 Infinite Loop", + city: "Cupertino", + state: "CA", + zipCode: 95014, + country: "United States") + } + } + + /// A simple person class that encodes as a dictionary of values. + fileprivate class Person: Codable, Equatable { + let name: String + let email: String + + init(name: String, email: String) { + self.name = name + self.email = email + } + + static func == (_ lhs: Person, _ rhs: Person) -> Bool { + return lhs.name == rhs.name && lhs.email == rhs.email + } + + static var testValue: Person { + return Person(name: "Johnny Appleseed", email: "appleseed@apple.com") + } + } + + /// A simple company struct which encodes as a dictionary of nested values. + fileprivate struct Company: Codable, Equatable { + let address: Address + var employees: [Person] + + init(address: Address, employees: [Person]) { + self.address = address + self.employees = employees + } + + static func == (_ lhs: Company, _ rhs: Company) -> Bool { + return lhs.address == rhs.address && lhs.employees == rhs.employees + } + + static var testValue: Company { + return Company(address: Address.testValue, employees: [Person.testValue]) + } + } + + // MARK: - Helper Types + + /// Wraps a type T so that it can be encoded at the top level of a payload. + fileprivate struct TopLevelWrapper : Codable, Equatable where T : Codable, T : Equatable { + let value: T + + init(_ value: T) { + self.value = value + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(value) + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + value = try container.decode(T.self) + assert(container.isAtEnd) + } + + static func == (_ lhs: TopLevelWrapper, _ rhs: TopLevelWrapper) -> Bool { + return lhs.value == rhs.value + } + } + +#endif diff --git a/Yams.xcodeproj/project.pbxproj b/Yams.xcodeproj/project.pbxproj index cd8d3db..bdce002 100644 --- a/Yams.xcodeproj/project.pbxproj +++ b/Yams.xcodeproj/project.pbxproj @@ -17,12 +17,15 @@ 6C3C90B91E0FFB6B009DEFE8 /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3C90B81E0FFB6B009DEFE8 /* NodeTests.swift */; }; 6C4A22071DF8553C002A0206 /* String+Yams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4A22061DF8553C002A0206 /* String+Yams.swift */; }; 6C4A22091DF855BB002A0206 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4A22081DF855BB002A0206 /* StringTests.swift */; }; + 6C4AF3201EBE1705008775BC /* Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4AF31E1EBE14A1008775BC /* Decoder.swift */; }; 6C6834C81E0281880047B4D1 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834C71E0281880047B4D1 /* Node.swift */; }; 6C6834CC1E0283980047B4D1 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834CB1E0283980047B4D1 /* Parser.swift */; }; 6C6834CD1E02847D0047B4D1 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834C91E0281D90047B4D1 /* Tag.swift */; }; 6C6834D11E0297390047B4D1 /* SpecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834D01E0297390047B4D1 /* SpecTests.swift */; }; 6C6834D31E02B9760047B4D1 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834D21E02B9760047B4D1 /* Resolver.swift */; }; 6C6834D51E02BC1F0047B4D1 /* ResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6834D41E02BC1F0047B4D1 /* ResolverTests.swift */; }; + 6C788A011EB87232005386F0 /* EncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C788A001EB87232005386F0 /* EncoderTests.swift */; }; + 6C788A041EB89162005386F0 /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C788A021EB876C4005386F0 /* Encoder.swift */; }; 6C78C5651E29B27D0096215F /* RepresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C78C5631E29B1CE0096215F /* RepresenterTests.swift */; }; 6CBAEE1A1E3839500021BF87 /* Yams.h in Headers */ = {isa = PBXBuildFile; fileRef = 6CBAEE191E3839500021BF87 /* Yams.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6CC2E33F1E22347B00F62269 /* Representer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC2E33E1E22347B00F62269 /* Representer.swift */; }; @@ -66,12 +69,15 @@ 6C4A22061DF8553C002A0206 /* String+Yams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Yams.swift"; sourceTree = ""; }; 6C4A22081DF855BB002A0206 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = ""; }; 6C4A220A1DF85793002A0206 /* LinuxMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; }; + 6C4AF31E1EBE14A1008775BC /* Decoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decoder.swift; sourceTree = ""; }; 6C6834C71E0281880047B4D1 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; 6C6834C91E0281D90047B4D1 /* Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 6C6834CB1E0283980047B4D1 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; 6C6834D01E0297390047B4D1 /* SpecTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecTests.swift; sourceTree = ""; }; 6C6834D21E02B9760047B4D1 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; 6C6834D41E02BC1F0047B4D1 /* ResolverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResolverTests.swift; sourceTree = ""; }; + 6C788A001EB87232005386F0 /* EncoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncoderTests.swift; sourceTree = ""; }; + 6C788A021EB876C4005386F0 /* Encoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = ""; }; 6C78C5631E29B1CE0096215F /* RepresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepresenterTests.swift; sourceTree = ""; }; 6CBAEE191E3839500021BF87 /* Yams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Yams.h; sourceTree = ""; }; 6CC2E33E1E22347B00F62269 /* Representer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Representer.swift; sourceTree = ""; }; @@ -127,7 +133,9 @@ isa = PBXGroup; children = ( 6C0D2A351E0A934B00C45545 /* Constructor.swift */, + 6C4AF31E1EBE14A1008775BC /* Decoder.swift */, 6CE603971E13502E00A13D8D /* Emitter.swift */, + 6C788A021EB876C4005386F0 /* Encoder.swift */, 6CF025371E9CF4380061FB47 /* Mark.swift */, 6C6834C71E0281880047B4D1 /* Node.swift */, 6C0409AB1E607E9900C95D83 /* Node.Scalar.swift */, @@ -160,6 +168,7 @@ 6CF6CE071E0E3A5900CB87D4 /* Fixtures */, 6C0488ED1E0CBD56006F9F80 /* ConstructorTests.swift */, 6C0A00D41E152D6200222704 /* EmitterTests.swift */, + 6C788A001EB87232005386F0 /* EncoderTests.swift */, 6CF025391E9D12680061FB47 /* MarkTests.swift */, 6C3C90B81E0FFB6B009DEFE8 /* NodeTests.swift */, 6CF6CE081E0E3B1000CB87D4 /* PerformanceTests.swift */, @@ -347,12 +356,14 @@ E8EDB8881DE2181B0062268D /* loader.c in Sources */, E8EDB88C1DE2181B0062268D /* writer.c in Sources */, E8EDB88B1DE2181B0062268D /* scanner.c in Sources */, + 6C4AF3201EBE1705008775BC /* Decoder.swift in Sources */, 6C0409AC1E607E9900C95D83 /* Node.Scalar.swift in Sources */, 6C4A22071DF8553C002A0206 /* String+Yams.swift in Sources */, E8EDB8891DE2181B0062268D /* parser.c in Sources */, 6CF025381E9CF4380061FB47 /* Mark.swift in Sources */, OBJ_50 /* YamlError.swift in Sources */, E8EDB88A1DE2181B0062268D /* reader.c in Sources */, + 6C788A041EB89162005386F0 /* Encoder.swift in Sources */, 6C6834CD1E02847D0047B4D1 /* Tag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -361,6 +372,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 6C788A011EB87232005386F0 /* EncoderTests.swift in Sources */, 6CF6CE091E0E3B1000CB87D4 /* PerformanceTests.swift in Sources */, 6C4A22091DF855BB002A0206 /* StringTests.swift in Sources */, 6C0488EC1E0BE113006F9F80 /* TestHelper.swift in Sources */,