Skip to content

Commit 53e9186

Browse files
authored
Rewrite SelfDocumentID as a property wrapper (#4031)
* 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.
1 parent e88b52f commit 53e9186

File tree

8 files changed

+235
-171
lines changed

8 files changed

+235
-171
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import FirebaseFirestore
18+
19+
#if compiler(>=5.1)
20+
/// A type that can initialize itself from a Firestore `DocumentReference`,
21+
/// which makes it suitable for use with the `@DocumentID` property wrapper.
22+
///
23+
/// Firestore includes extensions that make `String` and `DocumentReference`
24+
/// conform to `DocumentIDWrappable`.
25+
///
26+
/// Note that Firestore ignores fields annotated with `@DocumentID` when writing
27+
/// so there is no requirement to convert from the wrapped type back to a
28+
/// `DocumentReference`.
29+
public protocol DocumentIDWrappable {
30+
/// Creates a new instance by converting from the given `DocumentReference`.
31+
static func wrap(_ documentReference: DocumentReference) throws -> Self
32+
}
33+
34+
extension String: DocumentIDWrappable {
35+
public static func wrap(_ documentReference: DocumentReference) throws -> Self {
36+
return documentReference.documentID
37+
}
38+
}
39+
40+
extension DocumentReference: DocumentIDWrappable {
41+
public static func wrap(_ documentReference: DocumentReference) throws -> Self {
42+
// Swift complains that values of type DocumentReference cannot be returned
43+
// as Self which is nonsensical. The cast forces this to work.
44+
return documentReference as! Self
45+
}
46+
}
47+
48+
/// An internal protocol that allows Firestore.Decoder to test if a type is a
49+
/// DocumentID of some kind without knowing the specific generic parameter that
50+
/// the user actually used.
51+
///
52+
/// This is required because Swift does not define an existential type for all
53+
/// instances of a generic class--that is, it has no wildcard or raw type that
54+
/// matches a generic without any specific parameter. Swift does define an
55+
/// existential type for protocols though, so this protocol (to which DocumentID
56+
/// conforms) indirectly makes it possible to test for and act on any
57+
/// `DocumentID<Value>`.
58+
internal protocol DocumentIDProtocol {
59+
/// Initializes the DocumentID from a DocumentReference.
60+
init(from documentReference: DocumentReference?) throws
61+
}
62+
63+
/// A value that is populated in Codable objects with the `DocumentReference`
64+
/// of the current document by the Firestore.Decoder when a document is read.
65+
///
66+
/// If the field name used for this type conflicts with a read document field,
67+
/// an error is thrown. For example, if a custom object has a field `firstName`
68+
/// annotated with `@DocumentID`, and there is a property from the document
69+
/// named `firstName` as well, an error is thrown when you try to read the
70+
/// document.
71+
///
72+
/// When writing a Codable object containing an `@DocumentID` annotated field,
73+
/// its value is ignored. This allows you to read a document from one path and
74+
/// write it into another without adjusting the value here.
75+
///
76+
/// NOTE: Trying to encode/decode this type using encoders/decoders other than
77+
/// Firestore.Encoder leads to an error.
78+
@propertyWrapper
79+
public struct DocumentID<Value: DocumentIDWrappable & Codable & Equatable>:
80+
DocumentIDProtocol, Codable, Equatable {
81+
var value: Value?
82+
83+
public init(wrappedValue value: Value?) {
84+
self.value = value
85+
}
86+
87+
public var wrappedValue: Value? {
88+
get { value }
89+
set { value = newValue }
90+
}
91+
92+
// MARK: - `DocumentIDProtocol` conformance
93+
94+
public init(from documentReference: DocumentReference?) throws {
95+
if let documentReference = documentReference {
96+
value = try Value.wrap(documentReference)
97+
} else {
98+
value = nil
99+
}
100+
}
101+
102+
// MARK: - `Codable` implementation.
103+
104+
public init(from decoder: Decoder) throws {
105+
throw FirestoreDecodingError.decodingIsNotSupported(
106+
"DocumentID values can only be decoded with Firestore.Decoder"
107+
)
108+
}
109+
110+
public func encode(to encoder: Encoder) throws {
111+
throw FirestoreEncodingError.encodingIsNotSupported(
112+
"DocumentID values can only be encoded with Firestore.Encoder"
113+
)
114+
}
115+
116+
public static func == (lhs: DocumentID<Value>, rhs: DocumentID<Value>) -> Bool {
117+
return lhs.value == rhs.value
118+
}
119+
}
120+
#endif // compiler(>=5.1)

Firestore/Swift/Source/Codable/ExplicitNull.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import FirebaseFirestore
1818

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

6565
/// A compatibility version of `ExplicitNull` that does not use property
6666
/// wrappers, suitable for use in older versions of Swift.

Firestore/Swift/Source/Codable/SelfDocumentId.swift

Lines changed: 0 additions & 74 deletions
This file was deleted.

Firestore/Swift/Source/Codable/ServerTimestamp.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import FirebaseFirestore
1818

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

3131
/// Converts this value into a Firestore `Timestamp`.
3232
///
3333
/// - Returns: A `Timestamp` representation of this value.
34-
func timestampValue() -> Timestamp
34+
static func unwrap(_ value: Self) throws -> Timestamp
3535
}
3636

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

42-
func timestampValue() -> Timestamp {
43-
return Timestamp(date: self)
42+
public static func unwrap(_ value: Self) throws -> Timestamp {
43+
return Timestamp(date: value)
4444
}
4545
}
4646

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

53-
func timestampValue() -> Timestamp {
54-
return Timestamp(date: self)
53+
public static func unwrap(_ value: NSDate) throws -> Timestamp {
54+
return Timestamp(date: value as Date)
5555
}
5656
}
5757

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

63-
func timestampValue() -> Timestamp {
64-
return self
63+
public static func unwrap(_ value: Timestamp) throws -> Timestamp {
64+
return value
6565
}
6666
}
6767

@@ -100,20 +100,20 @@ import FirebaseFirestore
100100
if container.decodeNil() {
101101
value = nil
102102
} else {
103-
value = Value(from: try container.decode(Timestamp.self))
103+
value = try Value.wrap(try container.decode(Timestamp.self))
104104
}
105105
}
106106

107107
public func encode(to encoder: Encoder) throws {
108108
var container = encoder.singleValueContainer()
109109
if let value = value {
110-
try container.encode(value.timestampValue())
110+
try container.encode(Value.unwrap(value))
111111
} else {
112112
try container.encode(FieldValue.serverTimestamp())
113113
}
114114
}
115115
}
116-
#endif // swift(>=5.1)
116+
#endif // compiler(>=5.1)
117117

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

0 commit comments

Comments
 (0)