Skip to content

Commit f7b5ba3

Browse files
authored
feat(datastore): add support for Date, DateTime and Time for DataStore fixes #278
**Notes:** AWS supports more granular definition for Date types while the Swift language represents all date values with the built-in `Date` struct. This change adds `DateTime` and `Time` for an improved and more permissive (de)serialization support. **References:** - #278
1 parent 59f6b18 commit f7b5ba3

File tree

53 files changed

+1660
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1660
-171
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 90 additions & 2 deletions
Large diffs are not rendered by default.

Amplify/Categories/DataStore/Model/Model+DateFormatting.swift

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,28 @@
88
import Foundation
99

1010
public struct ModelDateFormatting {
11-
static let iso8601WithFractionalSeconds: ISO8601DateFormatter = {
12-
let formatter = ISO8601DateFormatter()
13-
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
14-
return formatter
15-
}()
16-
17-
static let iso8601WithoutFractionalSeconds: ISO8601DateFormatter = {
18-
let formatter = ISO8601DateFormatter()
19-
formatter.formatOptions = [.withInternetDateTime]
20-
return formatter
21-
}()
2211

2312
public static let decodingStrategy: JSONDecoder.DateDecodingStrategy = {
2413
let strategy = JSONDecoder.DateDecodingStrategy.custom { decoder -> Date in
2514
let container = try decoder.singleValueContainer()
2615
let dateString = try container.decode(String.self)
27-
28-
if let date = iso8601WithFractionalSeconds.date(from: dateString) {
29-
return date
30-
}
31-
32-
if let date = iso8601WithoutFractionalSeconds.date(from: dateString) {
33-
return date
34-
}
35-
36-
return try container.decode(Date.self)
16+
let dateTime = try Temporal.DateTime(iso8601String: dateString)
17+
return dateTime.foundationDate
3718
}
3819

3920
return strategy
4021
}()
4122

4223
public static let encodingStrategy: JSONEncoder.DateEncodingStrategy = {
4324
let strategy = JSONEncoder.DateEncodingStrategy.custom { date, encoder in
44-
let dateString = iso8601WithFractionalSeconds.string(from: date)
4525
var container = encoder.singleValueContainer()
46-
try container.encode(dateString)
26+
try container.encode(Temporal.DateTime(date).iso8601String)
4727
}
4828
return strategy
4929
}()
5030

5131
}
5232

53-
public extension Date {
54-
55-
/// Retrieve the ISO 8601 formatted String, like "2019-11-25T00:35:01.746Z", from the Date instance
56-
var iso8601String: String {
57-
return ModelDateFormatting.iso8601WithFractionalSeconds.string(from: self)
58-
}
59-
}
60-
61-
public extension String {
62-
63-
/// Retrieve the ISO 8601 Date for valid String values like "2019-11-25T00:35:01.746Z". Supports values with and
64-
/// without fractional seconds.
65-
var iso8601Date: Date? {
66-
if let date = ModelDateFormatting.iso8601WithFractionalSeconds.date(from: self) {
67-
return date
68-
}
69-
return ModelDateFormatting.iso8601WithoutFractionalSeconds.date(from: self)
70-
}
71-
}
72-
7333
public extension JSONDecoder {
7434
convenience init(dateDecodingStrategy: JSONDecoder.DateDecodingStrategy) {
7535
self.init()

Amplify/Categories/DataStore/Model/Persistable.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ import Foundation
1212
///
1313
/// Core Types that conform to this protocol:
1414
/// - `Bool`
15-
/// - `Date`
1615
/// - `Double`
1716
/// - `Int`
1817
/// - `String`
18+
/// - `Temporal.Date`
19+
/// - `Temporal.DateTime`
20+
/// - `Temporal.Time`
1921
public protocol Persistable {}
2022

2123
extension Bool: Persistable {}
22-
extension Date: Persistable {}
2324
extension Double: Persistable {}
2425
extension Int: Persistable {}
2526
extension String: Persistable {}
27+
extension Temporal.Date: Persistable {}
28+
extension Temporal.DateTime: Persistable {}
29+
extension Temporal.Time: Persistable {}
2630

2731
struct PersistableHelper {
2832

@@ -43,7 +47,11 @@ struct PersistableHelper {
4347
switch (lhs, rhs) {
4448
case let (lhs, rhs) as (Bool, Bool):
4549
return lhs == rhs
46-
case let (lhs, rhs) as (Date, Date):
50+
case let (lhs, rhs) as (Temporal.Date, Temporal.Date):
51+
return lhs == rhs
52+
case let (lhs, rhs) as (Temporal.DateTime, Temporal.DateTime):
53+
return lhs == rhs
54+
case let (lhs, rhs) as (Temporal.Time, Temporal.Time):
4755
return lhs == rhs
4856
case let (lhs, rhs) as (Double, Double):
4957
return lhs == rhs

Amplify/Categories/DataStore/Model/Schema/ModelSchema+Definition.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,18 @@ public enum ModelFieldType {
4545
if type is Bool.Type {
4646
return .bool
4747
}
48-
// TODO handle other DataScalar once the DateTime PR is merged
4948
if type is Date.Type {
5049
return .dateTime
5150
}
51+
if type is Temporal.Date.Type {
52+
return .date
53+
}
54+
if type is Temporal.DateTime.Type {
55+
return .dateTime
56+
}
57+
if type is Temporal.Time.Type {
58+
return .time
59+
}
5260
if let enumType = type as? EnumPersistable.Type {
5361
return .enum(type: enumType)
5462
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
extension DataStoreError {
11+
12+
public static func invalidDateFormat(_ value: String) -> DataStoreError {
13+
return DataStoreError.decodingError(
14+
"""
15+
Could not parse \(value) as a Date using the ISO8601 format.
16+
""",
17+
"""
18+
Check if the format used to parse the date is the correct one. Check
19+
`TemporalFormat` for all the options.
20+
""")
21+
}
22+
23+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
public enum DateUnit {
11+
12+
case days(_ value: Int)
13+
case weeks(_ value: Int)
14+
case months(_ value: Int)
15+
case years(_ value: Int)
16+
17+
public static let oneDay: DateUnit = .days(1)
18+
public static let oneWeek: DateUnit = .weeks(1)
19+
public static let oneMonth: DateUnit = .months(1)
20+
public static let oneYear: DateUnit = .years(1)
21+
22+
public var calendarComponent: Calendar.Component {
23+
switch self {
24+
case .days, .weeks:
25+
return .day
26+
case .months:
27+
return .month
28+
case .years:
29+
return .year
30+
}
31+
}
32+
33+
public var value: Int {
34+
switch self {
35+
case .days(let value),
36+
.months(let value),
37+
.years(let value):
38+
return value
39+
case .weeks(let value):
40+
return value * 7
41+
}
42+
}
43+
44+
}
45+
46+
public protocol DateUnitOperable {
47+
48+
static func + (left: Self, right: DateUnit) -> Self
49+
50+
static func - (left: Self, right: DateUnit) -> Self
51+
52+
}
53+
54+
extension TemporalSpec where Self: DateUnitOperable {
55+
56+
public static func + (left: Self, right: DateUnit) -> Self {
57+
return left.add(value: right.value, to: right.calendarComponent)
58+
}
59+
60+
public static func - (left: Self, right: DateUnit) -> Self {
61+
return left.add(value: -right.value, to: right.calendarComponent)
62+
}
63+
64+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
extension Temporal {
11+
12+
/// An extension that makes the `Date` struct conform with the `TemporalSpec` protocol.
13+
/// When used in persistence operations, the granularity of the different date representations
14+
/// is set by using different scalar types: `Date`, `DateTime` and `Time`.
15+
///
16+
/// In those scenarios, the standard `Date` is formatted to ISO-8601 without the time.
17+
/// When the full date information is required, use `DateTime` instead.
18+
public struct Date: TemporalSpec, DateUnitOperable {
19+
20+
public static func now() -> Self {
21+
return Temporal.Date(Foundation.Date())
22+
}
23+
24+
public let foundationDate: Foundation.Date
25+
26+
public init(iso8601String: String) throws {
27+
guard let date = Temporal.Date.iso8601Date(from: iso8601String) else {
28+
throw DataStoreError.invalidDateFormat(iso8601String)
29+
}
30+
self.init(date)
31+
}
32+
33+
public init(_ date: Foundation.Date) {
34+
// sets the date to a fixed instant so date-only operations are safe
35+
self.foundationDate = Date.iso8601Calendar.startOfDay(for: date)
36+
}
37+
}
38+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
extension Temporal {
11+
12+
/// `DateTime` is an immutable `TemporalSpec` object that represents a date with a time,
13+
/// often viewed as `yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ`.
14+
///
15+
/// `DateTime` can be represented to nanosecond precision and it also holds a reference
16+
/// to a TimeZone. As all Date scalars, `DateTime` relies on the ISO-8601 calendar.
17+
public struct DateTime: TemporalSpec, DateUnitOperable, TimeUnitOperable {
18+
19+
public static var iso8601DateComponents: Set<Calendar.Component> {
20+
[.year, .month, .day, .hour, .minute, .second, .nanosecond, .timeZone]
21+
}
22+
23+
public static func now() -> DateTime {
24+
return DateTime(Foundation.Date())
25+
}
26+
27+
public let foundationDate: Foundation.Date
28+
29+
public var time: Time {
30+
Time(foundationDate)
31+
}
32+
33+
public init(_ date: Foundation.Date) {
34+
let calendar = DateTime.iso8601Calendar
35+
let components = calendar.dateComponents(DateTime.iso8601DateComponents, from: date)
36+
self.foundationDate = components.date ?? date
37+
}
38+
39+
public init(iso8601String: String) throws {
40+
guard let date = DateTime.iso8601Date(from: iso8601String) else {
41+
throw DataStoreError.invalidDateFormat(iso8601String)
42+
}
43+
self.foundationDate = date
44+
}
45+
46+
}
47+
48+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
## Custom Data Types
2+
3+
`DataStore > Model > Temporal`
4+
5+
Model-based programming aims to simplify data management on apps by abstracting away the complexities of data persistence from the core logic of the app. Therefore, data types are a critical piece of it.
6+
7+
This module provides types that complements the Swift provided [`Date`](https://developer.apple.com/documentation/foundation/date) with more control over the date granularity when persisting values to a database (i.e. time-only, date-only, date+time).
8+
9+
**Table of Contents**
10+
11+
- [Custom Data Types](#custom-data-types)
12+
- [1. Temporal](#1-temporal)
13+
- [1.1. `Temporal.Date`, `Temporal.DateTime`, `Temporal.Time`](#11-temporaldate-temporaldatetime-temporaltime)
14+
- [1.2. ISO-8601](#12-iso-8601)
15+
- [1.3. The underlying `Date`](#13-the-underlying-date)
16+
- [1.4. Operations](#14-operations)
17+
- [1.5. References](#15-references)
18+
19+
### 1. Temporal
20+
21+
The Swift foundation module provides the [`Date`](https://developer.apple.com/documentation/foundation/date) struct that represents a single point in time and can fit any precision, calendrical system or time zone. While that approach is concise and powerful, when it comes to representing persistent data its flexibility can result in ambiguity (i.e. should only the date portion be used or both date and time).
22+
23+
24+
#### 1.1. `Temporal.Date`, `Temporal.DateTime`, `Temporal.Time`
25+
26+
The `TemporalSpec` protocol was introduced to establish a more strict way to represent dates that make sense in a data persistence context.
27+
28+
#### 1.2. ISO-8601
29+
30+
The temporal implementations rely on a fixed [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) Calendar implementation ([`.iso8601`](https://developer.apple.com/documentation/foundation/calendar/identifier/iso8601)). If a representation of the date is needed in different calendars, use the underlying date object described in the next section.
31+
32+
#### 1.3. The underlying `Date`
33+
34+
Both `DateTime` and `Time` are backed by a [`Date`](https://developer.apple.com/documentation/foundation/date) instance. Therefore, they are compatible with all existing Date APIs from Foundation, including third-party libraries.
35+
36+
#### 1.4. Operations
37+
38+
Swift offers great support for complex date operations using [`Calendar`](https://developer.apple.com/documentation/foundation/calendar), but unfortunately simple use-cases often require several lines of code.
39+
40+
The `TemporalSpec` implementation offers utilities that enable simple date operations to be defined in a readable and idiomatic way.
41+
42+
Time:
43+
44+
```swift
45+
// current time plus 2 hours
46+
let time = Time.now + .hours(2)
47+
```
48+
49+
Date/Time:
50+
51+
```swift
52+
// current date/time 2 weeks ago
53+
let datetime = DateTime.now - .weeks(2)
54+
```
55+
56+
#### 1.5. References
57+
58+
Some resources that inspired types defined here:
59+
60+
- Joda Time: https://www.joda.org/joda-time/
61+
- Ruby on Rails Date API: https://api.rubyonrails.org/classes/Date.html
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
extension TemporalSpec where Self: Codable {
9+
10+
public init(from decoder: Decoder) throws {
11+
let container = try decoder.singleValueContainer()
12+
let value = try container.decode(String.self)
13+
try self.init(iso8601String: value)
14+
}
15+
16+
public func encode(to encoder: Encoder) throws {
17+
var container = encoder.singleValueContainer()
18+
try container.encode(iso8601String)
19+
}
20+
21+
}
22+
23+
extension Temporal.Date: Codable {}
24+
extension Temporal.DateTime: Codable {}
25+
extension Temporal.Time: Codable {}

0 commit comments

Comments
 (0)