From 57d02ed7894b2531b0bc7e0d1653422a8d6eda67 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Thu, 6 Jun 2019 13:33:36 -0700 Subject: [PATCH] Make RealmOptional and List conform to Codable This makes it possible to serialize and deserialize them with the standard library's (en|de)coders such as JSONDecoder, and makes it so that Object subclasses with optional or list properties can have automatically synthesized Codable conformance. --- CHANGELOG.md | 2 + Realm.xcodeproj/project.pbxproj | 4 + RealmSwift/List.swift | 21 +++ RealmSwift/Optional.swift | 14 ++ RealmSwift/Results.swift | 11 ++ RealmSwift/Tests/CodableTests.swift | 220 ++++++++++++++++++++++++++++ 6 files changed, 272 insertions(+) create mode 100644 RealmSwift/Tests/CodableTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index caf553fc66..3ac375ee61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements * Add support for including Realm via Swift Package Manager. +* Add Codable conformance to RealmOptional and List, and Encodable conformance to Results. + ([PR #6172](https://github.com/realm/realm-cocoa/pull/6172)). ### Fixed * Attempting to observe an unmanaged LinkingObjects object crashed rather than diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index bc5335c57b..36ab6119fe 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -243,6 +243,7 @@ 3FB60BAD2040999300583735 /* SwiftPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB60BAC2040999300583735 /* SwiftPermissionsTests.swift */; }; 3FBEF67B1C63D66100F6935B /* RLMCollection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3FBEF6791C63D66100F6935B /* RLMCollection.mm */; }; 3FBEF67C1C63D66400F6935B /* RLMCollection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3FBEF6791C63D66100F6935B /* RLMCollection.mm */; }; + 3FCB1A7522A9B0A2003807FB /* CodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FCB1A7422A9B0A2003807FB /* CodableTests.swift */; }; 3FDCFEB619F6A8D3005E414A /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88C36FF19745E5500C9963D /* RLMSupport.swift */; }; 3FDE338D19C39A87003B7DBA /* RLMSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88C36FF19745E5500C9963D /* RLMSupport.swift */; }; 3FE5818622C2B4B900BA10E7 /* ObjectiveCSupport+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE5818422C2B4B900BA10E7 /* ObjectiveCSupport+Sync.swift */; }; @@ -841,6 +842,7 @@ 3FBD05FB1B94E1C3004559CF /* index_set.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = index_set.hpp; sourceTree = ""; }; 3FBEF6781C63D66100F6935B /* RLMCollection_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RLMCollection_Private.hpp; sourceTree = ""; }; 3FBEF6791C63D66100F6935B /* RLMCollection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMCollection.mm; sourceTree = ""; }; + 3FCB1A7422A9B0A2003807FB /* CodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableTests.swift; sourceTree = ""; }; 3FE556421B9A43E5002A1129 /* schema.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = schema.cpp; sourceTree = ""; }; 3FE556431B9A43E5002A1129 /* schema.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = schema.hpp; sourceTree = ""; }; 3FE5818322C2B4B900BA10E7 /* Nonsync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nonsync.swift; sourceTree = ""; }; @@ -1373,6 +1375,7 @@ isa = PBXGroup; children = ( 5D6610291BE98DAA0021E04F /* Supporting Files */, + 3FCB1A7422A9B0A2003807FB /* CodableTests.swift */, E8AE7C251EA436F800CDFF9A /* CompactionTests.swift */, 5D660FFF1BE98D880021E04F /* KVOTests.swift */, 5D6610001BE98D880021E04F /* ListTests.swift */, @@ -2490,6 +2493,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3FCB1A7522A9B0A2003807FB /* CodableTests.swift in Sources */, E8AE7C261EA436F800CDFF9A /* CompactionTests.swift in Sources */, 3FF3FFAF1F0D6D6400B84599 /* KVOTests.swift in Sources */, 5D6610161BE98D880021E04F /* ListTests.swift in Sources */, diff --git a/RealmSwift/List.swift b/RealmSwift/List.swift index 271d4bbebd..fc2b02de99 100644 --- a/RealmSwift/List.swift +++ b/RealmSwift/List.swift @@ -708,6 +708,27 @@ extension List: RangeReplaceableCollection { } #endif +// MARK: - Codable + +extension List: Decodable where Element: Decodable { + public convenience init(from decoder: Decoder) throws { + self.init() + var container = try decoder.unkeyedContainer() + while !container.isAtEnd { + append(try container.decode(Element.self)) + } + } +} + +extension List: Encodable where Element: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + for value in self { + try container.encode(value) + } + } +} + // MARK: - AssistedObjectiveCBridgeable extension List: AssistedObjectiveCBridgeable { diff --git a/RealmSwift/Optional.swift b/RealmSwift/Optional.swift index d3e585ca4c..12645690f4 100644 --- a/RealmSwift/Optional.swift +++ b/RealmSwift/Optional.swift @@ -64,3 +64,17 @@ public final class RealmOptional: RLMOptionalBase { self.value = value } } + +extension RealmOptional: Codable where Value: Codable { + public convenience init(from decoder: Decoder) throws { + self.init() + // `try decoder.singleValueContainer().decode(Value?.self)` incorrectly + // rejects null values: https://bugs.swift.org/browse/SR-7404 + let container = try decoder.singleValueContainer() + self.value = container.decodeNil() ? nil : try container.decode(Value.self) + } + + public func encode(to encoder: Encoder) throws { + try self.value.encode(to: encoder) + } +} diff --git a/RealmSwift/Results.swift b/RealmSwift/Results.swift index 9aecbdddf5..357c1cd213 100644 --- a/RealmSwift/Results.swift +++ b/RealmSwift/Results.swift @@ -418,3 +418,14 @@ extension Results: AssistedObjectiveCBridgeable { return (objectiveCValue: rlmResults, metadata: nil) } } + +// MARK: - Codable + +extension Results: Encodable where Element: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + for value in self { + try container.encode(value) + } + } +} diff --git a/RealmSwift/Tests/CodableTests.swift b/RealmSwift/Tests/CodableTests.swift new file mode 100644 index 0000000000..a43bd9cc75 --- /dev/null +++ b/RealmSwift/Tests/CodableTests.swift @@ -0,0 +1,220 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2019 Realm Inc. +// +// 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 XCTest +import RealmSwift + +final class CodableObject: Object, Codable { + @objc dynamic var string: String = "" + @objc dynamic var data: Data = Data() + @objc dynamic var date: Date = Date() + @objc dynamic var int: Int = 0 + @objc dynamic var int8: Int8 = 0 + @objc dynamic var int16: Int16 = 0 + @objc dynamic var int32: Int32 = 0 + @objc dynamic var int64: Int64 = 0 + @objc dynamic var float: Float = 0 + @objc dynamic var double: Double = 0 + @objc dynamic var bool: Bool = false + + @objc dynamic var stringOpt: String? + @objc dynamic var dataOpt: Data? + @objc dynamic var dateOpt: Date? + var intOpt = RealmOptional() + var int8Opt = RealmOptional() + var int16Opt = RealmOptional() + var int32Opt = RealmOptional() + var int64Opt = RealmOptional() + var floatOpt = RealmOptional() + var doubleOpt = RealmOptional() + var boolOpt = RealmOptional() + + var boolList = List() + var intList = List() + var int8List = List() + var int16List = List() + var int32List = List() + var int64List = List() + var floatList = List() + var doubleList = List() + var stringList = List() + var dataList = List() + var dateList = List() + + var boolOptList = List() + var intOptList = List() + var int8OptList = List() + var int16OptList = List() + var int32OptList = List() + var int64OptList = List() + var floatOptList = List() + var doubleOptList = List() + var stringOptList = List() + var dataOptList = List() + var dateOptList = List() +} + +class CodableTests: TestCase { + func decode(_ type: T.Type, _ str: String) -> RealmOptional { + let decoder = JSONDecoder() + return try! decoder.decode([RealmOptional].self, from: str.data(using: .utf8)!).first! + } + + func encode(_ value: T?) -> String { + let encoder = JSONEncoder() + let opt = RealmOptional() + opt.value = value + return try! String(data: encoder.encode([opt]), encoding: .utf8)! + } + + func testBool() { + XCTAssertEqual(true, decode(Bool.self, "[true]").value) + XCTAssertNil(decode(Bool.self, "[null]").value) + XCTAssertEqual(encode(true), "[true]") + XCTAssertEqual(encode(nil as Bool?), "[null]") + } + + func testInt() { + XCTAssertEqual(1, decode(Int.self, "[1]").value) + XCTAssertNil(decode(Int.self, "[null]").value) + XCTAssertEqual(encode(10), "[10]") + XCTAssertEqual(encode(nil as Int?), "[null]") + } + + func testFloat() { + XCTAssertEqual(2.2, decode(Float.self, "[2.2]").value) + XCTAssertNil(decode(Float.self, "[null]").value) + XCTAssertEqual(encode(2.25), "[2.25]") + XCTAssertEqual(encode(nil as Float?), "[null]") + } + + func testDouble() { + XCTAssertEqual(2.2, decode(Double.self, "[2.2]").value) + XCTAssertNil(decode(Double.self, "[null]").value) + XCTAssertEqual(encode(2.25), "[2.25]") + XCTAssertEqual(encode(nil as Double?), "[null]") + } + + func testObject() { + let str = """ + { + "bool": true, + "string": "abc", + "int": 123, + "int8": 123, + "int16": 123, + "int32": 123, + "int64": 123, + "float": 2.5, + "double": 2.5, + "date": 2.5, + "data": "\(Data("def".utf8).base64EncodedString())", + + "boolOpt": true, + "stringOpt": "abc", + "intOpt": 123, + "int8Opt": 123, + "int16Opt": 123, + "int32Opt": 123, + "int64Opt": 123, + "floatOpt": 2.5, + "doubleOpt": 2.5, + "dateOpt": 2.5, + "dataOpt": "\(Data("def".utf8).base64EncodedString())", + + "boolList": [true], + "stringList": ["abc"], + "intList": [123], + "int8List": [123], + "int16List": [123], + "int32List": [123], + "int64List": [123], + "floatList": [2.5], + "doubleList": [2.5], + "dateList": [2.5], + "dataList": ["\(Data("def".utf8).base64EncodedString())"], + + "boolOptList": [true], + "stringOptList": ["abc"], + "intOptList": [123], + "int8OptList": [123], + "int16OptList": [123], + "int32OptList": [123], + "int64OptList": [123], + "floatOptList": [2.5], + "doubleOptList": [2.5], + "dateOptList": [2.5], + "dataOptList": ["\(Data("def".utf8).base64EncodedString())"], + } + """ + let decoder = JSONDecoder() + let obj = try! decoder.decode(CodableObject.self, from: Data(str.utf8)) + + XCTAssertEqual(obj.bool, true) + XCTAssertEqual(obj.int, 123) + XCTAssertEqual(obj.int8, 123) + XCTAssertEqual(obj.int16, 123) + XCTAssertEqual(obj.int32, 123) + XCTAssertEqual(obj.int64, 123) + XCTAssertEqual(obj.float, 2.5) + XCTAssertEqual(obj.double, 2.5) + XCTAssertEqual(obj.string, "abc") + XCTAssertEqual(obj.date, Date(timeIntervalSinceReferenceDate: 2.5)) + XCTAssertEqual(obj.data, Data("def".utf8)) + + XCTAssertEqual(obj.boolOpt.value, true) + XCTAssertEqual(obj.intOpt.value, 123) + XCTAssertEqual(obj.int8Opt.value, 123) + XCTAssertEqual(obj.int16Opt.value, 123) + XCTAssertEqual(obj.int32Opt.value, 123) + XCTAssertEqual(obj.int64Opt.value, 123) + XCTAssertEqual(obj.floatOpt.value, 2.5) + XCTAssertEqual(obj.doubleOpt.value, 2.5) + XCTAssertEqual(obj.stringOpt, "abc") + XCTAssertEqual(obj.dateOpt, Date(timeIntervalSinceReferenceDate: 2.5)) + XCTAssertEqual(obj.dataOpt, Data("def".utf8)) + + XCTAssertEqual(obj.boolList.first, true) + XCTAssertEqual(obj.intList.first, 123) + XCTAssertEqual(obj.int8List.first, 123) + XCTAssertEqual(obj.int16List.first, 123) + XCTAssertEqual(obj.int32List.first, 123) + XCTAssertEqual(obj.int64List.first, 123) + XCTAssertEqual(obj.floatList.first, 2.5) + XCTAssertEqual(obj.doubleList.first, 2.5) + XCTAssertEqual(obj.stringList.first, "abc") + XCTAssertEqual(obj.dateList.first, Date(timeIntervalSinceReferenceDate: 2.5)) + XCTAssertEqual(obj.dataList.first, Data("def".utf8)) + + XCTAssertEqual(obj.boolOptList.first, true) + XCTAssertEqual(obj.intOptList.first, 123) + XCTAssertEqual(obj.int8OptList.first, 123) + XCTAssertEqual(obj.int16OptList.first, 123) + XCTAssertEqual(obj.int32OptList.first, 123) + XCTAssertEqual(obj.int64OptList.first, 123) + XCTAssertEqual(obj.floatOptList.first, 2.5) + XCTAssertEqual(obj.doubleOptList.first, 2.5) + XCTAssertEqual(obj.stringOptList.first, "abc") + XCTAssertEqual(obj.dateOptList.first, Date(timeIntervalSinceReferenceDate: 2.5)) + XCTAssertEqual(obj.dataOptList.first, Data("def".utf8)) + + let expected = "{\"int64Opt\":123,\"int\":123,\"intOptList\":[123],\"boolList\":[true],\"doubleList\":[2.5],\"dateList\":[2.5],\"int32OptList\":[123],\"dateOptList\":[2.5],\"int64OptList\":[123],\"doubleOptList\":[2.5],\"int64List\":[123],\"int8List\":[123],\"string\":\"abc\",\"dataOptList\":[\"ZGVm\"],\"intOpt\":123,\"double\":2.5,\"float\":2.5,\"int32Opt\":123,\"dateOpt\":2.5,\"boolOpt\":true,\"int16Opt\":123,\"stringList\":[\"abc\"],\"dataList\":[\"ZGVm\"],\"boolOptList\":[true],\"date\":2.5,\"int16\":123,\"data\":\"ZGVm\",\"stringOpt\":\"abc\",\"int32\":123,\"int16List\":[123],\"stringOptList\":[\"abc\"],\"dataOpt\":\"ZGVm\",\"int8OptList\":[123],\"int32List\":[123],\"int8\":123,\"int16OptList\":[123],\"intList\":[123],\"int8Opt\":123,\"floatOptList\":[2.5],\"floatOpt\":2.5,\"doubleOpt\":2.5,\"bool\":true,\"floatList\":[2.5],\"int64\":123}" + let encoder = JSONEncoder() + XCTAssertEqual(try! String(data: encoder.encode(obj), encoding: .utf8), expected) + } +}