Skip to content

Commit

Permalink
Make Firestore use FirebaseDataEncoder and FirebaseDataDecoder (re-im…
Browse files Browse the repository at this point in the history
…plementation of #8858) (#9465)
  • Loading branch information
mortenbekditlevsen authored Sep 23, 2022
1 parent b695d99 commit 65f6cfa
Show file tree
Hide file tree
Showing 20 changed files with 271 additions and 2,100 deletions.
1 change: 1 addition & 0 deletions CoreOnly/Tests/FirebasePodTest/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ target 'FirebasePodTest' do
pod 'FirebaseCoreDiagnostics', :path => '../../../'
pod 'FirebaseCoreInternal', :path => '../../../'
pod 'FirebaseCoreExtension', :path => '../../../'
pod 'FirebaseSharedSwift', :path => '../../../'

pod 'FirebaseAnalytics' # Analytics is not open source
end
2 changes: 1 addition & 1 deletion FirebaseFirestoreSwift.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
s.requires_arc = true
s.source_files = [
'Firestore/Swift/Source/**/*.swift',
'Firestore/third_party/FirestoreEncoder/*.swift',
]

s.dependency 'FirebaseFirestore', '~> 9.0'
s.dependency 'FirebaseSharedSwift', '~> 9.0'
end
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@

import Foundation

public protocol StructureCodingPassthroughTypeResolver {
static func isPassthroughType<T>(_ t: T) -> Bool
}

private struct NoPassthroughTypes: StructureCodingPassthroughTypeResolver {
static func isPassthroughType<T>(_ t: T) -> Bool {
return false
}
}

public protocol StructureCodingUncodedUnkeyed {}

extension DecodingError {
/// Returns a `.typeMismatch` error describing the expected type.
///
Expand Down Expand Up @@ -233,6 +245,9 @@ public class FirebaseDataEncoder {
/// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys

/// A type that can resolve which types to 'pass through' - or leave alone while encoding. Defaults to not passing any types through.
open var passthroughTypeResolver: StructureCodingPassthroughTypeResolver.Type = NoPassthroughTypes.self

/// Contextual user-provided information for use during encoding.
open var userInfo: [CodingUserInfoKey : Any] = [:]

Expand All @@ -242,6 +257,7 @@ public class FirebaseDataEncoder {
let dataEncodingStrategy: DataEncodingStrategy
let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
let keyEncodingStrategy: KeyEncodingStrategy
let passthroughTypeResolver: StructureCodingPassthroughTypeResolver.Type
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -251,6 +267,7 @@ public class FirebaseDataEncoder {
dataEncodingStrategy: dataEncodingStrategy,
nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
keyEncodingStrategy: keyEncodingStrategy,
passthroughTypeResolver: passthroughTypeResolver,
userInfo: userInfo)
}

Expand Down Expand Up @@ -501,6 +518,7 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon
}

public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
if T.self is StructureCodingUncodedUnkeyed.Type { return }
self.encoder.codingPath.append(key)
defer { self.encoder.codingPath.removeLast() }
self.container[_converted(key).stringValue] = try self.encoder.box(value)
Expand Down Expand Up @@ -928,6 +946,8 @@ extension __JSONEncoder {
return (value as! NSDecimalNumber)
} else if value is _JSONStringDictionaryEncodableMarker {
return try self.box(value as! [String : Encodable])
} else if let object = value as? NSObject, self.options.passthroughTypeResolver.isPassthroughType(value) {
return object
}

// The value should request a container from the __JSONEncoder.
Expand Down Expand Up @@ -1166,6 +1186,9 @@ public class FirebaseDataDecoder {
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

/// A type that can resolve which types to 'pass through' - or leave alone while decoding. Defaults to not passing any types through.
open var passthroughTypeResolver: StructureCodingPassthroughTypeResolver.Type = NoPassthroughTypes.self

/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey : Any] = [:]

Expand All @@ -1175,6 +1198,7 @@ public class FirebaseDataDecoder {
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let keyDecodingStrategy: KeyDecodingStrategy
let passthroughTypeResolver: StructureCodingPassthroughTypeResolver.Type
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -1184,6 +1208,7 @@ public class FirebaseDataDecoder {
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
passthroughTypeResolver: passthroughTypeResolver,
userInfo: userInfo)
}

Expand Down Expand Up @@ -1617,6 +1642,11 @@ fileprivate struct _JSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingCon
}

public func decode<T : Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
if type is StructureCodingUncodedUnkeyed.Type {
// Note: not pushing and popping key to codingPath since the key is
// not part of the decoded structure.
return try T.init(from: self.decoder)
}
guard let entry = self.container[key.stringValue] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key))."))
}
Expand Down Expand Up @@ -2505,12 +2535,12 @@ extension __JSONDecoder {
return try unbox_(value, as: type) as? T
}

fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
if type == Date.self || type == NSDate.self {
fileprivate func unbox_(_ value: Any, as _type: Decodable.Type) throws -> Any? {
if _type == Date.self || _type == NSDate.self {
return try self.unbox(value, as: Date.self)
} else if type == Data.self || type == NSData.self {
} else if _type == Data.self || _type == NSData.self {
return try self.unbox(value, as: Data.self)
} else if type == URL.self || type == NSURL.self {
} else if _type == URL.self || _type == NSURL.self {
guard let urlString = try self.unbox(value, as: String.self) else {
return nil
}
Expand All @@ -2520,14 +2550,17 @@ extension __JSONDecoder {
debugDescription: "Invalid URL string."))
}
return url
} else if type == Decimal.self || type == NSDecimalNumber.self {
} else if _type == Decimal.self || _type == NSDecimalNumber.self {
return try self.unbox(value, as: Decimal.self)
} else if let stringKeyedDictType = type as? _JSONStringDictionaryDecodableMarker.Type {
} else if let stringKeyedDictType = _type as? _JSONStringDictionaryDecodableMarker.Type {
return try self.unbox(value, as: stringKeyedDictType)
} else {
self.storage.push(container: value)
defer { self.storage.popContainer() }
return try type.init(from: self)
if self.options.passthroughTypeResolver.isPassthroughType(value) && type(of: value) == _type {
return value
}
return try _type.init(from: self)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions Firestore/Example/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ if is_platform(:ios)
target 'Firestore_IntegrationTests_iOS' do
inherit! :search_paths

pod 'FirebaseSharedSwift', :path => '../../'
pod 'FirebaseFirestoreSwift', :path => '../../'
pod 'GoogleBenchmark', :podspec => 'GoogleBenchmark.podspec'
pod 'GoogleTest', :podspec => 'GoogleTest.podspec'
Expand Down Expand Up @@ -152,6 +153,7 @@ if is_platform(:osx)
target 'Firestore_IntegrationTests_macOS' do
inherit! :search_paths

pod 'FirebaseSharedSwift', :path => '../../'
pod 'FirebaseFirestoreSwift', :path => '../../'
pod 'GoogleBenchmark', :podspec => 'GoogleBenchmark.podspec'
pod 'GoogleTest', :podspec => 'GoogleTest.podspec'
Expand Down Expand Up @@ -181,6 +183,7 @@ if is_platform(:tvos)
target 'Firestore_IntegrationTests_tvOS' do
inherit! :search_paths

pod 'FirebaseSharedSwift', :path => '../../'
pod 'FirebaseFirestoreSwift', :path => '../../'
pod 'GoogleBenchmark', :podspec => 'GoogleBenchmark.podspec'
pod 'GoogleTest', :podspec => 'GoogleTest.podspec'
Expand Down
11 changes: 11 additions & 0 deletions Firestore/Swift/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# 10.0.0
- [changed] `Firestore.Encoder` and `Firestore.Decoder` now wraps the shared `FirebaseDataEncoder` and `FirebaseDataDecoder` types which provides new customization options for encoding and decoding date to and from Firestore - similar to the options present on `JSONEncoder` and `JSONDecoder` from `Foundation`.
- [added] `Firestore.Encoder.KeyEncodingStrategy`
- [added] `Firestore.Encoder.DateEncodingStrategy`
- [added] `Firestore.Encoder.DataEncodingStrategy`
- [added] `Firestore.Encoder.NonConformingFloatEncodingStrategy`
- [added] `Firestore.Decoder.KeyDecodingStrategy`
- [added] `Firestore.Decoder.DateDecodingStrategy`
- [added] `Firestore.Decoder.DataDecodingStrategy`
- [added] `Firestore.Decoder.NonConformingFloatDecodingStrategy`

# 9.0.0
- [added] **Breaking change:** `FirebaseFirestoreSwift` has exited beta and is
now generally available for use.
Expand Down
15 changes: 9 additions & 6 deletions Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
*/

import Foundation
import FirebaseSharedSwift
import FirebaseFirestore

internal func isFirestorePassthroughType<T: Any>(_ value: T) -> Bool {
return
T.self == GeoPoint.self ||
T.self == Timestamp.self ||
T.self == FieldValue.self ||
T.self == DocumentReference.self
internal struct FirestorePassthroughTypes: StructureCodingPassthroughTypeResolver {
static func isPassthroughType<T>(_ t: T) -> Bool {
return
t is GeoPoint ||
t is Timestamp ||
t is FieldValue ||
t is DocumentReference
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public extension CollectionReference {
encoder: Firestore.Encoder = Firestore.Encoder(),
completion: ((Error?) -> Void)? = nil) throws
-> DocumentReference {
return addDocument(data: try encoder.encode(value), completion: completion)
let encoded = try encoder.encode(value)
return addDocument(data: encoded, completion: completion)
}
}
15 changes: 11 additions & 4 deletions Firestore/Swift/Source/Codable/DocumentID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/

import FirebaseFirestore
import FirebaseSharedSwift

let documentRefUserInfoKey =
CodingUserInfoKey(rawValue: "DocumentRefUserInfoKey")!

/// A type that can initialize itself from a Firestore `DocumentReference`,
/// which makes it suitable for use with the `@DocumentID` property wrapper.
Expand Down Expand Up @@ -76,7 +80,7 @@ internal protocol DocumentIDProtocol {
/// Firestore.Encoder leads to an error.
@propertyWrapper
public struct DocumentID<Value: DocumentIDWrappable & Codable>:
DocumentIDProtocol, Codable {
DocumentIDProtocol, Codable, StructureCodingUncodedUnkeyed {
var value: Value?

public init(wrappedValue value: Value?) {
Expand All @@ -101,9 +105,12 @@ public struct DocumentID<Value: DocumentIDWrappable & Codable>:
// MARK: - `Codable` implementation.

public init(from decoder: Decoder) throws {
throw FirestoreDecodingError.decodingIsNotSupported(
"DocumentID values can only be decoded with Firestore.Decoder"
)
guard let reference = decoder.userInfo[documentRefUserInfoKey] as? DocumentReference else {
throw FirestoreDecodingError.decodingIsNotSupported(
"Could not find DocumentReference for user info key: \(documentRefUserInfoKey)"
)
}
try self.init(from: reference)
}

public func encode(to encoder: Encoder) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public extension DocumentReference {
func setData<T: Encodable>(from value: T,
encoder: Firestore.Encoder = Firestore.Encoder(),
completion: ((Error?) -> Void)? = nil) throws {
setData(try encoder.encode(value), completion: completion)
let encoded = try encoder.encode(value)
setData(encoded, completion: completion)
}

/// Encodes an instance of `Encodable` and overwrites the encoded data
Expand All @@ -57,7 +58,8 @@ public extension DocumentReference {
merge: Bool,
encoder: Firestore.Encoder = Firestore.Encoder(),
completion: ((Error?) -> Void)? = nil) throws {
setData(try encoder.encode(value), merge: merge, completion: completion)
let encoded = try encoder.encode(value)
setData(encoded, merge: merge, completion: completion)
}

/// Encodes an instance of `Encodable` and writes the encoded data to the document referred
Expand All @@ -84,6 +86,7 @@ public extension DocumentReference {
mergeFields: [Any],
encoder: Firestore.Encoder = Firestore.Encoder(),
completion: ((Error?) -> Void)? = nil) throws {
setData(try encoder.encode(value), mergeFields: mergeFields, completion: completion)
let encoded = try encoder.encode(value)
setData(encoded, mergeFields: mergeFields, completion: completion)
}
}
Loading

0 comments on commit 65f6cfa

Please sign in to comment.