Skip to content

Commit 0104331

Browse files
authored
fix: decoder and encoder default instances (#711)
1 parent d1aaa1e commit 0104331

File tree

12 files changed

+203
-179
lines changed

12 files changed

+203
-179
lines changed

Sources/Auth/Defaults.swift

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,23 @@ import Foundation
1010
import Helpers
1111

1212
extension AuthClient.Configuration {
13-
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
14-
ISO8601DateFormatter.iso8601WithFractionalSeconds,
15-
ISO8601DateFormatter.iso8601,
16-
]
17-
1813
/// The default JSONEncoder instance used by the ``AuthClient``.
1914
public static let jsonEncoder: JSONEncoder = {
20-
let encoder = JSONEncoder()
15+
let encoder = JSONEncoder.supabase()
2116
encoder.keyEncodingStrategy = .convertToSnakeCase
22-
encoder.dateEncodingStrategy = .custom { date, encoder in
23-
let string = ISO8601DateFormatter.iso8601WithFractionalSeconds.value.string(from: date)
24-
var container = encoder.singleValueContainer()
25-
try container.encode(string)
26-
}
2717
return encoder
2818
}()
2919

3020
/// The default JSONDecoder instance used by the ``AuthClient``.
3121
public static let jsonDecoder: JSONDecoder = {
32-
let decoder = JSONDecoder()
22+
let decoder = JSONDecoder.supabase()
3323
decoder.keyDecodingStrategy = .convertFromSnakeCase
34-
decoder.dateDecodingStrategy = .custom { decoder in
35-
let container = try decoder.singleValueContainer()
36-
let string = try container.decode(String.self)
37-
38-
for formatter in supportedDateFormatters {
39-
if let date = formatter.value.date(from: string) {
40-
return date
41-
}
42-
}
43-
44-
throw DecodingError.dataCorruptedError(
45-
in: container, debugDescription: "Invalid date format: \(string)"
46-
)
47-
}
4824
return decoder
4925
}()
5026

27+
/// The default headers used by the ``AuthClient``.
5128
public static let defaultHeaders: [String: String] = [
52-
"X-Client-Info": "auth-swift/\(version)",
29+
"X-Client-Info": "auth-swift/\(version)"
5330
]
5431

5532
/// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance.

Sources/Auth/Internal/SessionStorage.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension SessionStorage {
3434
let migrations: [StorageMigration] = [
3535
.sessionNewKey(clientID: clientID),
3636
.storeSessionDirectly(clientID: clientID),
37+
.useDefaultEncoder(clientID: clientID),
3738
]
3839

3940
var key: String {
@@ -46,14 +47,16 @@ extension SessionStorage {
4647
do {
4748
try migration.run()
4849
} catch {
49-
logger?.error("Storage migration failed: \(error.localizedDescription)")
50+
logger?.error(
51+
"Storage migration '\(migration.name)' failed: \(error.localizedDescription)"
52+
)
5053
}
5154
}
5255

5356
do {
5457
let storedData = try storage.retrieve(key: key)
5558
return try storedData.flatMap {
56-
try AuthClient.Configuration.jsonDecoder.decode(Session.self, from: $0)
59+
try JSONDecoder().decode(Session.self, from: $0)
5760
}
5861
} catch {
5962
logger?.error("Failed to retrieve session: \(error.localizedDescription)")
@@ -64,7 +67,7 @@ extension SessionStorage {
6467
do {
6568
try storage.store(
6669
key: key,
67-
value: AuthClient.Configuration.jsonEncoder.encode(session)
70+
value: JSONEncoder().encode(session)
6871
)
6972
} catch {
7073
logger?.error("Failed to store session: \(error.localizedDescription)")
@@ -82,14 +85,15 @@ extension SessionStorage {
8285
}
8386

8487
struct StorageMigration {
88+
var name: String
8589
var run: @Sendable () throws -> Void
8690
}
8791

8892
extension StorageMigration {
8993
/// Migrate stored session from `supabase.session` key to the custom provided storage key
9094
/// or the default `supabase.auth.token` key.
9195
static func sessionNewKey(clientID: AuthClientID) -> StorageMigration {
92-
StorageMigration {
96+
StorageMigration(name: "sessionNewKey") {
9397
let storage = Dependencies[clientID].configuration.localStorage
9498
let newKey = SessionStorage.key(clientID)
9599

@@ -117,16 +121,38 @@ extension StorageMigration {
117121
var expirationDate: Date
118122
}
119123

120-
return StorageMigration {
124+
return StorageMigration(name: "storeSessionDirectly") {
121125
let storage = Dependencies[clientID].configuration.localStorage
122126
let key = SessionStorage.key(clientID)
123127

124128
if let data = try? storage.retrieve(key: key),
125-
let storedSession = try? AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: data)
129+
let storedSession = try? AuthClient.Configuration.jsonDecoder.decode(
130+
StoredSession.self,
131+
from: data
132+
)
126133
{
127134
let session = try AuthClient.Configuration.jsonEncoder.encode(storedSession.session)
128135
try storage.store(key: key, value: session)
129136
}
130137
}
131138
}
139+
140+
static func useDefaultEncoder(clientID: AuthClientID) -> StorageMigration {
141+
StorageMigration(name: "useDefaultEncoder") {
142+
let storage = Dependencies[clientID].configuration.localStorage
143+
let key = SessionStorage.key(clientID)
144+
145+
let storedData = try? storage.retrieve(key: key)
146+
let sessionUsingOldDecoder = storedData.flatMap {
147+
try? AuthClient.Configuration.jsonDecoder.decode(Session.self, from: $0)
148+
}
149+
150+
if let sessionUsingOldDecoder {
151+
try storage.store(
152+
key: key,
153+
value: JSONEncoder().encode(sessionUsingOldDecoder)
154+
)
155+
}
156+
}
157+
}
132158
}

Sources/Helpers/AnyJSON/AnyJSON+Codable.swift

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,12 @@ import Foundation
1010
extension AnyJSON {
1111
/// The decoder instance used for transforming AnyJSON to some Codable type.
1212
public static let decoder: JSONDecoder = {
13-
let decoder = JSONDecoder()
14-
decoder.dataDecodingStrategy = .base64
15-
decoder.dateDecodingStrategy = .custom { decoder in
16-
let container = try decoder.singleValueContainer()
17-
let dateString = try container.decode(String.self)
18-
19-
let date =
20-
ISO8601DateFormatter.iso8601WithFractionalSeconds.value.date(from: dateString)
21-
?? ISO8601DateFormatter.iso8601.value.date(from: dateString)
22-
23-
guard let decodedDate = date else {
24-
throw DecodingError.dataCorruptedError(
25-
in: container, debugDescription: "Invalid date format: \(dateString)"
26-
)
27-
}
28-
29-
return decodedDate
30-
}
31-
return decoder
13+
JSONDecoder.supabase()
3214
}()
3315

3416
/// The encoder instance used for transforming AnyJSON to some Codable type.
3517
public static let encoder: JSONEncoder = {
36-
let encoder = JSONEncoder()
37-
encoder.dataEncodingStrategy = .base64
38-
encoder.dateEncodingStrategy = .custom { date, encoder in
39-
let string = ISO8601DateFormatter.iso8601WithFractionalSeconds.value.string(from: date)
40-
var container = encoder.singleValueContainer()
41-
try container.encode(string)
42-
}
43-
return encoder
18+
JSONEncoder.supabase()
4419
}()
4520
}
4621

Sources/Helpers/Codable.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,33 @@ import ConcurrencyExtras
99
import Foundation
1010

1111
extension JSONDecoder {
12-
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
13-
ISO8601DateFormatter.iso8601WithFractionalSeconds,
14-
ISO8601DateFormatter.iso8601,
15-
]
16-
1712
/// Default `JSONDecoder` for decoding types from Supabase.
18-
package static let `default`: JSONDecoder = {
13+
package static func supabase() -> JSONDecoder {
1914
let decoder = JSONDecoder()
2015
decoder.dateDecodingStrategy = .custom { decoder in
2116
let container = try decoder.singleValueContainer()
2217
let string = try container.decode(String.self)
2318

24-
for formatter in supportedDateFormatters {
25-
if let date = formatter.value.date(from: string) {
26-
return date
27-
}
19+
if let date = string.date {
20+
return date
2821
}
2922

3023
throw DecodingError.dataCorruptedError(
3124
in: container, debugDescription: "Invalid date format: \(string)"
3225
)
3326
}
3427
return decoder
35-
}()
28+
}
29+
}
30+
extension JSONEncoder {
31+
/// Default `JSONEncoder` for encoding types to Supabase.
32+
package static func supabase() -> JSONEncoder {
33+
let encoder = JSONEncoder()
34+
encoder.dateEncodingStrategy = .custom { date, encoder in
35+
var container = encoder.singleValueContainer()
36+
let string = date.iso8601String
37+
try container.encode(string)
38+
}
39+
return encoder
40+
}
3641
}

Sources/Helpers/DateFormatter.swift

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,72 @@
55
// Created by Guilherme Souza on 28/12/23.
66
//
77

8-
import ConcurrencyExtras
98
import Foundation
109

11-
extension ISO8601DateFormatter {
12-
package static let iso8601: UncheckedSendable<ISO8601DateFormatter> = {
13-
let formatter = ISO8601DateFormatter()
14-
formatter.formatOptions = [.withInternetDateTime]
15-
return UncheckedSendable(formatter)
10+
extension DateFormatter {
11+
fileprivate static func iso8601(includingFractionalSeconds: Bool) -> DateFormatter {
12+
includingFractionalSeconds ? iso8601Fractional : iso8601Whole
13+
}
14+
15+
fileprivate static let iso8601Fractional: DateFormatter = {
16+
let formatter = DateFormatter()
17+
formatter.calendar = Calendar(identifier: .iso8601)
18+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
19+
formatter.locale = Locale(identifier: "en_US_POSIX")
20+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
21+
return formatter
1622
}()
1723

18-
package static let iso8601WithFractionalSeconds: UncheckedSendable<ISO8601DateFormatter> = {
19-
let formatter = ISO8601DateFormatter()
20-
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
21-
return UncheckedSendable(formatter)
24+
fileprivate static let iso8601Whole: DateFormatter = {
25+
let formatter = DateFormatter()
26+
formatter.calendar = Calendar(identifier: .iso8601)
27+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
28+
formatter.locale = Locale(identifier: "en_US_POSIX")
29+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
30+
return formatter
2231
}()
2332
}
33+
34+
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
35+
extension Date.ISO8601FormatStyle {
36+
fileprivate func currentTimestamp(includingFractionalSeconds: Bool) -> Self {
37+
year().month().day()
38+
.dateTimeSeparator(.standard)
39+
.time(includingFractionalSeconds: includingFractionalSeconds)
40+
}
41+
}
42+
43+
extension Date {
44+
package var iso8601String: String {
45+
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
46+
return formatted(.iso8601.currentTimestamp(includingFractionalSeconds: true))
47+
} else {
48+
return DateFormatter.iso8601(includingFractionalSeconds: true).string(from: self)
49+
}
50+
}
51+
}
52+
53+
extension String {
54+
package var date: Date? {
55+
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
56+
if let date = try? Date(
57+
self,
58+
strategy: .iso8601.currentTimestamp(includingFractionalSeconds: true)
59+
) {
60+
return date
61+
}
62+
return try? Date(
63+
self,
64+
strategy: .iso8601.currentTimestamp(includingFractionalSeconds: false)
65+
)
66+
} else {
67+
guard
68+
let date = DateFormatter.iso8601(includingFractionalSeconds: true).date(from: self)
69+
?? DateFormatter.iso8601(includingFractionalSeconds: false).date(from: self)
70+
else {
71+
return nil
72+
}
73+
return date
74+
}
75+
}
76+
}

Sources/Helpers/SupabaseLogger.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ public struct SupabaseLogMessage: Codable, CustomStringConvertible, Sendable {
5555
}
5656

5757
public var description: String {
58-
let date = ISO8601DateFormatter.iso8601.value.string(
59-
from: Date(timeIntervalSince1970: timestamp))
58+
let date = Date(timeIntervalSince1970: timestamp).iso8601String
6059
let file = fileID.split(separator: ".", maxSplits: 1).first.map(String.init) ?? fileID
6160
var description = "\(date) [\(level)] [\(system)] [\(file).\(function):\(line)] \(message)"
6261
if !additionalContext.isEmpty {

Sources/PostgREST/Defaults.swift

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,18 @@ import Helpers
1212
let version = Helpers.version
1313

1414
extension PostgrestClient.Configuration {
15-
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
16-
ISO8601DateFormatter.iso8601WithFractionalSeconds,
17-
ISO8601DateFormatter.iso8601,
18-
]
19-
2015
/// The default `JSONDecoder` instance for ``PostgrestClient`` responses.
21-
public static let jsonDecoder = { () -> JSONDecoder in
22-
let decoder = JSONDecoder()
23-
decoder.dateDecodingStrategy = .custom { decoder in
24-
let container = try decoder.singleValueContainer()
25-
let string = try container.decode(String.self)
26-
27-
for formatter in supportedDateFormatters {
28-
if let date = formatter.value.date(from: string) {
29-
return date
30-
}
31-
}
32-
33-
throw DecodingError.dataCorruptedError(
34-
in: container, debugDescription: "Invalid date format: \(string)"
35-
)
36-
}
37-
return decoder
16+
public static let jsonDecoder: JSONDecoder = {
17+
JSONDecoder.supabase()
3818
}()
3919

4020
/// The default `JSONEncoder` instance for ``PostgrestClient`` requests.
41-
public static let jsonEncoder = { () -> JSONEncoder in
42-
let encoder = JSONEncoder()
43-
encoder.dateEncodingStrategy = .iso8601
44-
return encoder
21+
public static let jsonEncoder: JSONEncoder = {
22+
JSONEncoder.supabase()
4523
}()
4624

25+
/// The default headers for ``PostgrestClient`` requests.
4726
public static let defaultHeaders: [String: String] = [
48-
"X-Client-Info": "postgrest-swift/\(version)",
27+
"X-Client-Info": "postgrest-swift/\(version)"
4928
]
5029
}

Sources/Storage/Codable.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension JSONEncoder {
2121

2222
extension JSONDecoder {
2323
@available(*, deprecated, message: "Access to storage decoder is going to be removed.")
24-
public static var defaultStorageDecoder: JSONDecoder {
25-
.default
26-
}
24+
public static let defaultStorageDecoder: JSONDecoder = {
25+
JSONDecoder.supabase()
26+
}()
2727
}

0 commit comments

Comments
 (0)