Skip to content

Commit e303599

Browse files
authored
Added the ability to customize encoding and decoding dates. (#54)
* Added the ability to customize encoding and decoding dates when parking or retrieving Codable objects. * Split out SwiftCodableTests into separate files to significantly reduce file size. * Fixed comment formatting. * Updated comments and documentation to match the recent changes.
1 parent 46550ce commit e303599

File tree

7 files changed

+54
-20
lines changed

7 files changed

+54
-20
lines changed

Sources/GarageStorage/Garage.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,23 @@ public class Garage: NSObject {
3333

3434
/// Autosave is set to true by default, for every operation that causes a change to the underlying Core Data Managed Object Context.
3535
///
36-
/// When set to true, the garage will be saved after any operation that causes a change to the underlying Core Data Managed Object Context, including `park()`, `setSyncStatus()`, and `delete()`. When set to false, `save()` must be called instead, in order to persist those changes. You might want to use `withAutosaveDisabled(:)` to set this to false, in order to perform batch changes to many objects before saving them all, optimizing performance.
36+
/// When set to true, the garage will be saved after any operation that causes a change to the underlying Core Data Managed Object Context, such as `park()`. When set to false, `save()` must be called instead, in order to persist those changes.
37+
///
38+
/// Use ``withAutosaveDisabled(_:)`` to set this to false, in order to perform batch changes to many objects before saving them all, optimizing performance.
3739
@objc(autosaveEnabled)
38-
public var isAutosaveEnabled = true
40+
public private(set) var isAutosaveEnabled = true
41+
42+
/// The date decoding strategy used by JSONDecoder when decoding objects.
43+
///
44+
/// By default, this is set to a custom strategy that uses `Date.isoFormatter`f or Codable objects and a wrapper conversion check for backward compatibility with Objective-C `MappableObject`.
45+
/// You can modify this to apply a different date decoding strategy for a `retrieve()` call using ``withDateDecodingStrategy(_:_:)``.
46+
public private(set) var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .custom(decodeTransformableDate)
47+
48+
/// The date encoding strategy used by JSONEncoder when encoding objects.
49+
///
50+
/// By default, this is set to use ISO 8601 formatted dates via `Date.isoFormatter`.
51+
/// You can modify this to apply a different date encoding strategy for a `park()` call using ``withDateEncodingStrategy(_:_:)``.
52+
public private(set) var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .formatted(Date.isoFormatter)
3953

4054
/// The date decoding strategy used by JSONDecoder when decoding objects.
4155
///
@@ -149,6 +163,8 @@ public class Garage: NSObject {
149163
///
150164
/// This is useful for performing batch operations where you want to defer saving until all changes are complete. The autosave state is automatically restored even if the closure throws an error.
151165
///
166+
/// - note: You must call ``save()`` directly at some point after executing one or more closures in this manner.
167+
///
152168
/// - parameter closure: A closure to execute with autosave disabled. If the closure throws, the error is propagated after restoring the autosave state.
153169
/// - throws: Rethrows any error thrown by the closure.
154170
///

Sources/GarageStorage/GarageStorage.docc/GarageStorage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ GarageStorage is designed to do two things:
1616

1717
The ``Garage`` is the main object that coordinates activity in GarageStorage. It's called a *garage* because you can park pretty much anything in it, like, you know, a garage. The `Garage` handles the backing Core Data stack, as well as the saving and retrieving of data. You *park* objects in the `Garage`, and *retrieve* them later.
1818

19-
Any object going into or coming out of the `Garage` must conform to the `Codable` protocol. If it's a top-level object, it must also conform to either the `Hashable` protocol, or GarageStorage's ``Mappable`` protocol. You can add whatever type of conforming object you like to the `Garage`, whenever you like. You don't have to migrate data models or anything, just park whatever you want!
19+
Any object going into or coming out of the `Garage` must conform to the `Codable` protocol. If it's a top-level object, it must also conform to either the `Hashable` protocol, or the `Identifiable` protocol. You can add whatever type of conforming object you like to the `Garage`, whenever you like. You don't have to migrate data models or anything, just park whatever you want!
2020

2121
## Topics
2222

Sources/GarageStorage/GarageStorage.docc/GettingStarted.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ To start working with GarageStorage, you create a `Garage`, then start parking a
88

99
### How does a Garage work?
1010

11-
Anything going into or coming out of the Garage must conform to the `Codable` protocol. Top-level objects—that is, anything stored or retrieved directly— must also conform to either the `Hashable` protocol, or GarageStorage's ``Mappable`` protocol (which is `Codable` and `Identifiable where ID == String`) for uniquely identified top-level objects. A top-level object can be stored or retrieved using its unique `id`, or as part of an array of the same type.
11+
Anything going into or coming out of the Garage must conform to the `Codable` protocol. Top-level objects—that is, anything stored or retrieved directly— must also conform to either the `Hashable` protocol, or `Identifiable` protocol for uniquely identified top-level objects. A top-level object can be stored or retrieved using its unique `id`, or as part of an array of the same type.
1212

1313
It's important to draw a distinction between how Garage Storage operates and how Core Data operates: Garage Storage stores a JSON representation of your objects in Core Data, as opposed to storing the objects themselves, as Core Data does. There are some implications to this (explained below), but the best part is that you can add whatever type of object you like to the Garage, whenever you like. You don't have to migrate data models or anything, just park whatever you want!
1414

@@ -46,7 +46,7 @@ In order to store this in GarageStorage, conform it to Codable:
4646
```swift
4747
extension Address: Codable { }
4848
```
49-
If this type is only embedded in another object, then no additional work is required. However, in order to *park* this object as a top-level object (that is, directly using `park(_:)` or `parkAll(_:)`), it must additionally conform to the either the `Hashable` protocol or the GarageStorage `Mappable` protocol, which combines `Codable` conformance with a `String` `id` property (see next section).
49+
If this type is only embedded in another object, then no additional work is required. However, in order to *park* this object as a top-level object (that is, directly using `park(_:)` or `parkAll(_:)`), it must additionally conform to the either the `Hashable` protocol or the `Identifiable` protocol (see next section).
5050

5151
### What making an object Hashable does
5252

@@ -61,10 +61,10 @@ If many references of this type with the same value are being embedded (e.g., mu
6161

6262
As indicated in the previous section, a top-level object or elements of a top-level array need only conform to `Hashable`. They may be either value or reference types.
6363

64-
Standalone top-level objects—that is, root objects that are parked directly using `park()`—often require being uniquely identified in the Garage, and are often reference types, such as classes. This is supported through the `Mappable` protocol, which combines `Codable` conformance with an `String` ID property. For example:
64+
Standalone top-level objects—that is, root objects that are parked directly using `park()`—often require being uniquely identified in the Garage, and are often reference types, such as classes. This is supported through the `Identifiable` protocol. The only requirement for `Identifiable` is that its ID conform to `LosslessStringConvertible`. For example:
6565

6666
```swift
67-
class Person: Mappable {
67+
class Person: Codable, Identifiable {
6868
// Map the identifier to a preferred property, if desired.
6969
var id: String { name }
7070

@@ -78,14 +78,28 @@ class Person: Mappable {
7878
}
7979
```
8080

81-
Note that in the above example, another property, `name`, is mapped to the `id` property, and the `id` property itself is synthesized (i.e., not stored directly with the object). In general, this is how you would map an existing unique identifier of any type (such as from a server or remote storage), to the one required for the Garage.
81+
Note that in the above example, another property, `name`, is mapped to the `id` property, and the `id` property itself is synthesized (i.e., not stored directly with the object). In general, this is how you might map an existing unique identifier of any type (such as from a server or remote storage), to the one required for the Garage.
82+
83+
Note: Many Swift types support the `LosslessStringConvertible` protocol requirement for `Identifiable`'s `ID`, including `String` (of course), `Int`, `Double`, etc. However, `UUID` does not support this directly. To add conformance for `UUID`, implement the following in your project:
84+
85+
```swift
86+
import Foundation
87+
88+
extension UUID: LosslessStringConvertible {
89+
public init?(_ description: String) {
90+
self.init(uuidString: description)
91+
}
92+
93+
public var description: String { self.uuidString }
94+
}
95+
```
8296

8397
### Parking Objects
84-
Parking an object puts a snapshot of that object into the Garage. As mentioned earlier, this is different from pure Core Data, where changes to your `NSManagedObjects` are directly reflected in the managed object context. With GarageStorage, since you're parking a snapshot, *you will need to park that object any time you want changes you've made to it to be reflected/persisted.* You can park the same object multiple times, which will update the existing object of that same type and identifier. To park a `Mappable` object in the garage, call:
98+
Parking an object puts a snapshot of that object into the Garage. As mentioned earlier, this is different from pure Core Data, where changes to your `NSManagedObjects` are directly reflected in the managed object context. With GarageStorage, since you're parking a snapshot, *you will need to park that object any time you want changes you've made to it to be reflected/persisted.* You can park the same object multiple times, which will update the existing object of that same type and identifier. To park an `Identifiable` object in the garage, call:
8599
```swift
86100
try garage.park(myPerson)
87101
```
88-
You may also park an array of objects in the garage (assuming all are `Codable` and `Hashable` or `Mappable` and of the same type):
102+
You may also park an array of objects in the garage (assuming all are `Codable` and `Hashable` or `Identifiable` and of the same type):
89103
```swift
90104
try garage.parkAll([myBrother, mySister, myMom, myDad])
91105
```
@@ -101,7 +115,7 @@ You can also retrieve all objects for a given type:
101115
```
102116

103117
### Deleting Objects
104-
To delete an object from the Garage, you must specify the mappable object that was originally parked:
118+
To delete an object from the Garage, you must specify the Identifiable object that was originally parked:
105119
```swift
106120
try garage.delete(myPerson)
107121
```
@@ -153,15 +167,15 @@ Parking, deleting, or modifying the sync status of objects will automatically pe
153167
```
154168

155169
### A Note about Identifying Objects
156-
It's worth going into a bit of detail about how *identified*, *unidentified*, and *anonymous* types work with respect to *top-level* vs. *embedded* objects, so you can best leverage (read: account for the quirks of) Garage Storage.
170+
It's worth going into a bit of detail about how *identified*, *unidentified*, and *anonymous* types work with respect to *top-level* vs. *embedded* objects, so you can best leverage (read: *"account for the quirks of"*) Garage Storage.
157171

158-
Any Mappable object with an *id* or identifying attribute will be stored as its own separate object in the Garage, and each *reference* will point back to that object. This is great if you have a bunch of objects that reference each other, as the graph is properly maintained in the garage, so a change to one object will be "seen" by the other objects pointing to it. This also enables you to *retrieve* any top-level object by its identifier.
172+
Any `Identifiable` object with an *id* attribute will be stored as its own separate object in the Garage, and each *reference* will point back to that object. This is great if you have a bunch of objects that reference each other, as the graph is properly maintained in the garage, so a change to one object will be "seen" by the other objects pointing to it. This also enables you to *retrieve* any top-level object by its identifier.
159173

160-
If you instead conform the type to `Hashable` then there is still only one instance or value of its type in storage; however, it is now an *unidentified* object. If you park an unidentified *Object A*, then change one of its properties, and park *Object A* again, you'll now have *two different versions* of *Object A* in the Garage, as its hash value has changed. If *Object A* had had an identifier, then *Object A* would have just been updated when it was parked the second time. Therefore, it's considered a best practice for top-level reference types to conform to `Mappable` so that they always have an identifying attribute, and are treated as the same instance in storage.
174+
If you instead conform the type to `Hashable` then there is still only one instance or value of its type in storage; however, it is now an *unidentified* object. If you park an unidentified *Object A*, then change one of its properties, and park *Object A* again, you'll now have *two different versions* of *Object A* in the Garage, as its hash value has changed. If *Object A* had had an identifier, then *Object A* would have just been updated when it was parked the second time. Therefore, it's considered a best practice for top-level reference types to conform to `Identifiable` so that they always have an *id* attribute, and are treated as the same instance in storage.
161175

162-
However, if the object is an embedded *property* of a top-level object, you may want to leave it *unidentified*, especially if it doesn't have an attribute that's logically its identifier, or if it is a value type. If the object object does not conform to `Hashable`, then it is completely *anonymous*: it is serialized as in-line JSON, instead of having a separate underlying core data object, as a `Mappable` or `Hashable` object would. This means you won't be able to retrieve anonymous sub-objects by type directly. To make an object completely anonymous, it only needs to conform to `Codable`.
176+
However, if the object is an embedded *property* of a top-level object, you may want to leave it *unidentified*, especially if it doesn't have an attribute that's logically its identifier, or if it is a value type. If the object does not conform to `Hashable`, then it is completely *anonymous*: it is serialized as in-line JSON, instead of having a separate underlying core data object, as an `Identifiable` or `Hashable` object would. This means you won't be able to retrieve anonymous sub-objects by type directly. To make an object completely anonymous, it only needs to conform to `Codable`.
163177

164-
The primary advantages of *anonymous* objects are twofold: First, you don't have to arbitrarily pick an identifier if your object doesn't naturally have one. Second, there's an underlying difference in how deletion is handled. When you delete an object from the Garage, only the top level `Mappable` is deleted. If it references other `Mappable` or `Hashable` objects, those are not deleted. Garage Storage doesn't monitor retain counts on objects, so for safety, only the object specified is removed. However, since *anonymous* objects are part of the top level object's JSON, and are not separate underlying objects, they will be removed. It's considered best practice for embedded objects to be *anonymous*, unless there is a compelling reason otherwise, such as to reduce overall storage footprint.
178+
The primary advantages of *anonymous* objects are twofold: First, you don't have to arbitrarily pick an *identifier* if your object doesn't naturally have one, and you don't have to bother conforming it to `Hashable` either. Second, there's an underlying difference in how deletion is handled. When you delete an object from the Garage, only the top level `Identifiable` is deleted. If it references other `Identifiable` or `Hashable` objects, those sub-objects are not deleted. Garage Storage doesn't monitor retain counts on objects, so for safety, only the object specified is removed. However, since *anonymous* objects are part of its parent object's JSON, and are not separate underlying objects, they will be removed along with that top level object. It's considered best practice for embedded objects to be *anonymous*, unless there is a compelling reason otherwise, such as to reduce overall storage footprint.
165179

166180
### Handling errors
167181

Sources/GarageStorage/Interface/Mappable.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import Foundation
1010

1111
/// An optional, convenience protocol that conforms to `Codable` and `Identifiable` where `ID` conforms to `LosslessStringConvertible`.
12-
/// These two protocol conformances are required for uniquely identified objects in a Garage.
13-
/// Most clients can simply conform to `Codable` and `Identifiable` directly, to meet these requirements.
14-
/// This protocol is only required for backward compatibility with existing code already using Mappable.
12+
/// These two protocol conformances are required for parking and retrieving uniquely identified objects in a Garage.
13+
///
14+
/// - note: Most clients can simply conform to `Codable` and `Identifiable` directly, to meet these requirements.
15+
/// This protocol is only required for backward compatibility with existing code already using `Mappable`.
1516
public protocol Mappable: Codable, Identifiable where ID: LosslessStringConvertible { }

Sources/GarageStorage/Interface/MappableObject.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import Foundation
1010

1111

12-
/// A protocol for providing Objective-C Key-Value Coding mappings of properties for garage storage. Optional support is provided through ``ObjectMapping`` to uniquely identify top-level objects.
12+
/// A protocol for providing Objective-C Key-Value Coding mappings of properties for garage storage, with optional support is provided to uniquely identify top-level objects.
1313
///
1414
/// Properties must include the `@objc` keyword if declared in Swift. If the object must be uniquely identified in storage, the ``ObjectMapping/identifyingAttribute`` can be assigned.
1515
///
1616
/// Supported property types include:
1717
/// - Core types: `Int`, `Double`, `Bool`, `String`, `Date`
1818
/// - Container types: `Array`, `Dictionary`
1919
/// - Other ``MappableObject`` classes
20+
/// - note: This protocol is only required for Objective-C compatibility. For Swift projects, use `Codable`, and for top-level objects use `Hashable` or `Identifiable`.
2021
@objc(GSMappableObject)
2122
public protocol MappableObject : NSObjectProtocol {
2223

Sources/GarageStorage/Interface/ObjectMapping.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Foundation
1717
/// - Core types: `Int`, `Double`, `Bool`, `String`, `Date`
1818
/// - Container types: `Array`, `Dictionary`
1919
/// - Other ``MappableObject`` classes
20+
/// - note: This protocol is only required for Objective-C compatibility. For Swift projects, use `Codable`, and for top-level objects use `Hashable` or `Identifiable`.
2021
@objc(GSObjectMapping)
2122
public class ObjectMapping: NSObject {
2223

Sources/GarageStorage/Interface/SyncableObject.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Foundation
1212
/// An optional protocol for managing the web server sync status of an Objective-C ``MappableObject``.
1313
///
1414
/// This can be used to retrieve all objects of a specific class and sync status, eliminating the need to fetch all instances of a specific class then filtering on sync status.
15+
/// - note: This protocol is only required for Objective-C compatibility. For Swift projects, use `Syncable`.
1516
@objc(GSSyncableObject)
1617
public protocol SyncableObject: NSObjectProtocol {
1718

0 commit comments

Comments
 (0)