Skip to content

Commit 7235f8f

Browse files
authored
Rewrite ExplicitNull as a propertyWrapper (#3954)
Add Swift4ExplicitNull that behaves as before.
1 parent ca8570a commit 7235f8f

File tree

3 files changed

+108
-12
lines changed

3 files changed

+108
-12
lines changed

Firestore/Swift/Source/Codable/ExplicitNull.swift

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,65 @@
1616

1717
import FirebaseFirestore
1818

19-
/// Wraps around a `Optional` such that it explicitly sets the corresponding document field
20-
/// to Null, instead of not setting the field at all.
19+
#if swift(>=5.1)
20+
21+
/// Wraps an `Optional` field in a `Codable` object such that when the field has
22+
/// a `nil` value it will encode to a null value in Firestore. Normally,
23+
/// optional fields are omitted from the encoded document.
24+
///
25+
/// This is useful for ensuring a field is present in a Firestore document, even
26+
/// when there is no associated value.
27+
@propertyWrapper
28+
public struct ExplicitNull<Value> {
29+
var value: Value?
30+
31+
public init(wrappedValue value: Value?) {
32+
self.value = value
33+
}
34+
35+
public var wrappedValue: Value? {
36+
get { value }
37+
set { value = newValue }
38+
}
39+
}
40+
41+
extension ExplicitNull: Equatable where Value: Equatable {}
42+
43+
extension ExplicitNull: Encodable where Value: Encodable {
44+
public func encode(to encoder: Encoder) throws {
45+
var container = encoder.singleValueContainer()
46+
if let value = value {
47+
try container.encode(value)
48+
} else {
49+
try container.encodeNil()
50+
}
51+
}
52+
}
53+
54+
extension ExplicitNull: Decodable where Value: Decodable {
55+
public init(from decoder: Decoder) throws {
56+
let container = try decoder.singleValueContainer()
57+
if container.decodeNil() {
58+
value = nil
59+
} else {
60+
value = try container.decode(Value.self)
61+
}
62+
}
63+
}
64+
65+
#endif // swift(>=5.1)
66+
67+
/// A compatibility version of `ExplicitNull` that does not use property
68+
/// wrappers, suitable for use in older versions of Swift.
2169
///
22-
/// When encoded into a Firestore document by `Firestore.Encoder`, an `Optional` field with
23-
/// `nil` value will be skipped, so the resulting document simply will not have the field.
70+
/// Wraps an `Optional` field in a `Codable` object such that when the field has
71+
/// a `nil` value it will encode to a null value in Firestore. Normally,
72+
/// optional fields are omitted from the encoded document.
2473
///
25-
/// When setting the field to `Null` instead of skipping it is desired, `ExplicitNull` can be
26-
/// used instead of `Optional`.
27-
public enum ExplicitNull<Wrapped> {
74+
/// This is useful for ensuring a field is present in a Firestore document, even
75+
/// when there is no associated value.
76+
@available(swift, deprecated: 5.1)
77+
public enum Swift4ExplicitNull<Wrapped> {
2878
case none
2979
case some(Wrapped)
3080

@@ -49,9 +99,11 @@ public enum ExplicitNull<Wrapped> {
4999
}
50100
}
51101

52-
extension ExplicitNull: Equatable where Wrapped: Equatable {}
102+
@available(swift, deprecated: 5.1)
103+
extension Swift4ExplicitNull: Equatable where Wrapped: Equatable {}
53104

54-
extension ExplicitNull: Encodable where Wrapped: Encodable {
105+
@available(swift, deprecated: 5.1)
106+
extension Swift4ExplicitNull: Encodable where Wrapped: Encodable {
55107
public func encode(to encoder: Encoder) throws {
56108
var container = encoder.singleValueContainer()
57109
switch self {
@@ -63,7 +115,8 @@ extension ExplicitNull: Encodable where Wrapped: Encodable {
63115
}
64116
}
65117

66-
extension ExplicitNull: Decodable where Wrapped: Decodable {
118+
@available(swift, deprecated: 5.1)
119+
extension Swift4ExplicitNull: Decodable where Wrapped: Decodable {
67120
public init(from decoder: Decoder) throws {
68121
let container = try decoder.singleValueContainer()
69122
if container.decodeNil() {

Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,24 @@ class FirestoreEncoderTests: XCTestCase {
478478
.decodes(to: Model(timestamp: .pending))
479479
}
480480

481+
#if swift(>=5.1)
481482
func testExplicitNull() throws {
482483
struct Model: Codable, Equatable {
483-
var name: ExplicitNull<String>
484+
@ExplicitNull var name: String?
485+
}
486+
487+
assertThat(Model(name: nil))
488+
.roundTrips(to: ["name": NSNull()])
489+
490+
assertThat(Model(name: "good name"))
491+
.roundTrips(to: ["name": "good name"])
492+
}
493+
#endif // swift(>=5.1)
494+
495+
@available(swift, deprecated: 5.1)
496+
func testSwift4ExplicitNull() throws {
497+
struct Model: Codable, Equatable {
498+
var name: Swift4ExplicitNull<String>
484499
}
485500

486501
assertThat(Model(name: .none))

Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,38 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
144144
}
145145
}
146146

147+
#if swift(>=5.1)
147148
func testExplicitNull() throws {
148149
struct Model: Encodable {
149150
var name: String
150-
var explicitNull: ExplicitNull<String>
151+
@ExplicitNull var explicitNull: String?
152+
var optional: String?
153+
}
154+
let model = Model(
155+
name: "name",
156+
explicitNull: nil,
157+
optional: nil
158+
)
159+
160+
let docToWrite = documentRef()
161+
162+
for flavor in allFlavors {
163+
try setData(from: model, forDocument: docToWrite, withFlavor: flavor)
164+
165+
let data = readDocument(forRef: docToWrite).data()
166+
167+
XCTAssertTrue(data!.keys.contains("explicitNull"), "Failed with flavor \(flavor)")
168+
XCTAssertEqual(data!["explicitNull"] as! NSNull, NSNull(), "Failed with flavor \(flavor)")
169+
XCTAssertFalse(data!.keys.contains("optional"), "Failed with flavor \(flavor)")
170+
}
171+
}
172+
#endif // swift(>=5.1)
173+
174+
@available(swift, deprecated: 5.1)
175+
func testSwift4ExplicitNull() throws {
176+
struct Model: Encodable {
177+
var name: String
178+
var explicitNull: Swift4ExplicitNull<String>
151179
var optional: String?
152180
}
153181
let model = Model(

0 commit comments

Comments
 (0)