Skip to content

Commit

Permalink
Rewrite SelfDocumentID as a property wrapper (#4031)
Browse files Browse the repository at this point in the history
* Rewrite SelfDocumentID as a property wrapper

Make wrapped types conform to DocumentReferenceConvertible and use that
protocol to implement the conversion.

* Change DocumentReferenceConvertible to DocumentIDWrappable

* Make ServerTimestampWrappable use static functions

Use the same form as DocumentIDWrappable.

* Update extension String: ServerTimestampWrappable

* Use #if compiler, not #if swift

The FirebaseFirestoreSwift module is declared as conforming to Swift
4.0 so #if swift is still false, even when compiling with Swift 5.1.
Using #if compiler will see the actual compiler version. Property
wrappers don't depend on runtime features, so this works.

Also, allow Wrappable protocols to throw during conversions.
  • Loading branch information
wilhuff committed Oct 15, 2019
1 parent e88b52f commit 53e9186
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 171 deletions.
120 changes: 120 additions & 0 deletions Firestore/Swift/Source/Codable/DocumentID.swift
@@ -0,0 +1,120 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import FirebaseFirestore

#if compiler(>=5.1)
/// A type that can initialize itself from a Firestore `DocumentReference`,
/// which makes it suitable for use with the `@DocumentID` property wrapper.
///
/// Firestore includes extensions that make `String` and `DocumentReference`
/// conform to `DocumentIDWrappable`.
///
/// Note that Firestore ignores fields annotated with `@DocumentID` when writing
/// so there is no requirement to convert from the wrapped type back to a
/// `DocumentReference`.
public protocol DocumentIDWrappable {
/// Creates a new instance by converting from the given `DocumentReference`.
static func wrap(_ documentReference: DocumentReference) throws -> Self
}

extension String: DocumentIDWrappable {
public static func wrap(_ documentReference: DocumentReference) throws -> Self {
return documentReference.documentID
}
}

extension DocumentReference: DocumentIDWrappable {
public static func wrap(_ documentReference: DocumentReference) throws -> Self {
// Swift complains that values of type DocumentReference cannot be returned
// as Self which is nonsensical. The cast forces this to work.
return documentReference as! Self
}
}

/// An internal protocol that allows Firestore.Decoder to test if a type is a
/// DocumentID of some kind without knowing the specific generic parameter that
/// the user actually used.
///
/// This is required because Swift does not define an existential type for all
/// instances of a generic class--that is, it has no wildcard or raw type that
/// matches a generic without any specific parameter. Swift does define an
/// existential type for protocols though, so this protocol (to which DocumentID
/// conforms) indirectly makes it possible to test for and act on any
/// `DocumentID<Value>`.
internal protocol DocumentIDProtocol {
/// Initializes the DocumentID from a DocumentReference.
init(from documentReference: DocumentReference?) throws
}

/// A value that is populated in Codable objects with the `DocumentReference`
/// of the current document by the Firestore.Decoder when a document is read.
///
/// If the field name used for this type conflicts with a read document field,
/// an error is thrown. For example, if a custom object has a field `firstName`
/// annotated with `@DocumentID`, and there is a property from the document
/// named `firstName` as well, an error is thrown when you try to read the
/// document.
///
/// When writing a Codable object containing an `@DocumentID` annotated field,
/// its value is ignored. This allows you to read a document from one path and
/// write it into another without adjusting the value here.
///
/// NOTE: Trying to encode/decode this type using encoders/decoders other than
/// Firestore.Encoder leads to an error.
@propertyWrapper
public struct DocumentID<Value: DocumentIDWrappable & Codable & Equatable>:
DocumentIDProtocol, Codable, Equatable {
var value: Value?

public init(wrappedValue value: Value?) {
self.value = value
}

public var wrappedValue: Value? {
get { value }
set { value = newValue }
}

// MARK: - `DocumentIDProtocol` conformance

public init(from documentReference: DocumentReference?) throws {
if let documentReference = documentReference {
value = try Value.wrap(documentReference)
} else {
value = nil
}
}

// MARK: - `Codable` implementation.

public init(from decoder: Decoder) throws {
throw FirestoreDecodingError.decodingIsNotSupported(
"DocumentID values can only be decoded with Firestore.Decoder"
)
}

public func encode(to encoder: Encoder) throws {
throw FirestoreEncodingError.encodingIsNotSupported(
"DocumentID values can only be encoded with Firestore.Encoder"
)
}

public static func == (lhs: DocumentID<Value>, rhs: DocumentID<Value>) -> Bool {
return lhs.value == rhs.value
}
}
#endif // compiler(>=5.1)
4 changes: 2 additions & 2 deletions Firestore/Swift/Source/Codable/ExplicitNull.swift
Expand Up @@ -16,7 +16,7 @@

import FirebaseFirestore

#if swift(>=5.1)
#if compiler(>=5.1)
/// Wraps an `Optional` field in a `Codable` object such that when the field
/// has a `nil` value it will encode to a null value in Firestore. Normally,
/// optional fields are omitted from the encoded document.
Expand Down Expand Up @@ -60,7 +60,7 @@ import FirebaseFirestore
}
}
}
#endif // swift(>=5.1)
#endif // compiler(>=5.1)

/// A compatibility version of `ExplicitNull` that does not use property
/// wrappers, suitable for use in older versions of Swift.
Expand Down
74 changes: 0 additions & 74 deletions Firestore/Swift/Source/Codable/SelfDocumentId.swift

This file was deleted.

36 changes: 18 additions & 18 deletions Firestore/Swift/Source/Codable/ServerTimestamp.swift
Expand Up @@ -16,7 +16,7 @@

import FirebaseFirestore

#if swift(>=5.1)
#if compiler(>=5.1)
/// A type that can initialize itself from a Firestore Timestamp, which makes
/// it suitable for use with the `@ServerTimestamp` property wrapper.
///
Expand All @@ -26,42 +26,42 @@ import FirebaseFirestore
/// Creates a new instance by converting from the given `Timestamp`.
///
/// - Parameter timestamp: The timestamp from which to convert.
init(from timestamp: Timestamp)
static func wrap(_ timestamp: Timestamp) throws -> Self

/// Converts this value into a Firestore `Timestamp`.
///
/// - Returns: A `Timestamp` representation of this value.
func timestampValue() -> Timestamp
static func unwrap(_ value: Self) throws -> Timestamp
}

extension Date: ServerTimestampWrappable {
init(from timestamp: Timestamp) {
self = timestamp.dateValue()
public static func wrap(_ timestamp: Timestamp) throws -> Self {
return timestamp.dateValue()
}

func timestampValue() -> Timestamp {
return Timestamp(date: self)
public static func unwrap(_ value: Self) throws -> Timestamp {
return Timestamp(date: value)
}
}

extension NSDate: ServerTimestampWrappable {
init(from timestamp: Timestamp) {
public static func wrap(_ timestamp: Timestamp) throws -> Self {
let interval = timestamp.dateValue().timeIntervalSince1970
self = NSDate(timeIntervalSince1970: interval)
return NSDate(timeIntervalSince1970: interval) as! Self
}

func timestampValue() -> Timestamp {
return Timestamp(date: self)
public static func unwrap(_ value: NSDate) throws -> Timestamp {
return Timestamp(date: value as Date)
}
}

extension Timestamp: ServerTimestampWrappable {
init(from timestamp: Timestamp) {
self = timestamp
public static func wrap(_ timestamp: Timestamp) throws -> Self {
return timestamp as! Self
}

func timestampValue() -> Timestamp {
return self
public static func unwrap(_ value: Timestamp) throws -> Timestamp {
return value
}
}

Expand Down Expand Up @@ -100,20 +100,20 @@ import FirebaseFirestore
if container.decodeNil() {
value = nil
} else {
value = Value(from: try container.decode(Timestamp.self))
value = try Value.wrap(try container.decode(Timestamp.self))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let value = value {
try container.encode(value.timestampValue())
try container.encode(Value.unwrap(value))
} else {
try container.encode(FieldValue.serverTimestamp())
}
}
}
#endif // swift(>=5.1)
#endif // compiler(>=5.1)

/// A compatibility version of `ServerTimestamp` that does not use property
/// wrappers, suitable for use in older versions of Swift.
Expand Down

0 comments on commit 53e9186

Please sign in to comment.