mirror of
https://github.com/caoer/CodableFirebase.git
synced 2026-04-21 18:32:14 +08:00
Add codable tests
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
CEFDBF8A1FF3E24200745EBE /* FirebaseDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF891FF3E24200745EBE /* FirebaseDecoder.swift */; };
|
||||
CEFDBF8C1FF3E3CB00745EBE /* FirebaseEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF8B1FF3E3CB00745EBE /* FirebaseEncoder.swift */; };
|
||||
CEFDBF8E1FF3E63700745EBE /* CodaleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF8D1FF3E63700745EBE /* CodaleTests.swift */; };
|
||||
CEFDBF901FF3EB2900745EBE /* TestCodableFirebase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF8F1FF3EB2900745EBE /* TestCodableFirebase.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -43,6 +44,7 @@
|
||||
CEFDBF891FF3E24200745EBE /* FirebaseDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseDecoder.swift; sourceTree = "<group>"; };
|
||||
CEFDBF8B1FF3E3CB00745EBE /* FirebaseEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseEncoder.swift; sourceTree = "<group>"; };
|
||||
CEFDBF8D1FF3E63700745EBE /* CodaleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodaleTests.swift; sourceTree = "<group>"; };
|
||||
CEFDBF8F1FF3EB2900745EBE /* TestCodableFirebase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCodableFirebase.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -102,6 +104,7 @@
|
||||
children = (
|
||||
CEFDBF8D1FF3E63700745EBE /* CodaleTests.swift */,
|
||||
CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */,
|
||||
CEFDBF8F1FF3EB2900745EBE /* TestCodableFirebase.swift */,
|
||||
CE7DD3771F9CFA81000225C5 /* Info.plist */,
|
||||
);
|
||||
path = CodableFirebaseTests;
|
||||
@@ -233,6 +236,7 @@
|
||||
files = (
|
||||
CE7DD3861F9DE4F7000225C5 /* TestCodableFirestore.swift in Sources */,
|
||||
CEFDBF8E1FF3E63700745EBE /* CodaleTests.swift in Sources */,
|
||||
CEFDBF901FF3EB2900745EBE /* TestCodableFirebase.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
901
CodableFirebaseTests/TestCodableFirebase.swift
Normal file
901
CodableFirebaseTests/TestCodableFirebase.swift
Normal file
@@ -0,0 +1,901 @@
|
||||
//
|
||||
// TestCodableFirebase.swift
|
||||
// CodableFirebaseTests
|
||||
//
|
||||
// Created by Oleksii on 27/12/2017.
|
||||
// Copyright © 2017 ViolentOctopus. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import CodableFirebase
|
||||
|
||||
class TestCodableFirebase: XCTestCase {
|
||||
// MARK: - Encoding Top-Level Empty Types
|
||||
func testEncodingTopLevelEmptyStruct() {
|
||||
_testRoundTrip(of: EmptyStruct(), expectedValue: _emptyDictionary)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelEmptyClass() {
|
||||
_testRoundTrip(of: EmptyClass(), expectedValue: _emptyDictionary)
|
||||
}
|
||||
|
||||
// MARK: - Encoding Top-Level Single-Value Types
|
||||
func testEncodingTopLevelSingleValueEnum() {
|
||||
_testRoundTrip(of: Switch.off)
|
||||
_testRoundTrip(of: Switch.on)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(Switch.off))
|
||||
_testRoundTrip(of: TopLevelWrapper(Switch.on))
|
||||
}
|
||||
|
||||
func testEncodingTopLevelSingleValueStruct() {
|
||||
_testRoundTrip(of: Timestamp(3141592653))
|
||||
_testRoundTrip(of: TopLevelWrapper(Timestamp(3141592653)))
|
||||
}
|
||||
|
||||
func testEncodingTopLevelSingleValueClass() {
|
||||
_testRoundTrip(of: Counter())
|
||||
_testRoundTrip(of: TopLevelWrapper(Counter()))
|
||||
}
|
||||
|
||||
// MARK: - Encoding Top-Level Structured Types
|
||||
func testEncodingTopLevelStructuredStruct() {
|
||||
_testRoundTrip(of: Address.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelStructuredClass() {
|
||||
_testRoundTrip(of: Person.testValue, expectedValue: ["name": "Johnny Appleseed","email":"appleseed@apple.com"])
|
||||
}
|
||||
|
||||
func testEncodingTopLevelStructuredSingleStruct() {
|
||||
_testRoundTrip(of: Numbers.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelStructuredSingleClass() {
|
||||
_testRoundTrip(of: Mapping.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelDeepStructuredType() {
|
||||
_testRoundTrip(of: Company.testValue)
|
||||
}
|
||||
|
||||
func testEncodingClassWhichSharesEncoderWithSuper() {
|
||||
_testRoundTrip(of: Employee.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelNullableType() {
|
||||
// EnhancedBool is a type which encodes either as a Bool or as nil.
|
||||
_testRoundTrip(of: EnhancedBool.true)
|
||||
_testRoundTrip(of: EnhancedBool.false)
|
||||
_testRoundTrip(of: EnhancedBool.fileNotFound)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.true), expectedValue: ["value": true])
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.false), expectedValue: ["value": false])
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound), expectedValue: ["value": NSNull()])
|
||||
}
|
||||
|
||||
// MARK: - Date Strategy Tests
|
||||
func testEncodingDate() {
|
||||
_testRoundTrip(of: Date())
|
||||
_testRoundTrip(of: TopLevelWrapper(Date()))
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(Date()))
|
||||
}
|
||||
|
||||
func testEncodingDateSecondsSince1970() {
|
||||
// Cannot encode an arbitrary number of seconds since we've lost precision since 1970.
|
||||
let seconds = 1000.0
|
||||
let expected = ["value":1000]
|
||||
|
||||
_testRoundTrip(of: Date(timeIntervalSince1970: seconds),
|
||||
expectedValue: 1000,
|
||||
dateEncodingStrategy: .secondsSince1970,
|
||||
dateDecodingStrategy: .secondsSince1970)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(Date(timeIntervalSince1970: seconds)),
|
||||
expectedValue: expected,
|
||||
dateEncodingStrategy: .secondsSince1970,
|
||||
dateDecodingStrategy: .secondsSince1970)
|
||||
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(Date(timeIntervalSince1970: seconds)),
|
||||
expectedValue: expected,
|
||||
dateEncodingStrategy: .secondsSince1970,
|
||||
dateDecodingStrategy: .secondsSince1970)
|
||||
}
|
||||
|
||||
func testEncodingDateMillisecondsSince1970() {
|
||||
// Cannot encode an arbitrary number of seconds since we've lost precision since 1970.
|
||||
let seconds = 1000.0
|
||||
let expectedValue = ["value": 1000000]
|
||||
|
||||
_testRoundTrip(of: Date(timeIntervalSince1970: seconds),
|
||||
expectedValue: 1000000,
|
||||
dateEncodingStrategy: .millisecondsSince1970,
|
||||
dateDecodingStrategy: .millisecondsSince1970)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(Date(timeIntervalSince1970: seconds)),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .millisecondsSince1970,
|
||||
dateDecodingStrategy: .millisecondsSince1970)
|
||||
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(Date(timeIntervalSince1970: seconds)),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .millisecondsSince1970,
|
||||
dateDecodingStrategy: .millisecondsSince1970)
|
||||
}
|
||||
|
||||
func testEncodingDateISO8601() {
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = .withInternetDateTime
|
||||
|
||||
let timestamp = Date(timeIntervalSince1970: 1000)
|
||||
let expectedValue = ["value": formatter.string(from: timestamp)]
|
||||
|
||||
_testRoundTrip(of: timestamp,
|
||||
expectedValue: formatter.string(from: timestamp),
|
||||
dateEncodingStrategy: .iso8601,
|
||||
dateDecodingStrategy: .iso8601)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .iso8601,
|
||||
dateDecodingStrategy: .iso8601)
|
||||
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .iso8601,
|
||||
dateDecodingStrategy: .iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
func testEncodingDateFormatted() {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .full
|
||||
formatter.timeStyle = .full
|
||||
|
||||
let timestamp = Date(timeIntervalSince1970: 1000)
|
||||
let expectedValue = ["value": formatter.string(from: timestamp)]
|
||||
|
||||
_testRoundTrip(of: timestamp,
|
||||
expectedValue: formatter.string(from: timestamp),
|
||||
dateEncodingStrategy: .formatted(formatter),
|
||||
dateDecodingStrategy: .formatted(formatter))
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .formatted(formatter),
|
||||
dateDecodingStrategy: .formatted(formatter))
|
||||
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .formatted(formatter),
|
||||
dateDecodingStrategy: .formatted(formatter))
|
||||
}
|
||||
|
||||
func testEncodingDateCustom() {
|
||||
let timestamp = Date()
|
||||
|
||||
// We'll encode a number instead of a date.
|
||||
let encode = { (_ data: Date, _ encoder: Encoder) throws -> Void in
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(42)
|
||||
}
|
||||
let decode = { (_: Decoder) throws -> Date in return timestamp }
|
||||
let expectedValue = ["value": 42]
|
||||
|
||||
_testRoundTrip(of: timestamp,
|
||||
expectedValue: 42,
|
||||
dateEncodingStrategy: .custom(encode),
|
||||
dateDecodingStrategy: .custom(decode))
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .custom(encode),
|
||||
dateDecodingStrategy: .custom(decode))
|
||||
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .custom(encode),
|
||||
dateDecodingStrategy: .custom(decode))
|
||||
}
|
||||
|
||||
func testEncodingDateCustomEmpty() {
|
||||
let timestamp = Date()
|
||||
|
||||
// Encoding nothing should encode an empty keyed container ({}).
|
||||
let encode = { (_: Date, _: Encoder) throws -> Void in }
|
||||
let decode = { (_: Decoder) throws -> Date in return timestamp }
|
||||
let expectedValue = ["value": [:]]
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .custom(encode),
|
||||
dateDecodingStrategy: .custom(decode))
|
||||
|
||||
// Optional dates should encode the same way.
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(timestamp),
|
||||
expectedValue: expectedValue,
|
||||
dateEncodingStrategy: .custom(encode),
|
||||
dateDecodingStrategy: .custom(decode))
|
||||
}
|
||||
|
||||
// MARK: - Data Strategy Tests
|
||||
func testEncodingData() {
|
||||
let data = Data(bytes: [0xDE, 0xAD, 0xBE, 0xEF])
|
||||
let expectedValue = ["value":[222,173,190,239]]
|
||||
|
||||
_testRoundTrip(of: data,
|
||||
expectedValue: [222,173,190,239],
|
||||
dataEncodingStrategy: .deferredToData,
|
||||
dataDecodingStrategy: .deferredToData)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(data),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .deferredToData,
|
||||
dataDecodingStrategy: .deferredToData)
|
||||
|
||||
// Optional data should encode the same way.
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(data),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .deferredToData,
|
||||
dataDecodingStrategy: .deferredToData)
|
||||
}
|
||||
|
||||
func testEncodingDataBase64() {
|
||||
let data = Data(bytes: [0xDE, 0xAD, 0xBE, 0xEF])
|
||||
let expectedValue = ["value":"3q2+7w=="]
|
||||
|
||||
_testRoundTrip(of: data, expectedValue: "3q2+7w==")
|
||||
_testRoundTrip(of: TopLevelWrapper(data), expectedValue: expectedValue)
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(data), expectedValue: expectedValue)
|
||||
}
|
||||
|
||||
func testEncodingDataCustom() {
|
||||
// We'll encode a number instead of data.
|
||||
let encode = { (_ data: Data, _ encoder: Encoder) throws -> Void in
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(42)
|
||||
}
|
||||
let decode = { (_: Decoder) throws -> Data in return Data() }
|
||||
let expectedValue = ["value": 42]
|
||||
|
||||
_testRoundTrip(of: Data(),
|
||||
expectedValue: 42,
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(Data()),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
|
||||
// Optional data should encode the same way.
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(Data()),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
}
|
||||
|
||||
func testEncodingDataCustomEmpty() {
|
||||
// Encoding nothing should encode an empty keyed container ({}).
|
||||
let encode = { (_: Data, _: Encoder) throws -> Void in }
|
||||
let decode = { (_: Decoder) throws -> Data in return Data() }
|
||||
let expectedValue = ["value": [:]]
|
||||
|
||||
_testRoundTrip(of: Data(),
|
||||
expectedValue: [:],
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(Data()),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
|
||||
// Optional Data should encode the same way.
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(Data()),
|
||||
expectedValue: expectedValue,
|
||||
dataEncodingStrategy: .custom(encode),
|
||||
dataDecodingStrategy: .custom(decode))
|
||||
}
|
||||
|
||||
// MARK: - Encoder Features
|
||||
func testNestedContainerCodingPaths() {
|
||||
let encoder = FirebaseEncoder()
|
||||
do {
|
||||
let _ = try encoder.encode(NestedContainersTestType())
|
||||
} catch let error as NSError {
|
||||
XCTFail("Caught error during encoding nested container types: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testSuperEncoderCodingPaths() {
|
||||
let encoder = FirebaseEncoder()
|
||||
do {
|
||||
let _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true))
|
||||
} catch let error as NSError {
|
||||
XCTFail("Caught error during encoding nested container types: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testInterceptURL() {
|
||||
// Want to make sure JSONEncoder writes out single-value URLs, not the keyed encoding.
|
||||
let expectedValue = ["value": "http://swift.org"]
|
||||
let url = URL(string: "http://swift.org")!
|
||||
_testRoundTrip(of: url, expectedValue: "http://swift.org")
|
||||
_testRoundTrip(of: TopLevelWrapper(url), expectedValue: expectedValue)
|
||||
_testRoundTrip(of: OptionalTopLevelWrapper(url), expectedValue: expectedValue)
|
||||
}
|
||||
|
||||
func testTypeCoercion() {
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self)
|
||||
_testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelNumericTypes() {
|
||||
_testRoundTrip(of: 3 as Int)
|
||||
_testRoundTrip(of: 3 as Int8)
|
||||
_testRoundTrip(of: 3 as Int16)
|
||||
_testRoundTrip(of: 3 as Int32)
|
||||
_testRoundTrip(of: 3 as Int64)
|
||||
_testRoundTrip(of: 3 as UInt)
|
||||
_testRoundTrip(of: 3 as UInt8)
|
||||
_testRoundTrip(of: 3 as UInt16)
|
||||
_testRoundTrip(of: 3 as UInt32)
|
||||
_testRoundTrip(of: 3 as UInt64)
|
||||
_testRoundTrip(of: 3 as Float)
|
||||
_testRoundTrip(of: 3 as Double)
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
private var _emptyDictionary: [String: Any] = [:]
|
||||
|
||||
private func _testRoundTrip<T>(of value: T,
|
||||
expectedValue json: Any? = nil,
|
||||
dateEncodingStrategy: FirebaseEncoder.DateEncodingStrategy = .deferredToDate,
|
||||
dateDecodingStrategy: FirebaseDecoder.DateDecodingStrategy = .deferredToDate,
|
||||
dataEncodingStrategy: FirebaseEncoder.DataEncodingStrategy = .base64,
|
||||
dataDecodingStrategy: FirebaseDecoder.DataDecodingStrategy = .base64) where T : Codable, T : Equatable {
|
||||
var payload: Any! = nil
|
||||
do {
|
||||
let encoder = FirebaseEncoder()
|
||||
encoder.dateEncodingStrategy = dateEncodingStrategy
|
||||
encoder.dataEncodingStrategy = dataEncodingStrategy
|
||||
payload = try encoder.encode(value)
|
||||
} catch {
|
||||
XCTFail("Failed to encode \(T.self) to val: \(error)")
|
||||
}
|
||||
|
||||
if let expectedJSON = json.flatMap({ $0 as? NSObject }), let payload = payload as? NSObject {
|
||||
XCTAssertEqual(expectedJSON, payload, "Produced JSON not identical to expected JSON.")
|
||||
}
|
||||
|
||||
do {
|
||||
let decoder = FirebaseDecoder()
|
||||
decoder.dateDecodingStrategy = dateDecodingStrategy
|
||||
decoder.dataDecodingStrategy = dataDecodingStrategy
|
||||
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 val: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func _testRoundTripTypeCoercionFailure<T,U>(of value: T, as type: U.Type) where T : Codable, U : Codable {
|
||||
do {
|
||||
let data = try FirebaseEncoder().encode(value)
|
||||
let _ = try FirebaseDecoder().decode(U.self, from: data)
|
||||
XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.")
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
/* FIXME: Import from %S/Inputs/Coding/SharedTypes.swift somehow. */
|
||||
|
||||
// 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
|
||||
|
||||
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 === rhs || 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
|
||||
let website: URL?
|
||||
|
||||
init(name: String, email: String, website: URL? = nil) {
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.website = website
|
||||
}
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case name
|
||||
case email
|
||||
case website
|
||||
}
|
||||
|
||||
// FIXME: Remove when subclasses (Employee) are able to override synthesized conformance.
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
email = try container.decode(String.self, forKey: .email)
|
||||
website = try container.decodeIfPresent(URL.self, forKey: .website)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(email, forKey: .email)
|
||||
try container.encodeIfPresent(website, forKey: .website)
|
||||
}
|
||||
|
||||
func isEqual(_ other: Person) -> Bool {
|
||||
return self.name == other.name &&
|
||||
self.email == other.email &&
|
||||
self.website == other.website
|
||||
}
|
||||
|
||||
static func ==(_ lhs: Person, _ rhs: Person) -> Bool {
|
||||
return lhs.isEqual(rhs)
|
||||
}
|
||||
|
||||
class var testValue: Person {
|
||||
return Person(name: "Johnny Appleseed", email: "appleseed@apple.com")
|
||||
}
|
||||
}
|
||||
|
||||
/// A class which shares its encoder and decoder with its superclass.
|
||||
fileprivate class Employee : Person {
|
||||
let id: Int
|
||||
|
||||
init(name: String, email: String, website: URL? = nil, id: Int) {
|
||||
self.id = id
|
||||
super.init(name: name, email: email, website: website)
|
||||
}
|
||||
|
||||
enum CodingKeys : String, CodingKey {
|
||||
case id
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(Int.self, forKey: .id)
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
override func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try super.encode(to: encoder)
|
||||
}
|
||||
|
||||
override func isEqual(_ other: Person) -> Bool {
|
||||
if let employee = other as? Employee {
|
||||
guard self.id == employee.id else { return false }
|
||||
}
|
||||
|
||||
return super.isEqual(other)
|
||||
}
|
||||
|
||||
override class var testValue: Employee {
|
||||
return Employee(name: "Johnny Appleseed", email: "appleseed@apple.com", id: 42)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple company struct which encodes as a dictionary of nested values.
|
||||
fileprivate struct Company : Codable, Equatable {
|
||||
let address: Address
|
||||
var employees: [Employee]
|
||||
|
||||
init(address: Address, employees: [Employee]) {
|
||||
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: [Employee.testValue])
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum type which decodes from Bool?.
|
||||
fileprivate enum EnhancedBool : Codable {
|
||||
case `true`
|
||||
case `false`
|
||||
case fileNotFound
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if container.decodeNil() {
|
||||
self = .fileNotFound
|
||||
} else {
|
||||
let value = try container.decode(Bool.self)
|
||||
self = value ? .true : .false
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .true: try container.encode(true)
|
||||
case .false: try container.encode(false)
|
||||
case .fileNotFound: try container.encodeNil()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which encodes as an array directly through a single value container.
|
||||
struct Numbers : Codable, Equatable {
|
||||
let values = [4, 8, 15, 16, 23, 42]
|
||||
|
||||
init() {}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let decodedValues = try container.decode([Int].self)
|
||||
guard decodedValues == values else {
|
||||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "The Numbers are wrong!"))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(values)
|
||||
}
|
||||
|
||||
static func ==(_ lhs: Numbers, _ rhs: Numbers) -> Bool {
|
||||
return lhs.values == rhs.values
|
||||
}
|
||||
|
||||
static var testValue: Numbers {
|
||||
return Numbers()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which encodes as a dictionary directly through a single value container.
|
||||
fileprivate final class Mapping : Codable, Equatable {
|
||||
let values: [String : URL]
|
||||
|
||||
init(values: [String : URL]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
values = try container.decode([String : URL].self)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(values)
|
||||
}
|
||||
|
||||
static func ==(_ lhs: Mapping, _ rhs: Mapping) -> Bool {
|
||||
return lhs === rhs || lhs.values == rhs.values
|
||||
}
|
||||
|
||||
static var testValue: Mapping {
|
||||
return Mapping(values: ["Apple": URL(string: "http://apple.com")!,
|
||||
"localhost": URL(string: "http://127.0.0.1")!])
|
||||
}
|
||||
}
|
||||
|
||||
struct NestedContainersTestType : Encodable {
|
||||
let testSuperEncoder: Bool
|
||||
|
||||
init(testSuperEncoder: Bool = false) {
|
||||
self.testSuperEncoder = testSuperEncoder
|
||||
}
|
||||
|
||||
enum TopLevelCodingKeys : Int, CodingKey {
|
||||
case a
|
||||
case b
|
||||
case c
|
||||
}
|
||||
|
||||
enum IntermediateCodingKeys : Int, CodingKey {
|
||||
case one
|
||||
case two
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
if self.testSuperEncoder {
|
||||
var topLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self)
|
||||
expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(topLevelContainer.codingPath, [], "New first-level keyed container has non-empty codingPath.")
|
||||
|
||||
let superEncoder = topLevelContainer.superEncoder(forKey: .a)
|
||||
expectEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(topLevelContainer.codingPath, [], "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(superEncoder.codingPath, [TopLevelCodingKeys.a], "New superEncoder had unexpected codingPath.")
|
||||
_testNestedContainers(in: superEncoder, baseCodingPath: [TopLevelCodingKeys.a])
|
||||
} else {
|
||||
_testNestedContainers(in: encoder, baseCodingPath: [])
|
||||
}
|
||||
}
|
||||
|
||||
func _testNestedContainers(in encoder: Encoder, baseCodingPath: [CodingKey]) {
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "New encoder has non-empty codingPath.")
|
||||
|
||||
// codingPath should not change upon fetching a non-nested container.
|
||||
var firstLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "New first-level keyed container has non-empty codingPath.")
|
||||
|
||||
// Nested Keyed Container
|
||||
do {
|
||||
// Nested container for key should have a new key pushed on.
|
||||
var secondLevelContainer = firstLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self, forKey: .a)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "New second-level keyed container had unexpected codingPath.")
|
||||
|
||||
// Inserting a keyed container should not change existing coding paths.
|
||||
let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self, forKey: .one)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.one], "New third-level keyed container had unexpected codingPath.")
|
||||
|
||||
// Inserting an unkeyed container should not change existing coding paths.
|
||||
let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer(forKey: .two)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath + [], "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath + [], "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.two], "New third-level unkeyed container had unexpected codingPath.")
|
||||
}
|
||||
|
||||
// Nested Unkeyed Container
|
||||
do {
|
||||
// Nested container for key should have a new key pushed on.
|
||||
var secondLevelContainer = firstLevelContainer.nestedUnkeyedContainer(forKey: .b)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "New second-level keyed container had unexpected codingPath.")
|
||||
|
||||
// Appending a keyed container should not change existing coding paths.
|
||||
let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self)
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.")
|
||||
expectEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 0)], "New third-level keyed container had unexpected codingPath.")
|
||||
|
||||
// Appending an unkeyed container should not change existing coding paths.
|
||||
let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer()
|
||||
expectEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.")
|
||||
expectEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.")
|
||||
expectEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.")
|
||||
expectEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 1)], "New third-level unkeyed container had unexpected codingPath.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Types
|
||||
|
||||
/// A key type which can take on any string or integer value.
|
||||
/// This needs to mirror _JSONKey.
|
||||
fileprivate struct _TestKey : CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
init(index: Int) {
|
||||
self.stringValue = "Index \(index)"
|
||||
self.intValue = index
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a type T (as T?) so that it can be encoded at the top level of a payload.
|
||||
fileprivate struct OptionalTopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
|
||||
let value: T?
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
// Provide an implementation of Codable to encode(forKey:) instead of encodeIfPresent(forKey:).
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case value
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
value = try container.decode(T?.self, forKey: .value)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(value, forKey: .value)
|
||||
}
|
||||
|
||||
static func ==(_ lhs: OptionalTopLevelWrapper<T>, _ rhs: OptionalTopLevelWrapper<T>) -> Bool {
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct FloatNaNPlaceholder : Codable, Equatable {
|
||||
init() {}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(Float.nan)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let float = try container.decode(Float.self)
|
||||
if !float.isNaN {
|
||||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(_ lhs: FloatNaNPlaceholder, _ rhs: FloatNaNPlaceholder) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct DoubleNaNPlaceholder : Codable, Equatable {
|
||||
init() {}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(Double.nan)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let double = try container.decode(Double.self)
|
||||
if !double.isNaN {
|
||||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(_ lhs: DoubleNaNPlaceholder, _ rhs: DoubleNaNPlaceholder) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user