From 32a3612ae51a36ed4e597fa4ac04bfdbb4c1e55d Mon Sep 17 00:00:00 2001 From: "tattn (Tatsuya Tanaka)" Date: Sun, 9 Sep 2018 17:27:37 +0900 Subject: [PATCH] Add CodableAny --- MoreCodable.xcodeproj/project.pbxproj | 8 +++ Sources/CodableAny.swift | 98 +++++++++++++++++++++++++++ Tests/CodableAnyTests.swift | 71 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 Sources/CodableAny.swift create mode 100644 Tests/CodableAnyTests.swift diff --git a/MoreCodable.xcodeproj/project.pbxproj b/MoreCodable.xcodeproj/project.pbxproj index 01e6e4c..8a8f0a0 100644 --- a/MoreCodable.xcodeproj/project.pbxproj +++ b/MoreCodable.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 24A4FF6720304D8F001618E1 /* DictionaryDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A4FF6520304D8C001618E1 /* DictionaryDecoderTests.swift */; }; 24CF7FE42144DC8D007A5C6C /* CodableDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CF7FE32144DC8D007A5C6C /* CodableDictionary.swift */; }; 24CF7FE62144DCA4007A5C6C /* CodableDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CF7FE52144DCA4007A5C6C /* CodableDictionaryTests.swift */; }; + 24CF7FE82144E76D007A5C6C /* CodableAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CF7FE72144E76D007A5C6C /* CodableAny.swift */; }; + 24CF7FEA2144E7DC007A5C6C /* CodableAnyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CF7FE92144E7DC007A5C6C /* CodableAnyTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -71,6 +73,8 @@ 24A4FF6520304D8C001618E1 /* DictionaryDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryDecoderTests.swift; sourceTree = ""; }; 24CF7FE32144DC8D007A5C6C /* CodableDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableDictionary.swift; sourceTree = ""; }; 24CF7FE52144DCA4007A5C6C /* CodableDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableDictionaryTests.swift; sourceTree = ""; }; + 24CF7FE72144E76D007A5C6C /* CodableAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableAny.swift; sourceTree = ""; }; + 24CF7FE92144E7DC007A5C6C /* CodableAnyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableAnyTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -128,6 +132,7 @@ 249140702039E27F00D3E4CD /* StringTo.swift */, 2491407E203C85A500D3E4CD /* RuleBasedCodingKey.swift */, 24CF7FE32144DC8D007A5C6C /* CodableDictionary.swift */, + 24CF7FE72144E76D007A5C6C /* CodableAny.swift */, ); path = Sources; sourceTree = ""; @@ -144,6 +149,7 @@ 24914080203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift */, 24028DD1204859A400721297 /* ObjectMergerTests.swift */, 24CF7FE52144DCA4007A5C6C /* CodableDictionaryTests.swift */, + 24CF7FE92144E7DC007A5C6C /* CodableAnyTests.swift */, 24A4FF4020302322001618E1 /* Products */, 24A4FF5820302490001618E1 /* Info.plist */, ); @@ -266,6 +272,7 @@ 2491406D2039DF1D00D3E4CD /* Failable.swift in Sources */, 249140712039E27F00D3E4CD /* StringTo.swift in Sources */, 242C3E2D2030DDDE00AAA577 /* URLQueryItem+.swift in Sources */, + 24CF7FE82144E76D007A5C6C /* CodableAny.swift in Sources */, 24A4FF4D20302407001618E1 /* AnyCodingKey.swift in Sources */, 24028DD0204856B400721297 /* ObjectMerger.swift in Sources */, 242C3E2B2030D83600AAA577 /* URLQueryItemsDecoder.swift in Sources */, @@ -282,6 +289,7 @@ buildActionMask = 2147483647; files = ( 2491406F2039DF7B00D3E4CD /* FailableTests.swift in Sources */, + 24CF7FEA2144E7DC007A5C6C /* CodableAnyTests.swift in Sources */, 24CF7FE62144DCA4007A5C6C /* CodableDictionaryTests.swift in Sources */, 24914081203C89D000D3E4CD /* RuleBasedCodingKeyTests.swift in Sources */, 24A4FF6720304D8F001618E1 /* DictionaryDecoderTests.swift in Sources */, diff --git a/Sources/CodableAny.swift b/Sources/CodableAny.swift new file mode 100644 index 0000000..61138dc --- /dev/null +++ b/Sources/CodableAny.swift @@ -0,0 +1,98 @@ +// +// CodableAny.swift +// MoreCodable +// +// Created by Tatsuya Tanaka on 20180909. +// Copyright © 2018年 tattn. All rights reserved. +// + +import Foundation + +public struct CodableAny { + public let value: Any + + public init(_ value: Any) { + self.value = value + } +} + +extension CodableAny: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self.init(()) + } else if let bool = try? container.decode(Bool.self) { + self.init(bool) + } else if let int = try? container.decode(Int.self) { + self.init(int) + } else if let uint = try? container.decode(UInt.self) { + self.init(uint) + } else if let double = try? container.decode(Double.self) { + self.init(double) + } else if let string = try? container.decode(String.self) { + self.init(string) + } else if let array = try? container.decode([CodableAny].self) { + self.init(array.map { $0.value }) + } else if let dictionary = try? container.decode([String: CodableAny].self) { + self.init(dictionary.mapValues { $0.value }) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "This type is not supported." + ) + } + } +} + +extension CodableAny: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch value { + case is Void, Optional.none: + try container.encodeNil() + case let bool as Bool: + try container.encode(bool) + case let int as Int: + try container.encode(int) + case let int8 as Int8: + try container.encode(int8) + case let int16 as Int16: + try container.encode(int16) + case let int32 as Int32: + try container.encode(int32) + case let int64 as Int64: + try container.encode(int64) + case let uint as UInt: + try container.encode(uint) + case let uint8 as UInt8: + try container.encode(uint8) + case let uint16 as UInt16: + try container.encode(uint16) + case let uint32 as UInt32: + try container.encode(uint32) + case let uint64 as UInt64: + try container.encode(uint64) + case let float as Float: + try container.encode(float) + case let double as Double: + try container.encode(double) + case let string as String: + try container.encode(string) + case let date as Date: + try container.encode(date) + case let url as URL: + try container.encode(url) + case let array as [Any]: + try container.encode(array.map(CodableAny.init)) + case let dictionary as [String: Any]: + try container.encode(dictionary.mapValues(CodableAny.init)) + default: + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "\(type(of: value)) type is not supported." + )) + } + } +} diff --git a/Tests/CodableAnyTests.swift b/Tests/CodableAnyTests.swift new file mode 100644 index 0000000..c017362 --- /dev/null +++ b/Tests/CodableAnyTests.swift @@ -0,0 +1,71 @@ +// +// CodableAnyTests.swift +// MoreCodableTests +// +// Created by Tatsuya Tanaka on 20180909. +// Copyright © 2018年 tattn. All rights reserved. +// + +import XCTest +import MoreCodable + +class CodableAnyTests: XCTestCase { + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + override func setUp() { + super.setUp() + } + + func testInt() { + assertEncodingAndDecoding(["value": 1]) + } + + func testStringIntDictionary() { + assertEncodingAndDecoding(["value": ["key": 1]]) + } + + func testDoubleArray() { + assertEncodingAndDecoding([1.1, 2.2, 3.3]) + + let decodedValue = encodeAndDecode([1, 2, 3] as [Double]) + XCTAssertEqual(decodedValue as! [Int], [1, 2, 3]) // double to int + } + + func testNestedArray() { + assertEncodingAndDecoding([[[["one", "two", "three"]]]]) + } + + func testOptional() { + let decodedValue = encodeAndDecode([nil, 1, nil]) + let values = decodedValue as! [Any] + XCTAssertNotNil(values[0]) + XCTAssertTrue(values[0] is Void) + XCTAssertEqual(values[1] as! Int, 1) + XCTAssertNotNil(values[2]) + XCTAssertTrue(values[2] is Void) + } + + func testDate() { + let date = Date() + let decodedValue = encodeAndDecode(["key": date]) + XCTAssertEqual(decodedValue as! [String: Double], ["key": date.timeIntervalSinceReferenceDate]) // date to double + } + + func testURL() { + let url = URL(string: "https://example.com")! + let decodedValue = encodeAndDecode(["key": url]) + XCTAssertEqual(decodedValue as! [String: String], ["key": url.absoluteString]) // url to string + } + + private func encodeAndDecode(_ value: Any) -> Any { + let _any = CodableAny(value) + let data = try! jsonEncoder.encode(_any) + return (try! jsonDecoder.decode(CodableAny.self, from: data)).value + } + + private func assertEncodingAndDecoding(_ expectedValue: T) { + let decodedValue = encodeAndDecode(expectedValue) + XCTAssertEqual(decodedValue as! T, expectedValue) + } +}