mirror of
https://github.com/caoer/CodableFirebase.git
synced 2026-06-12 07:58:35 +08:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505f140e29 | ||
|
|
7ac3efc8b8 | ||
|
|
55b8bd92c1 | ||
|
|
9885bf2381 | ||
|
|
5e9f86496e | ||
|
|
9b65f4e473 | ||
|
|
cef13572db | ||
|
|
03459ec82a | ||
|
|
8ff6771f42 | ||
|
|
3767b9a771 | ||
|
|
c722389e5c | ||
|
|
6b0b78e597 | ||
|
|
1fbf4295b3 | ||
|
|
8ed17c7aec | ||
|
|
3cae6e90d9 | ||
|
|
222b080693 | ||
|
|
5b8627dbbe | ||
|
|
1384814716 | ||
|
|
b0e86866a0 | ||
|
|
0c60f70d91 | ||
|
|
df9dfe1dda | ||
|
|
2afa8c101c | ||
|
|
ec22c1649e | ||
|
|
8279c70901 | ||
|
|
c4ed0ec8eb | ||
|
|
fcfb7b8b1e | ||
|
|
747d540d77 | ||
|
|
e6947deb6b | ||
|
|
eaf809498a | ||
|
|
3a08c9e650 | ||
|
|
4cfd13702b | ||
|
|
3c61d54654 | ||
|
|
2528453f9b | ||
|
|
587db265cd | ||
|
|
04facd2f6d | ||
|
|
f3eed29a7d | ||
|
|
7aff81a267 | ||
|
|
9fa47dd610 | ||
|
|
31ecb57f14 | ||
|
|
6a6fcc1d07 | ||
|
|
02cd074714 | ||
|
|
69d0a835be | ||
|
|
e0b61e57e5 | ||
|
|
e32bd8f4d6 | ||
|
|
4dcdd049f0 | ||
|
|
db7bd47c06 | ||
|
|
969cd78a6b | ||
|
|
3da07216a3 | ||
|
|
cbf348f01d | ||
|
|
40a14eb088 | ||
|
|
95b168e16e | ||
|
|
1d7c5e88d9 | ||
|
|
04ff2eba1d | ||
|
|
ab2f387640 | ||
|
|
2e49c677f1 | ||
|
|
bd11b2b5bf | ||
|
|
0195c92b82 | ||
|
|
27d6397244 | ||
|
|
e9685432c0 | ||
|
|
fe226d61e2 | ||
|
|
6981a38487 | ||
|
|
af2921a5a9 | ||
|
|
fb110d0a6c | ||
|
|
24a2172269 | ||
|
|
a336aec29c | ||
|
|
b3f02bf7e1 | ||
|
|
31f708f215 | ||
|
|
3029b53316 | ||
|
|
f339062e3e | ||
|
|
37b70fddb4 | ||
|
|
a9480c9f6d | ||
|
|
d255a07fb7 |
@@ -1,11 +1,11 @@
|
||||
language: objective-c
|
||||
osx_image: xcode9
|
||||
osx_image: xcode9.3
|
||||
|
||||
env:
|
||||
- ACTION=test PLATFORM=Mac DESTINATION='platform=OS X'
|
||||
- ACTION=test PLATFORM=iOS DESTINATION='platform=iOS Simulator,name=iPhone 8'
|
||||
- ACTION=build PLATFORM=watchOS DESTINATION='platform=watchOS Simulator,name=Apple Watch - 38mm'
|
||||
- ACTION=test PLATFORM=tvOS DESTINATION='platform=tvOS Simulator,name=Apple TV 1080p'
|
||||
- ACTION=test PLATFORM=tvOS DESTINATION='platform=tvOS Simulator,name=Apple TV'
|
||||
|
||||
script:
|
||||
- xcodebuild clean $ACTION -project CodableFirebase.xcodeproj -scheme CodableFirebase -destination "$DESTINATION"
|
||||
|
||||
19
CodableFirebase.podspec
Normal file
19
CodableFirebase.podspec
Normal file
@@ -0,0 +1,19 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "CodableFirebase"
|
||||
s.version = "0.0.11"
|
||||
s.summary = "Use Codable with Firebase"
|
||||
s.description = "This library helps you use your custom models that conform to Codable protocol with Firebase Realtime Database and Firestore"
|
||||
s.homepage = "https://github.com/alickbass/CodableFirebase"
|
||||
s.license = { :type => "MIT", :file => "LICENSE" }
|
||||
s.author = { "Oleksii Dykan" => "alick_bass@mail.ru" }
|
||||
|
||||
s.ios.deployment_target = "9.0"
|
||||
s.osx.deployment_target = "10.11"
|
||||
s.watchos.deployment_target = "2.0"
|
||||
s.tvos.deployment_target = "9.0"
|
||||
s.requires_arc = true
|
||||
|
||||
s.source = { :git => "https://github.com/alickbass/CodableFirebase.git", :tag => s.version, :branch => 'master'}
|
||||
s.source_files = "CodableFirebase/*.swift"
|
||||
s.swift_version = '4.1'
|
||||
end
|
||||
@@ -12,6 +12,12 @@
|
||||
CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */; };
|
||||
CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */; };
|
||||
CE7DD3861F9DE4F7000225C5 /* TestCodableFirestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */; };
|
||||
CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF811FF3B35B00745EBE /* Encoder.swift */; };
|
||||
CEFDBF861FF3B56200745EBE /* Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF851FF3B56200745EBE /* Decoder.swift */; };
|
||||
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 */
|
||||
@@ -33,6 +39,12 @@
|
||||
CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreEncoder.swift; sourceTree = "<group>"; };
|
||||
CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreDecoder.swift; sourceTree = "<group>"; };
|
||||
CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCodableFirestore.swift; sourceTree = "<group>"; };
|
||||
CEFDBF811FF3B35B00745EBE /* Encoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = "<group>"; };
|
||||
CEFDBF851FF3B56200745EBE /* Decoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decoder.swift; sourceTree = "<group>"; };
|
||||
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 */
|
||||
@@ -78,6 +90,10 @@
|
||||
CE7DD36A1F9CFA81000225C5 /* CodableFirebase.h */,
|
||||
CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */,
|
||||
CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */,
|
||||
CEFDBF891FF3E24200745EBE /* FirebaseDecoder.swift */,
|
||||
CEFDBF8B1FF3E3CB00745EBE /* FirebaseEncoder.swift */,
|
||||
CEFDBF851FF3B56200745EBE /* Decoder.swift */,
|
||||
CEFDBF811FF3B35B00745EBE /* Encoder.swift */,
|
||||
CE7DD36B1F9CFA81000225C5 /* Info.plist */,
|
||||
);
|
||||
path = CodableFirebase;
|
||||
@@ -86,7 +102,9 @@
|
||||
CE7DD3741F9CFA81000225C5 /* CodableFirebaseTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEFDBF8D1FF3E63700745EBE /* CodaleTests.swift */,
|
||||
CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */,
|
||||
CEFDBF8F1FF3EB2900745EBE /* TestCodableFirebase.swift */,
|
||||
CE7DD3771F9CFA81000225C5 /* Info.plist */,
|
||||
);
|
||||
path = CodableFirebaseTests;
|
||||
@@ -149,7 +167,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0900;
|
||||
LastUpgradeCheck = 0900;
|
||||
LastUpgradeCheck = 0930;
|
||||
ORGANIZATIONNAME = ViolentOctopus;
|
||||
TargetAttributes = {
|
||||
CE7DD3661F9CFA81000225C5 = {
|
||||
@@ -204,7 +222,11 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */,
|
||||
CEFDBF8C1FF3E3CB00745EBE /* FirebaseEncoder.swift in Sources */,
|
||||
CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */,
|
||||
CEFDBF861FF3B56200745EBE /* Decoder.swift in Sources */,
|
||||
CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */,
|
||||
CEFDBF8A1FF3E24200745EBE /* FirebaseDecoder.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -213,6 +235,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE7DD3861F9DE4F7000225C5 /* TestCodableFirestore.swift in Sources */,
|
||||
CEFDBF8E1FF3E63700745EBE /* CodaleTests.swift in Sources */,
|
||||
CEFDBF901FF3EB2900745EBE /* TestCodableFirebase.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -241,6 +265,7 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -248,6 +273,7 @@
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -300,6 +326,7 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -307,6 +334,7 @@
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0900"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,7 +26,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -56,7 +55,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
1250
CodableFirebase/Decoder.swift
Normal file
1250
CodableFirebase/Decoder.swift
Normal file
File diff suppressed because it is too large
Load Diff
589
CodableFirebase/Encoder.swift
Normal file
589
CodableFirebase/Encoder.swift
Normal file
@@ -0,0 +1,589 @@
|
||||
//
|
||||
// Encoder.swift
|
||||
// CodableFirebase
|
||||
//
|
||||
// Created by Oleksii on 27/12/2017.
|
||||
// Copyright © 2017 ViolentOctopus. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class _FirebaseEncoder : Encoder {
|
||||
/// Options set on the top-level encoder to pass down the encoding hierarchy.
|
||||
struct _Options {
|
||||
let dateEncodingStrategy: FirebaseEncoder.DateEncodingStrategy?
|
||||
let dataEncodingStrategy: FirebaseEncoder.DataEncodingStrategy?
|
||||
let skipFirestoreTypes: Bool
|
||||
let userInfo: [CodingUserInfoKey : Any]
|
||||
}
|
||||
|
||||
fileprivate var storage: _FirebaseEncodingStorage
|
||||
fileprivate let options: _Options
|
||||
fileprivate(set) public var codingPath: [CodingKey]
|
||||
|
||||
public var userInfo: [CodingUserInfoKey : Any] {
|
||||
return options.userInfo
|
||||
}
|
||||
|
||||
init(options: _Options, codingPath: [CodingKey] = []) {
|
||||
self.storage = _FirebaseEncodingStorage()
|
||||
self.codingPath = codingPath
|
||||
self.options = options
|
||||
}
|
||||
|
||||
/// Returns whether a new element can be encoded at this coding path.
|
||||
///
|
||||
/// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
|
||||
fileprivate var canEncodeNewValue: Bool {
|
||||
// Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container).
|
||||
// At the same time, every time a container is requested, a new value gets pushed onto the storage stack.
|
||||
// If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition.
|
||||
//
|
||||
// This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path.
|
||||
// Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here).
|
||||
return self.storage.count == self.codingPath.count
|
||||
}
|
||||
|
||||
// MARK: - Encoder Methods
|
||||
public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
|
||||
// If an existing keyed container was already requested, return that one.
|
||||
let topContainer: NSMutableDictionary
|
||||
if canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = storage.pushKeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableDictionary else {
|
||||
preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
let container = _FirebaseKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
// If an existing unkeyed container was already requested, return that one.
|
||||
let topContainer: NSMutableArray
|
||||
if canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushUnkeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableArray else {
|
||||
preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
return _FirebaseUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirebaseEncodingStorage {
|
||||
// MARK: Properties
|
||||
/// The container stack.
|
||||
/// Elements may be any one of the plist types (NSNumber, NSString, NSDate, NSArray, NSDictionary).
|
||||
private(set) fileprivate var containers: [NSObject] = []
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with no containers.
|
||||
fileprivate init() {}
|
||||
|
||||
// MARK: - Modifying the Stack
|
||||
fileprivate var count: Int {
|
||||
return containers.count
|
||||
}
|
||||
|
||||
fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary {
|
||||
let dictionary = NSMutableDictionary()
|
||||
containers.append(dictionary)
|
||||
return dictionary
|
||||
}
|
||||
|
||||
fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray {
|
||||
let array = NSMutableArray()
|
||||
containers.append(array)
|
||||
return array
|
||||
}
|
||||
|
||||
fileprivate mutating func push(container: NSObject) {
|
||||
containers.append(container)
|
||||
}
|
||||
|
||||
fileprivate mutating func popContainer() -> NSObject {
|
||||
precondition(containers.count > 0, "Empty container stack.")
|
||||
return containers.popLast()!
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirebaseKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContainerProtocol {
|
||||
typealias Key = K
|
||||
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _FirebaseEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableDictionary
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _FirebaseEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - KeyedEncodingContainerProtocol Methods
|
||||
public mutating func encodeNil(forKey key: Key) throws { container[key.stringValue] = NSNull() }
|
||||
public mutating func encode(_ value: Bool, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: String, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Float, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
|
||||
encoder.codingPath.append(key)
|
||||
defer { encoder.codingPath.removeLast() }
|
||||
container[key.stringValue] = try encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container[key.stringValue] = dictionary
|
||||
|
||||
codingPath.append(key)
|
||||
defer { codingPath.removeLast() }
|
||||
|
||||
let container = _FirebaseKeyedEncodingContainer<NestedKey>(referencing: encoder, codingPath: codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
let array = NSMutableArray()
|
||||
container[key.stringValue] = array
|
||||
|
||||
codingPath.append(key)
|
||||
defer { codingPath.removeLast() }
|
||||
return _FirebaseUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _FirebaseReferencingEncoder(referencing: encoder, at: _FirebaseKey.super, wrapping: container)
|
||||
}
|
||||
|
||||
public mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
return _FirebaseReferencingEncoder(referencing: encoder, at: key, wrapping: container)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirebaseUnkeyedEncodingContainer : UnkeyedEncodingContainer {
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _FirebaseEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableArray
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
/// The number of elements encoded into the container.
|
||||
public var count: Int {
|
||||
return container.count
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _FirebaseEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - UnkeyedEncodingContainer Methods
|
||||
public mutating func encodeNil() throws { container.add(NSNull()) }
|
||||
public mutating func encode(_ value: Bool) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int8) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int16) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int32) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int64) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt8) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt16) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt32) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt64) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Float) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Double) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: String) throws { container.add(self.encoder.box(value)) }
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T) throws {
|
||||
encoder.codingPath.append(_FirebaseKey(index: count))
|
||||
defer { encoder.codingPath.removeLast() }
|
||||
container.add(try encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
|
||||
self.codingPath.append(_FirebaseKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container.add(dictionary)
|
||||
|
||||
let container = _FirebaseKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
self.codingPath.append(_FirebaseKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let array = NSMutableArray()
|
||||
self.container.add(array)
|
||||
return _FirebaseUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _FirebaseReferencingEncoder(referencing: encoder, at: container.count, wrapping: container)
|
||||
}
|
||||
}
|
||||
|
||||
struct _FirebaseKey : CodingKey {
|
||||
public var stringValue: String
|
||||
public var intValue: Int?
|
||||
|
||||
public init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
public init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
init(index: Int) {
|
||||
self.stringValue = "Index \(index)"
|
||||
self.intValue = index
|
||||
}
|
||||
|
||||
static let `super` = _FirebaseKey(stringValue: "super")!
|
||||
}
|
||||
|
||||
extension _FirebaseEncoder {
|
||||
|
||||
/// Returns the given value boxed in a container appropriate for pushing onto the container stack.
|
||||
fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Float) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Double) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) }
|
||||
|
||||
fileprivate func box<T : Encodable>(_ value: T) throws -> NSObject {
|
||||
return try self.box_(value) ?? NSDictionary()
|
||||
}
|
||||
|
||||
fileprivate func box(_ date: Date) throws -> NSObject {
|
||||
guard let options = options.dateEncodingStrategy else { return date as NSDate }
|
||||
|
||||
switch options {
|
||||
case .deferredToDate:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
try date.encode(to: self)
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .secondsSince1970:
|
||||
return NSNumber(value: date.timeIntervalSince1970)
|
||||
|
||||
case .millisecondsSince1970:
|
||||
return NSNumber(value: 1000.0 * date.timeIntervalSince1970)
|
||||
|
||||
case .iso8601:
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
return NSString(string: _iso8601Formatter.string(from: date))
|
||||
} else {
|
||||
fatalError("ISO8601DateFormatter is unavailable on this platform.")
|
||||
}
|
||||
|
||||
case .formatted(let formatter):
|
||||
return NSString(string: formatter.string(from: date))
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
try closure(date, self)
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func box(_ data: Data) throws -> NSObject {
|
||||
guard let options = options.dataEncodingStrategy else { return data as NSData }
|
||||
|
||||
switch options {
|
||||
case .deferredToData:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
try data.encode(to: self)
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .base64:
|
||||
return NSString(string: data.base64EncodedString())
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
try closure(data, self)
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
func box_<T : Encodable>(_ value: T) throws -> NSObject? {
|
||||
if T.self == Date.self || T.self == NSDate.self {
|
||||
return try self.box((value as! Date))
|
||||
} else if T.self == Data.self || T.self == NSData.self {
|
||||
return try self.box((value as! Data))
|
||||
} else if T.self == URL.self || T.self == NSURL.self {
|
||||
return self.box((value as! URL).absoluteString)
|
||||
} else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
|
||||
return (value as! NSDecimalNumber)
|
||||
} else if options.skipFirestoreTypes && (value is FirestoreEncodable) {
|
||||
guard let value = value as? NSObject else {
|
||||
throw DocumentReferenceError.typeIsNotNSObject
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// The value should request a container from the _FirebaseEncoder.
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try value.encode(to: self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// The top container should be a new container.
|
||||
guard self.storage.count > depth else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
extension _FirebaseEncoder : SingleValueEncodingContainer {
|
||||
// MARK: - SingleValueEncodingContainer Methods
|
||||
private func assertCanEncodeNewValue() {
|
||||
precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
|
||||
}
|
||||
|
||||
public func encodeNil() throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: NSNull())
|
||||
}
|
||||
|
||||
public func encode(_ value: Bool) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: String) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Float) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Double) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode<T : Encodable>(_ value: T) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try storage.push(container: box(value))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class _FirebaseReferencingEncoder : _FirebaseEncoder {
|
||||
// MARK: Reference types.
|
||||
/// The type of container we're referencing.
|
||||
private enum Reference {
|
||||
/// Referencing a specific index in an array container.
|
||||
case array(NSMutableArray, Int)
|
||||
|
||||
/// Referencing a specific key in a dictionary container.
|
||||
case dictionary(NSMutableDictionary, String)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
/// The encoder we're referencing.
|
||||
private let encoder: _FirebaseEncoder
|
||||
|
||||
/// The container reference itself.
|
||||
private let reference: Reference
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` by referencing the given array container in the given encoder.
|
||||
fileprivate init(referencing encoder: _FirebaseEncoder, at index: Int, wrapping array: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.reference = .array(array, index)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(_FirebaseKey(index: index))
|
||||
}
|
||||
|
||||
/// Initializes `self` by referencing the given dictionary container in the given encoder.
|
||||
fileprivate init(referencing encoder: _FirebaseEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
reference = .dictionary(dictionary, key.stringValue)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
codingPath.append(key)
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
fileprivate override var canEncodeNewValue: Bool {
|
||||
// With a regular encoder, the storage and coding path grow together.
|
||||
// A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
|
||||
// We have to take this into account.
|
||||
return storage.count == codingPath.count - encoder.codingPath.count - 1
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
// Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
|
||||
deinit {
|
||||
let value: Any
|
||||
switch storage.count {
|
||||
case 0: value = NSDictionary()
|
||||
case 1: value = self.storage.popContainer()
|
||||
default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
|
||||
}
|
||||
|
||||
switch self.reference {
|
||||
case .array(let array, let index):
|
||||
array.insert(value, at: index)
|
||||
|
||||
case .dictionary(let dictionary, let key):
|
||||
dictionary[NSString(string: key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DecodingError {
|
||||
internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError {
|
||||
let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
|
||||
return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
|
||||
}
|
||||
|
||||
fileprivate static func _typeDescription(of value: Any) -> String {
|
||||
if value is NSNull {
|
||||
return "a null value"
|
||||
} else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ {
|
||||
return "a number"
|
||||
} else if value is String {
|
||||
return "a string/data"
|
||||
} else if value is [Any] {
|
||||
return "an array"
|
||||
} else if value is [String : Any] {
|
||||
return "a dictionary"
|
||||
} else {
|
||||
return "\(type(of: value))"
|
||||
}
|
||||
}
|
||||
}
|
||||
66
CodableFirebase/FirebaseDecoder.swift
Normal file
66
CodableFirebase/FirebaseDecoder.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// FirebaseDecoder.swift
|
||||
// CodableFirebase
|
||||
//
|
||||
// Created by Oleksii on 27/12/2017.
|
||||
// Copyright © 2017 ViolentOctopus. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class FirebaseDecoder {
|
||||
/// The strategy to use for decoding `Date` values.
|
||||
public enum DateDecodingStrategy {
|
||||
/// Defer to `Date` for decoding. This is the default strategy.
|
||||
case deferredToDate
|
||||
|
||||
/// Decode the `Date` as a UNIX timestamp from a JSON number.
|
||||
case secondsSince1970
|
||||
|
||||
/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
|
||||
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
||||
case iso8601
|
||||
|
||||
/// Decode the `Date` as a string parsed by the given formatter.
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Decode the `Date` as a custom value decoded by the given closure.
|
||||
case custom((_ decoder: Decoder) throws -> Date)
|
||||
}
|
||||
|
||||
/// The strategy to use for decoding `Data` values.
|
||||
public enum DataDecodingStrategy {
|
||||
/// Defer to `Data` for decoding.
|
||||
case deferredToData
|
||||
|
||||
/// Decode the `Data` from a Base64-encoded string. This is the default strategy.
|
||||
case base64
|
||||
|
||||
/// Decode the `Data` as a custom value decoded by the given closure.
|
||||
case custom((_ decoder: Decoder) throws -> Data)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
open var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
|
||||
open var dataDecodingStrategy: DataDecodingStrategy = .deferredToData
|
||||
|
||||
open func decode<T : Decodable>(_ type: T.Type, from container: Any) throws -> T {
|
||||
let options = _FirebaseDecoder._Options(
|
||||
dateDecodingStrategy: dateDecodingStrategy,
|
||||
dataDecodingStrategy: dataDecodingStrategy,
|
||||
skipFirestoreTypes: false,
|
||||
userInfo: userInfo
|
||||
)
|
||||
let decoder = _FirebaseDecoder(referencing: container, options: options)
|
||||
guard let value = try decoder.unbox(container, as: T.self) else {
|
||||
throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given dictionary was invalid"))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
72
CodableFirebase/FirebaseEncoder.swift
Normal file
72
CodableFirebase/FirebaseEncoder.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// FirebaseEncoder.swift
|
||||
// CodableFirebase
|
||||
//
|
||||
// Created by Oleksii on 27/12/2017.
|
||||
// Copyright © 2017 ViolentOctopus. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class FirebaseEncoder {
|
||||
/// The strategy to use for encoding `Date` values.
|
||||
public enum DateEncodingStrategy {
|
||||
/// Defer to `Date` for choosing an encoding. This is the default strategy.
|
||||
case deferredToDate
|
||||
|
||||
/// Encode the `Date` as a UNIX timestamp (as a JSON number).
|
||||
case secondsSince1970
|
||||
|
||||
/// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
|
||||
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
||||
case iso8601
|
||||
|
||||
/// Encode the `Date` as a string formatted by the given formatter.
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Encode the `Date` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Date, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
/// The strategy to use for encoding `Data` values.
|
||||
public enum DataEncodingStrategy {
|
||||
/// Defer to `Data` for choosing an encoding.
|
||||
case deferredToData
|
||||
|
||||
/// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
|
||||
case base64
|
||||
|
||||
/// Encode the `Data` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Data, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
open var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
|
||||
open var dataEncodingStrategy: DataEncodingStrategy = .deferredToData
|
||||
|
||||
open func encode<Value : Encodable>(_ value: Value) throws -> Any {
|
||||
let options = _FirebaseEncoder._Options(
|
||||
dateEncodingStrategy: dateEncodingStrategy,
|
||||
dataEncodingStrategy: dataEncodingStrategy,
|
||||
skipFirestoreTypes: false,
|
||||
userInfo: userInfo
|
||||
)
|
||||
let encoder = _FirebaseEncoder(options: options)
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value,
|
||||
EncodingError.Context(codingPath: [],
|
||||
debugDescription: "Top-level \(Value.self) did not encode any values."))
|
||||
}
|
||||
|
||||
return topLevel
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ import Foundation
|
||||
open class FirestoreEncoder {
|
||||
public init() {}
|
||||
|
||||
open var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
open func encode<Value : Encodable>(_ value: Value) throws -> [String: Any] {
|
||||
let topLevel = try encodeToTopLevelContainer(value)
|
||||
switch topLevel {
|
||||
@@ -24,7 +26,13 @@ open class FirestoreEncoder {
|
||||
}
|
||||
|
||||
internal func encodeToTopLevelContainer<Value : Encodable>(_ value: Value) throws -> Any {
|
||||
let encoder = _FirestoreEncoder()
|
||||
let options = _FirebaseEncoder._Options(
|
||||
dateEncodingStrategy: nil,
|
||||
dataEncodingStrategy: nil,
|
||||
skipFirestoreTypes: true,
|
||||
userInfo: userInfo
|
||||
)
|
||||
let encoder = _FirebaseEncoder(options: options)
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value,
|
||||
EncodingError.Context(codingPath: [],
|
||||
@@ -34,489 +42,3 @@ open class FirestoreEncoder {
|
||||
return topLevel
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class _FirestoreEncoder : Encoder {
|
||||
fileprivate var storage: _FirestoreEncodingStorage
|
||||
fileprivate(set) public var codingPath: [CodingKey]
|
||||
public var userInfo: [CodingUserInfoKey : Any]
|
||||
|
||||
fileprivate init(codingPath: [CodingKey] = []) {
|
||||
self.storage = _FirestoreEncodingStorage()
|
||||
self.codingPath = codingPath
|
||||
userInfo = [:]
|
||||
}
|
||||
|
||||
/// Returns whether a new element can be encoded at this coding path.
|
||||
///
|
||||
/// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
|
||||
fileprivate var canEncodeNewValue: Bool {
|
||||
// Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container).
|
||||
// At the same time, every time a container is requested, a new value gets pushed onto the storage stack.
|
||||
// If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition.
|
||||
//
|
||||
// This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path.
|
||||
// Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here).
|
||||
return self.storage.count == self.codingPath.count
|
||||
}
|
||||
|
||||
// MARK: - Encoder Methods
|
||||
public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
|
||||
// If an existing keyed container was already requested, return that one.
|
||||
let topContainer: NSMutableDictionary
|
||||
if canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = storage.pushKeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableDictionary else {
|
||||
preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
let container = _FirestoreKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
// If an existing unkeyed container was already requested, return that one.
|
||||
let topContainer: NSMutableArray
|
||||
if canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushUnkeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableArray else {
|
||||
preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
return _FirestoreUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirestoreEncodingStorage {
|
||||
// MARK: Properties
|
||||
/// The container stack.
|
||||
/// Elements may be any one of the plist types (NSNumber, NSString, NSDate, NSArray, NSDictionary).
|
||||
private(set) fileprivate var containers: [NSObject] = []
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with no containers.
|
||||
fileprivate init() {}
|
||||
|
||||
// MARK: - Modifying the Stack
|
||||
fileprivate var count: Int {
|
||||
return containers.count
|
||||
}
|
||||
|
||||
fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary {
|
||||
let dictionary = NSMutableDictionary()
|
||||
containers.append(dictionary)
|
||||
return dictionary
|
||||
}
|
||||
|
||||
fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray {
|
||||
let array = NSMutableArray()
|
||||
containers.append(array)
|
||||
return array
|
||||
}
|
||||
|
||||
fileprivate mutating func push(container: NSObject) {
|
||||
containers.append(container)
|
||||
}
|
||||
|
||||
fileprivate mutating func popContainer() -> NSObject {
|
||||
precondition(containers.count > 0, "Empty container stack.")
|
||||
return containers.popLast()!
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirestoreKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContainerProtocol {
|
||||
typealias Key = K
|
||||
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _FirestoreEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableDictionary
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - KeyedEncodingContainerProtocol Methods
|
||||
public mutating func encodeNil(forKey key: Key) throws { container[key.stringValue] = NSNull() }
|
||||
public mutating func encode(_ value: Bool, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Int64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: UInt64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: String, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Float, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) }
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
|
||||
encoder.codingPath.append(key)
|
||||
defer { encoder.codingPath.removeLast() }
|
||||
container[key.stringValue] = try encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container[key.stringValue] = dictionary
|
||||
|
||||
codingPath.append(key)
|
||||
defer { codingPath.removeLast() }
|
||||
|
||||
let container = _FirestoreKeyedEncodingContainer<NestedKey>(referencing: encoder, codingPath: codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
let array = NSMutableArray()
|
||||
container[key.stringValue] = array
|
||||
|
||||
codingPath.append(key)
|
||||
defer { codingPath.removeLast() }
|
||||
return _FirestoreUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _FirestoreReferencingEncoder(referencing: encoder, at: _FirestoreKey.super, wrapping: container)
|
||||
}
|
||||
|
||||
public mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
return _FirestoreReferencingEncoder(referencing: encoder, at: key, wrapping: container)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _FirestoreUnkeyedEncodingContainer : UnkeyedEncodingContainer {
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _FirestoreEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableArray
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
/// The number of elements encoded into the container.
|
||||
public var count: Int {
|
||||
return container.count
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - UnkeyedEncodingContainer Methods
|
||||
public mutating func encodeNil() throws { container.add(NSNull()) }
|
||||
public mutating func encode(_ value: Bool) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int8) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int16) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int32) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int64) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt8) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt16) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt32) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt64) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Float) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Double) throws { container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: String) throws { container.add(self.encoder.box(value)) }
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T) throws {
|
||||
encoder.codingPath.append(_FirestoreKey(index: count))
|
||||
defer { encoder.codingPath.removeLast() }
|
||||
container.add(try encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
|
||||
self.codingPath.append(_FirestoreKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container.add(dictionary)
|
||||
|
||||
let container = _FirestoreKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
self.codingPath.append(_FirestoreKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let array = NSMutableArray()
|
||||
self.container.add(array)
|
||||
return _FirestoreUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _FirestoreReferencingEncoder(referencing: encoder, at: container.count, wrapping: container)
|
||||
}
|
||||
}
|
||||
|
||||
struct _FirestoreKey : CodingKey {
|
||||
public var stringValue: String
|
||||
public var intValue: Int?
|
||||
|
||||
public init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
public init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
init(index: Int) {
|
||||
self.stringValue = "Index \(index)"
|
||||
self.intValue = index
|
||||
}
|
||||
|
||||
static let `super` = _FirestoreKey(stringValue: "super")!
|
||||
}
|
||||
|
||||
extension _FirestoreEncoder {
|
||||
|
||||
/// Returns the given value boxed in a container appropriate for pushing onto the container stack.
|
||||
fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Float) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Double) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) }
|
||||
|
||||
fileprivate func box<T : Encodable>(_ value: T) throws -> NSObject {
|
||||
return try self.box_(value) ?? NSDictionary()
|
||||
}
|
||||
|
||||
fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
|
||||
if T.self == Date.self || T.self == NSDate.self {
|
||||
return (value as! NSDate)
|
||||
} else if T.self == Data.self || T.self == NSData.self {
|
||||
return (value as! NSData)
|
||||
}
|
||||
|
||||
// The value should request a container from the _FirestoreEncoder.
|
||||
let depth = storage.count
|
||||
try value.encode(to: self)
|
||||
|
||||
// The top container should be a new container.
|
||||
guard storage.count > depth else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
extension _FirestoreEncoder : SingleValueEncodingContainer {
|
||||
// MARK: - SingleValueEncodingContainer Methods
|
||||
private func assertCanEncodeNewValue() {
|
||||
precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
|
||||
}
|
||||
|
||||
public func encodeNil() throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: NSNull())
|
||||
}
|
||||
|
||||
public func encode(_ value: Bool) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: String) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Float) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Double) throws {
|
||||
assertCanEncodeNewValue()
|
||||
storage.push(container: box(value))
|
||||
}
|
||||
|
||||
public func encode<T : Encodable>(_ value: T) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try storage.push(container: box(value))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class _FirestoreReferencingEncoder : _FirestoreEncoder {
|
||||
// MARK: Reference types.
|
||||
/// The type of container we're referencing.
|
||||
private enum Reference {
|
||||
/// Referencing a specific index in an array container.
|
||||
case array(NSMutableArray, Int)
|
||||
|
||||
/// Referencing a specific key in a dictionary container.
|
||||
case dictionary(NSMutableDictionary, String)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
/// The encoder we're referencing.
|
||||
private let encoder: _FirestoreEncoder
|
||||
|
||||
/// The container reference itself.
|
||||
private let reference: Reference
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` by referencing the given array container in the given encoder.
|
||||
fileprivate init(referencing encoder: _FirestoreEncoder, at index: Int, wrapping array: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.reference = .array(array, index)
|
||||
super.init(codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(_FirestoreKey(index: index))
|
||||
}
|
||||
|
||||
/// Initializes `self` by referencing the given dictionary container in the given encoder.
|
||||
fileprivate init(referencing encoder: _FirestoreEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
reference = .dictionary(dictionary, key.stringValue)
|
||||
super.init(codingPath: encoder.codingPath)
|
||||
codingPath.append(key)
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
fileprivate override var canEncodeNewValue: Bool {
|
||||
// With a regular encoder, the storage and coding path grow together.
|
||||
// A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
|
||||
// We have to take this into account.
|
||||
return storage.count == codingPath.count - encoder.codingPath.count - 1
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
// Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
|
||||
deinit {
|
||||
let value: Any
|
||||
switch storage.count {
|
||||
case 0: value = NSDictionary()
|
||||
case 1: value = self.storage.popContainer()
|
||||
default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
|
||||
}
|
||||
|
||||
switch self.reference {
|
||||
case .array(let array, let index):
|
||||
array.insert(value, at: index)
|
||||
|
||||
case .dictionary(let dictionary, let key):
|
||||
dictionary[NSString(string: key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DecodingError {
|
||||
internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError {
|
||||
let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
|
||||
return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
|
||||
}
|
||||
|
||||
fileprivate static func _typeDescription(of value: Any) -> String {
|
||||
if value is NSNull {
|
||||
return "a null value"
|
||||
} else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ {
|
||||
return "a number"
|
||||
} else if value is String {
|
||||
return "a string/data"
|
||||
} else if value is [Any] {
|
||||
return "an array"
|
||||
} else if value is [String : Any] {
|
||||
return "a dictionary"
|
||||
} else {
|
||||
return "\(type(of: value))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.0.1</string>
|
||||
<string>0.0.11</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
202
CodableFirebaseTests/CodaleTests.swift
Normal file
202
CodableFirebaseTests/CodaleTests.swift
Normal file
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// CodaleTests.swift
|
||||
// CodableFirebaseTests
|
||||
//
|
||||
// Created by Oleksii on 27/12/2017.
|
||||
// Copyright © 2017 ViolentOctopus. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import CodableFirebase
|
||||
|
||||
func debugDescription<T>(_ value: T) -> String {
|
||||
if let debugDescribable = value as? CustomDebugStringConvertible {
|
||||
return debugDescribable.debugDescription
|
||||
} else if let describable = value as? CustomStringConvertible {
|
||||
return describable.description
|
||||
} else {
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
func expectRoundTripEquality<T : Codable>(of value: T, encode: (T) throws -> Any, decode: (Any) throws -> T, lineNumber: Int) where T : Equatable {
|
||||
let data: Any
|
||||
do {
|
||||
data = try encode(value)
|
||||
} catch {
|
||||
fatalError("\(#file):\(lineNumber): Unable to encode \(T.self) <\(debugDescription(value))>: \(error)")
|
||||
}
|
||||
|
||||
let decoded: T
|
||||
do {
|
||||
decoded = try decode(data as! NSObject)
|
||||
} catch {
|
||||
fatalError("\(#file):\(lineNumber): Unable to decode \(T.self) <\(debugDescription(value))>: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertEqual(value, decoded, "\(#file):\(lineNumber): Decoded \(T.self) <\(debugDescription(decoded))> not equal to original <\(debugDescription(value))>")
|
||||
}
|
||||
|
||||
func expectRoundTripEqualityThroughFirebaseDatabase<T : Codable>(for value: T, lineNumber: Int) where T : Equatable {
|
||||
let encode = { (_ value: T) throws -> Any in
|
||||
return try FirebaseEncoder().encode(value) as! NSObject
|
||||
}
|
||||
|
||||
let decode = { (_ data: Any) throws -> T in
|
||||
return try FirebaseDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
expectRoundTripEquality(of: value, encode: encode, decode: decode, lineNumber: lineNumber)
|
||||
}
|
||||
|
||||
// MARK: - Helper Types
|
||||
// A wrapper around a UUID that will allow it to be encoded at the top level of an encoder.
|
||||
struct UUIDCodingWrapper : Codable, Equatable {
|
||||
let value: UUID
|
||||
|
||||
init(_ value: UUID) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
static func ==(_ lhs: UUIDCodingWrapper, _ rhs: UUIDCodingWrapper) -> Bool {
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
}
|
||||
|
||||
class CodaleTests: XCTestCase {
|
||||
|
||||
// MARK: - Calendar
|
||||
lazy var calendarValues: [Int : Calendar] = [
|
||||
#line : Calendar(identifier: .gregorian),
|
||||
#line : Calendar(identifier: .buddhist),
|
||||
#line : Calendar(identifier: .chinese),
|
||||
#line : Calendar(identifier: .coptic),
|
||||
#line : Calendar(identifier: .ethiopicAmeteMihret),
|
||||
#line : Calendar(identifier: .ethiopicAmeteAlem),
|
||||
#line : Calendar(identifier: .hebrew),
|
||||
#line : Calendar(identifier: .iso8601),
|
||||
#line : Calendar(identifier: .indian),
|
||||
#line : Calendar(identifier: .islamic),
|
||||
#line : Calendar(identifier: .islamicCivil),
|
||||
#line : Calendar(identifier: .japanese),
|
||||
#line : Calendar(identifier: .persian),
|
||||
#line : Calendar(identifier: .republicOfChina),
|
||||
]
|
||||
|
||||
func test_Calendar_Database() {
|
||||
for (testLine, calendar) in calendarValues {
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: calendar, lineNumber: testLine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CharacterSet
|
||||
lazy var characterSetValues: [Int : CharacterSet] = [
|
||||
#line : CharacterSet.controlCharacters,
|
||||
#line : CharacterSet.whitespaces,
|
||||
#line : CharacterSet.whitespacesAndNewlines,
|
||||
#line : CharacterSet.decimalDigits,
|
||||
#line : CharacterSet.letters,
|
||||
#line : CharacterSet.lowercaseLetters,
|
||||
#line : CharacterSet.uppercaseLetters,
|
||||
#line : CharacterSet.nonBaseCharacters,
|
||||
#line : CharacterSet.alphanumerics,
|
||||
#line : CharacterSet.decomposables,
|
||||
#line : CharacterSet.illegalCharacters,
|
||||
#line : CharacterSet.punctuationCharacters,
|
||||
#line : CharacterSet.capitalizedLetters,
|
||||
#line : CharacterSet.symbols,
|
||||
#line : CharacterSet.newlines
|
||||
]
|
||||
|
||||
func test_CharacterSet_Database() {
|
||||
for (testLine, characterSet) in characterSetValues {
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: characterSet, lineNumber: testLine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CGAffineTransform
|
||||
lazy var cg_affineTransformValues: [Int : CGAffineTransform] = {
|
||||
var values = [
|
||||
#line : CGAffineTransform.identity,
|
||||
#line : CGAffineTransform(),
|
||||
#line : CGAffineTransform(translationX: 2.0, y: 2.0),
|
||||
#line : CGAffineTransform(scaleX: 2.0, y: 2.0),
|
||||
#line : CGAffineTransform(a: 1.0, b: 2.5, c: 66.2, d: 40.2, tx: -5.5, ty: 3.7),
|
||||
#line : CGAffineTransform(a: -55.66, b: 22.7, c: 1.5, d: 0.0, tx: -22, ty: -33),
|
||||
#line : CGAffineTransform(a: 4.5, b: 1.1, c: 0.025, d: 0.077, tx: -0.55, ty: 33.2),
|
||||
#line : CGAffineTransform(a: 7.0, b: -2.3, c: 6.7, d: 0.25, tx: 0.556, ty: 0.99),
|
||||
#line : CGAffineTransform(a: 0.498, b: -0.284, c: -0.742, d: 0.3248, tx: 12, ty: 44)
|
||||
]
|
||||
|
||||
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
|
||||
values[#line] = CGAffineTransform(rotationAngle: .pi / 2)
|
||||
}
|
||||
|
||||
return values
|
||||
}()
|
||||
|
||||
func test_CGAffineTransform_Database() {
|
||||
for (testLine, transform) in cg_affineTransformValues {
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: transform, lineNumber: testLine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DateComponents
|
||||
lazy var dateComponents: Set<Calendar.Component> = [
|
||||
.era, .year, .month, .day, .hour, .minute, .second, .nanosecond,
|
||||
.weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear,
|
||||
.yearForWeekOfYear, .timeZone, .calendar
|
||||
]
|
||||
|
||||
func test_DateComponents_Database() {
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let components = calendar.dateComponents(dateComponents, from: Date())
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: components, lineNumber: #line - 1)
|
||||
}
|
||||
|
||||
// MARK: - URL
|
||||
lazy var urlValues: [Int : URL] = {
|
||||
var values: [Int : URL] = [
|
||||
#line : URL(fileURLWithPath: NSTemporaryDirectory()),
|
||||
#line : URL(fileURLWithPath: "/"),
|
||||
#line : URL(string: "http://swift.org")!,
|
||||
#line : URL(string: "documentation", relativeTo: URL(string: "http://swift.org")!)!
|
||||
]
|
||||
|
||||
if #available(OSX 10.11, iOS 9.0, *) {
|
||||
values[#line] = URL(fileURLWithPath: "bin/sh", relativeTo: URL(fileURLWithPath: "/"))
|
||||
}
|
||||
|
||||
return values
|
||||
}()
|
||||
|
||||
func test_URL_Database() {
|
||||
for (testLine, url) in urlValues {
|
||||
// URLs encode as single strings in JSON. They lose their baseURL this way.
|
||||
// For relative URLs, we don't expect them to be equal to the original.
|
||||
if url.baseURL == nil {
|
||||
// This is an absolute URL; we can expect equality.
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: TopLevelWrapper(url), lineNumber: testLine)
|
||||
} else {
|
||||
// This is a relative URL. Make it absolute first.
|
||||
let absoluteURL = URL(string: url.absoluteString)!
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: TopLevelWrapper(absoluteURL), lineNumber: testLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UUID
|
||||
lazy var uuidValues: [Int : UUID] = [
|
||||
#line : UUID(),
|
||||
#line : UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!,
|
||||
#line : UUID(uuidString: "e621e1f8-c36c-495a-93fc-0c247a3e6e5f")!,
|
||||
#line : UUID(uuid: uuid_t(0xe6,0x21,0xe1,0xf8,0xc3,0x6c,0x49,0x5a,0x93,0xfc,0x0c,0x24,0x7a,0x3e,0x6e,0x5f))
|
||||
]
|
||||
|
||||
func test_UUID_Database() {
|
||||
for (testLine, uuid) in uuidValues {
|
||||
// We have to wrap the UUID since we cannot have a top-level string.
|
||||
expectRoundTripEqualityThroughFirebaseDatabase(for: UUIDCodingWrapper(uuid), lineNumber: testLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>0.0.11</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
|
||||
885
CodableFirebaseTests/TestCodableFirebase.swift
Normal file
885
CodableFirebaseTests/TestCodableFirebase.swift
Normal file
@@ -0,0 +1,885 @@
|
||||
//
|
||||
// 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: - GeoPoint
|
||||
func testEncodingGeoPoint() {
|
||||
let point = Point(latitude: 2, longitude: 2)
|
||||
XCTAssertEqual((try? FirebaseEncoder().encode(point)) as? NSDictionary, ["latitude": 2, "longitude": 2])
|
||||
XCTAssertEqual(try? FirebaseDecoder().decode(Point.self, from: ["latitude": 2, "longitude": 2]), point)
|
||||
}
|
||||
|
||||
// MARK: - Document Reference
|
||||
func testEncodingDocumentReference() {
|
||||
XCTAssertThrowsError(try FirebaseEncoder().encode(DocumentReference()))
|
||||
XCTAssertThrowsError(try FirebaseDecoder().decode(DocumentReference.self, from: []))
|
||||
}
|
||||
|
||||
// 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 value not identical to expected value.")
|
||||
}
|
||||
|
||||
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: - GeoPoint
|
||||
struct Point: GeoPointType, Equatable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
|
||||
static func == (lhs: Point, rhs: Point) -> Bool {
|
||||
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ReferenceType
|
||||
fileprivate struct DocumentReference: DocumentReferenceType {}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -29,280 +29,8 @@ fileprivate struct Document: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
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 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")!])
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a type T so that it can be encoded at the top level of a payload.
|
||||
fileprivate struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
|
||||
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
|
||||
enum CodingKeys : String, CodingKey {
|
||||
case value
|
||||
}
|
||||
@@ -328,118 +56,6 @@ fileprivate struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A key type which can take on any string or integer value.
|
||||
/// This needs to mirror _PlistKey.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
class TestCodableFirestore: XCTestCase {
|
||||
|
||||
func testFirebaseEncoder() {
|
||||
@@ -466,116 +82,6 @@ class TestCodableFirestore: XCTestCase {
|
||||
XCTAssertEqual(try? FirestoreDecoder().decode(Document.self, from: dict) , model)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelEmptyStruct() {
|
||||
_testRoundTrip(of: EmptyStruct(), expected: [:])
|
||||
}
|
||||
|
||||
func testEncodingTopLevelEmptyClass() {
|
||||
_testRoundTrip(of: EmptyClass(), expected: [:])
|
||||
}
|
||||
|
||||
// MARK: - Encoding Top-Level Single-Value Types
|
||||
func testEncodingTopLevelSingleValueEnum() {
|
||||
let s1 = Switch.off
|
||||
_testEncodeFailure(of: s1)
|
||||
_testRoundTrip(of: TopLevelWrapper(s1))
|
||||
|
||||
let s2 = Switch.on
|
||||
_testEncodeFailure(of: s2)
|
||||
_testRoundTrip(of: TopLevelWrapper(s2))
|
||||
}
|
||||
|
||||
func testEncodingTopLevelSingleValueStruct() {
|
||||
let t = Timestamp(3141592653)
|
||||
_testEncodeFailure(of: t)
|
||||
_testRoundTrip(of: TopLevelWrapper(t))
|
||||
}
|
||||
|
||||
func testEncodingTopLevelSingleValueClass() {
|
||||
let c = Counter()
|
||||
_testEncodeFailure(of: c)
|
||||
_testRoundTrip(of: TopLevelWrapper(c))
|
||||
}
|
||||
|
||||
// MARK: - Encoding Top-Level Structured Types
|
||||
func testEncodingTopLevelStructuredStruct() {
|
||||
// Address is a struct type with multiple fields.
|
||||
_testRoundTrip(of: Address.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelStructuredClass() {
|
||||
// Person is a class with multiple fields.
|
||||
_testRoundTrip(of: Person.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelStructuredSingleClass() {
|
||||
// Mapping is a class which encodes as a dictionary through a single value container.
|
||||
_testRoundTrip(of: Mapping.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelDeepStructuredType() {
|
||||
// Company is a type with fields which are Codable themselves.
|
||||
_testRoundTrip(of: Company.testValue)
|
||||
}
|
||||
|
||||
func testEncodingClassWhichSharesEncoderWithSuper() {
|
||||
// Employee is a type which shares its encoder & decoder with its superclass, Person.
|
||||
_testRoundTrip(of: Employee.testValue)
|
||||
}
|
||||
|
||||
func testEncodingTopLevelNullableType() {
|
||||
// EnhancedBool is a type which encodes either as a Bool or as nil.
|
||||
_testEncodeFailure(of: EnhancedBool.true)
|
||||
_testEncodeFailure(of: EnhancedBool.false)
|
||||
_testEncodeFailure(of: EnhancedBool.fileNotFound)
|
||||
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.true))
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.false))
|
||||
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound))
|
||||
}
|
||||
|
||||
func testEncodingTopLevelNumericTypes() {
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Int))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Int8))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Int16))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Int32))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Int64))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as UInt))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as UInt8))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as UInt16))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as UInt32))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as UInt64))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Float))
|
||||
_testRoundTrip(of: TopLevelWrapper(3 as Double))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Encoder Features
|
||||
func testNestedContainerCodingPaths() {
|
||||
do {
|
||||
@@ -603,6 +109,26 @@ class TestCodableFirestore: XCTestCase {
|
||||
_testRoundTrip(of: TopLevelWrapper(date), expected: ["value": date])
|
||||
}
|
||||
|
||||
func testDecimalValue() {
|
||||
let value = Decimal(2)
|
||||
_testRoundTrip(of: TopLevelWrapper(value), expected: ["value": value])
|
||||
}
|
||||
|
||||
// MARK: - GeoPoint & Document Reference
|
||||
func testEncodingGeoPoint() {
|
||||
let point = GeoPoint(latitude: 2, longitude: 2)
|
||||
let wrapper = TopLevelWrapper(point)
|
||||
XCTAssertEqual((try? FirestoreEncoder().encode(wrapper)) as NSDictionary?, ["value": point])
|
||||
XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper<GeoPoint>.self, from: ["value": point]), wrapper)
|
||||
XCTAssertThrowsError(try FirestoreEncoder().encode(TopLevelWrapper(Point(latitude: 2, longitude: 2))))
|
||||
}
|
||||
|
||||
func testEncodingDocumentReference() {
|
||||
let val = TopLevelWrapper(DocumentReference())
|
||||
XCTAssertEqual((try? FirestoreEncoder().encode(val)) as NSDictionary?, ["value": val.value])
|
||||
XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper<DocumentReference>.self, from: ["value": val.value]), val)
|
||||
}
|
||||
|
||||
private func _testEncodeFailure<T : Encodable>(of value: T) {
|
||||
do {
|
||||
let _ = try FirestoreEncoder().encode(value)
|
||||
@@ -610,14 +136,6 @@ class TestCodableFirestore: XCTestCase {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func _testRoundTripTypeCoercionFailure<T,U>(of value: T, as type: U.Type) where T : Codable, U : Codable {
|
||||
do {
|
||||
let data = try FirestoreEncoder().encode(value)
|
||||
let _ = try FirestoreDecoder().decode(U.self, from: data)
|
||||
XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func _testRoundTrip<T>(of value: T, expected dict: [String: Any]? = nil) where T : Codable, T : Equatable {
|
||||
var payload: [String: Any]! = nil
|
||||
do {
|
||||
@@ -666,3 +184,21 @@ func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String)
|
||||
XCTAssertEqual(key1.stringValue, key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GeioPoint
|
||||
fileprivate class GeoPoint: NSObject, GeoPointType {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
|
||||
required init(latitude: Double, longitude: Double) {
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
}
|
||||
|
||||
static func == (lhs: Point, rhs: Point) -> Bool {
|
||||
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ReferenceType
|
||||
fileprivate class DocumentReference: NSObject, DocumentReferenceType {}
|
||||
|
||||
4
Gemfile
Normal file
4
Gemfile
Normal file
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'fastlane'
|
||||
gem 'cocoapods', '1.5.0'
|
||||
205
Gemfile.lock
Normal file
205
Gemfile.lock
Normal file
@@ -0,0 +1,205 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.0)
|
||||
activesupport (4.2.10)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
atomos (0.1.2)
|
||||
babosa (1.0.2)
|
||||
claide (1.0.2)
|
||||
cocoapods (1.5.0)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.5.0)
|
||||
cocoapods-deintegrate (>= 1.0.2, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.3.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (~> 2.0.1)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.6.5)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.1)
|
||||
xcodeproj (>= 1.5.7, < 2.0)
|
||||
cocoapods-core (1.5.0)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.2)
|
||||
cocoapods-downloader (1.2.0)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-stats (1.0.0)
|
||||
cocoapods-trunk (1.3.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.4.0)
|
||||
emoji_regex (0.1.1)
|
||||
escape (0.0.4)
|
||||
excon (0.62.0)
|
||||
faraday (0.15.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.12.2)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.1)
|
||||
fastlane (2.93.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 2.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (~> 0.1)
|
||||
excon (>= 0.45.0, < 1.0.0)
|
||||
faraday (~> 0.9)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.9)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.13.1, < 0.14.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
mini_magick (~> 4.5.1)
|
||||
multi_json
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.1.0, < 2.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 1.6.2, < 2.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.5.7, < 2.0.0)
|
||||
xcpretty (>= 0.2.4, < 1.0.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fourflusher (2.0.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.13.6)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.5)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
googleauth (0.6.2)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
logging (~> 2.0)
|
||||
memoist (~> 0.12)
|
||||
multi_json (~> 1.11)
|
||||
os (~> 0.9)
|
||||
signet (~> 0.7)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.1.0)
|
||||
jwt (2.1.0)
|
||||
little-plugger (1.1.4)
|
||||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
memoist (0.16.0)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_magick (4.5.1)
|
||||
minitest (5.11.3)
|
||||
molinillo (0.6.5)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.5)
|
||||
nap (1.1.0)
|
||||
naturally (2.1.0)
|
||||
netrc (0.11.0)
|
||||
os (0.9.6)
|
||||
plist (3.4.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.1)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (1.1.0)
|
||||
rubyzip (1.2.1)
|
||||
security (0.1.3)
|
||||
signet (0.8.1)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.4)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (1.8.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thread_safe (0.3.6)
|
||||
tty-cursor (0.5.0)
|
||||
tty-screen (0.6.4)
|
||||
tty-spinner (0.8.0)
|
||||
tty-cursor (>= 0.5.0)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.3.2)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.5.7)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.2)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.4)
|
||||
xcpretty (0.2.8)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (= 1.5.0)
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
78
README.md
78
README.md
@@ -1,16 +1,20 @@
|
||||
# CodableFirebase
|
||||
Use [Codable](https://developer.apple.com/documentation/swift/codable) with [Firebase](https://firebase.google.com)
|
||||
|
||||
[](https://github.com/alickbass/CodableFirebase)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://travis-ci.org/alickbass/CodableFirebase)
|
||||
[](https://codecov.io/gh/alickbass/CodableFirebase)
|
||||
|
||||
## Overview
|
||||
|
||||
This library helps you to use your custom type that conform to `Codable` protocol with Firebase. Here's an example of model:
|
||||
This library helps you to use your custom types that conform to `Codable` protocol with Firebase. Here's an example of a custom model:
|
||||
|
||||
```swift
|
||||
struct Model: Codable {
|
||||
enum MyEnum: Int, Codable {
|
||||
case one, two, three
|
||||
}
|
||||
|
||||
let stringExample: String
|
||||
let booleanExample: Bool
|
||||
let numberExample: Double
|
||||
@@ -18,9 +22,40 @@ struct Model: Codable {
|
||||
let arrayExample: [String]
|
||||
let nullExample: Int?
|
||||
let objectExample: [String: String]
|
||||
let myEnum: MyEnum
|
||||
}
|
||||
```
|
||||
|
||||
### Firebase Database usage
|
||||
|
||||
This is how you would use the library with [Firebase Realtime Database](https://firebase.google.com/products/realtime-database/):
|
||||
|
||||
```swift
|
||||
import Firebase
|
||||
import CodableFirebase
|
||||
|
||||
let model: Model // here you will create an instance of Model
|
||||
let data = try! FirebaseEncoder().encode(model)
|
||||
|
||||
Database.database().reference().child("model").setValue(data)
|
||||
```
|
||||
|
||||
And here is how you would read the same value from [Firebase Realtime Database](https://firebase.google.com/products/realtime-database/):
|
||||
|
||||
```swift
|
||||
Database.database().reference().child("model").observeSingleEvent(of: .value, with: { (snapshot) in
|
||||
guard let value = snapshot.value else { return }
|
||||
do {
|
||||
let model = try FirebaseDecoder().decode(Model.self, from: value)
|
||||
print(model)
|
||||
} catch let error {
|
||||
print(error)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Firestore usage
|
||||
|
||||
And this is how you would encode it with [Firebase Firestore](https://firebase.google.com/products/firestore/):
|
||||
|
||||
```swift
|
||||
@@ -50,3 +85,42 @@ Firestore.firestore().collection("data").document("one").getDocument { (document
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How to use `GeoPoint`, `DocumentRefence`, `FieldValue` in Firestore
|
||||
|
||||
In order to use these 2 types with `Firestore`, you need to add the following code somewhere in your app:
|
||||
|
||||
```swift
|
||||
extension DocumentReference: DocumentReferenceType {}
|
||||
extension GeoPoint: GeoPointType {}
|
||||
extension FieldValue: FieldValueType {}
|
||||
```
|
||||
|
||||
and now they become `Codable` and can be used properly with `FirestoreEncoder` and `FirestoreDecoder`.
|
||||
|
||||
***PLEASE NOTE*** that as `FieldValue` is only used to [`setData()` and `updateData()`](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/FieldValue), it only adopts the `Encodable` protocol.
|
||||
|
||||
## Integration
|
||||
|
||||
### CocoaPods (iOS 9+)
|
||||
|
||||
You can use CocoaPods to install CodableFirebase by adding it to your Podfile:
|
||||
|
||||
```swift
|
||||
platform :ios, '9.0'
|
||||
use_frameworks!
|
||||
|
||||
target 'MyApp' do
|
||||
pod 'CodableFirebase'
|
||||
end
|
||||
```
|
||||
|
||||
Note that this requires CocoaPods version 36, and your iOS deployment target to be at least 9.0:
|
||||
|
||||
### Carthage (iOS 9+)
|
||||
|
||||
You can use Carthage to install CodableFirebase by adding it to your Cartfile:
|
||||
|
||||
```swift
|
||||
github "alickbass/CodableFirebase"
|
||||
```
|
||||
|
||||
34
fastlane/Fastfile
Normal file
34
fastlane/Fastfile
Normal file
@@ -0,0 +1,34 @@
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
|
||||
desc "Release a new version with a patch bump_type"
|
||||
lane :patch do
|
||||
release("patch")
|
||||
end
|
||||
|
||||
desc "Release a new version with a minor bump_type"
|
||||
lane :minor do
|
||||
release("minor")
|
||||
end
|
||||
|
||||
desc "Release a new version with a major bump_type"
|
||||
lane :major do
|
||||
release("major")
|
||||
end
|
||||
|
||||
def release(type)
|
||||
pod_lib_lint
|
||||
podspec_name = "CodableFirebase.podspec"
|
||||
version = version_bump_podspec(path: podspec_name,
|
||||
bump_type: type)
|
||||
increment_version_number(version_number: version)
|
||||
#cocoapods
|
||||
git_add
|
||||
git_commit(path: ".",
|
||||
message: "#{version} release")
|
||||
add_git_tag(tag: "#{version}")
|
||||
push_to_git_remote
|
||||
pod_push
|
||||
end
|
||||
end
|
||||
39
fastlane/README.md
Normal file
39
fastlane/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
fastlane documentation
|
||||
================
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
Install _fastlane_ using
|
||||
```
|
||||
[sudo] gem install fastlane -NV
|
||||
```
|
||||
or alternatively using `brew cask install fastlane`
|
||||
|
||||
# Available Actions
|
||||
## iOS
|
||||
### ios patch
|
||||
```
|
||||
fastlane ios patch
|
||||
```
|
||||
Release a new version with a patch bump_type
|
||||
### ios minor
|
||||
```
|
||||
fastlane ios minor
|
||||
```
|
||||
Release a new version with a minor bump_type
|
||||
### ios major
|
||||
```
|
||||
fastlane ios major
|
||||
```
|
||||
Release a new version with a major bump_type
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
|
||||
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
|
||||
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
Reference in New Issue
Block a user