Skip to content

fix: decoder and encoder default instances #711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 4 additions & 27 deletions Sources/Auth/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,23 @@ import Foundation
import Helpers

extension AuthClient.Configuration {
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
ISO8601DateFormatter.iso8601WithFractionalSeconds,
ISO8601DateFormatter.iso8601,
]

/// The default JSONEncoder instance used by the ``AuthClient``.
public static let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
let encoder = JSONEncoder.supabase()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .custom { date, encoder in
let string = ISO8601DateFormatter.iso8601WithFractionalSeconds.value.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(string)
}
return encoder
}()

/// The default JSONDecoder instance used by the ``AuthClient``.
public static let jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
let decoder = JSONDecoder.supabase()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

for formatter in supportedDateFormatters {
if let date = formatter.value.date(from: string) {
return date
}
}

throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Invalid date format: \(string)"
)
}
return decoder
}()

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

/// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance.
Expand Down
38 changes: 32 additions & 6 deletions Sources/Auth/Internal/SessionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension SessionStorage {
let migrations: [StorageMigration] = [
.sessionNewKey(clientID: clientID),
.storeSessionDirectly(clientID: clientID),
.useDefaultEncoder(clientID: clientID),
]

var key: String {
Expand All @@ -46,14 +47,16 @@ extension SessionStorage {
do {
try migration.run()
} catch {
logger?.error("Storage migration failed: \(error.localizedDescription)")
logger?.error(
"Storage migration '\(migration.name)' failed: \(error.localizedDescription)"
)
}
}

do {
let storedData = try storage.retrieve(key: key)
return try storedData.flatMap {
try AuthClient.Configuration.jsonDecoder.decode(Session.self, from: $0)
try JSONDecoder().decode(Session.self, from: $0)
Copy link
Preview

Copilot AI Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using JSONDecoder.supabase() here instead of instantiating a new JSONDecoder(), to ensure consistent decoding behavior across the codebase.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the decoder used for decoding the stored session, it is safer to use a new decoder instance, unrelated to the other ones used in codebase, since any change to decoder configuration may break the session decoding.

}
} catch {
logger?.error("Failed to retrieve session: \(error.localizedDescription)")
Expand All @@ -64,7 +67,7 @@ extension SessionStorage {
do {
try storage.store(
key: key,
value: AuthClient.Configuration.jsonEncoder.encode(session)
value: JSONEncoder().encode(session)
Copy link
Preview

Copilot AI Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using JSONEncoder.supabase() here instead of a new JSONEncoder(), to maintain consistent encoding behavior in session storage.

Suggested change
value: JSONEncoder().encode(session)
value: JSONEncoder.supabase().encode(session)

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

@grdsdev grdsdev Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment related to JSONDecoder.

)
} catch {
logger?.error("Failed to store session: \(error.localizedDescription)")
Expand All @@ -82,14 +85,15 @@ extension SessionStorage {
}

struct StorageMigration {
var name: String
var run: @Sendable () throws -> Void
}

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

Expand Down Expand Up @@ -117,16 +121,38 @@ extension StorageMigration {
var expirationDate: Date
}

return StorageMigration {
return StorageMigration(name: "storeSessionDirectly") {
let storage = Dependencies[clientID].configuration.localStorage
let key = SessionStorage.key(clientID)

if let data = try? storage.retrieve(key: key),
let storedSession = try? AuthClient.Configuration.jsonDecoder.decode(StoredSession.self, from: data)
let storedSession = try? AuthClient.Configuration.jsonDecoder.decode(
StoredSession.self,
from: data
)
{
let session = try AuthClient.Configuration.jsonEncoder.encode(storedSession.session)
try storage.store(key: key, value: session)
}
}
}

static func useDefaultEncoder(clientID: AuthClientID) -> StorageMigration {
StorageMigration(name: "useDefaultEncoder") {
let storage = Dependencies[clientID].configuration.localStorage
let key = SessionStorage.key(clientID)

let storedData = try? storage.retrieve(key: key)
let sessionUsingOldDecoder = storedData.flatMap {
try? AuthClient.Configuration.jsonDecoder.decode(Session.self, from: $0)
}

if let sessionUsingOldDecoder {
try storage.store(
key: key,
value: JSONEncoder().encode(sessionUsingOldDecoder)
)
}
}
}
}
29 changes: 2 additions & 27 deletions Sources/Helpers/AnyJSON/AnyJSON+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,12 @@ import Foundation
extension AnyJSON {
/// The decoder instance used for transforming AnyJSON to some Codable type.
public static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)

let date =
ISO8601DateFormatter.iso8601WithFractionalSeconds.value.date(from: dateString)
?? ISO8601DateFormatter.iso8601.value.date(from: dateString)

guard let decodedDate = date else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Invalid date format: \(dateString)"
)
}

return decodedDate
}
return decoder
JSONDecoder.supabase()
}()

/// The encoder instance used for transforming AnyJSON to some Codable type.
public static let encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.dateEncodingStrategy = .custom { date, encoder in
let string = ISO8601DateFormatter.iso8601WithFractionalSeconds.value.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(string)
}
return encoder
JSONEncoder.supabase()
}()
}

Expand Down
27 changes: 16 additions & 11 deletions Sources/Helpers/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,33 @@ import ConcurrencyExtras
import Foundation

extension JSONDecoder {
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
ISO8601DateFormatter.iso8601WithFractionalSeconds,
ISO8601DateFormatter.iso8601,
]

/// Default `JSONDecoder` for decoding types from Supabase.
package static let `default`: JSONDecoder = {
package static func supabase() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

for formatter in supportedDateFormatters {
if let date = formatter.value.date(from: string) {
return date
}
if let date = string.date {
return date
}

throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Invalid date format: \(string)"
)
}
return decoder
}()
}
}
extension JSONEncoder {
/// Default `JSONEncoder` for encoding types to Supabase.
package static func supabase() -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom { date, encoder in
var container = encoder.singleValueContainer()
let string = date.iso8601String
try container.encode(string)
}
return encoder
}
}
73 changes: 63 additions & 10 deletions Sources/Helpers/DateFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,72 @@
// Created by Guilherme Souza on 28/12/23.
//

import ConcurrencyExtras
import Foundation

extension ISO8601DateFormatter {
package static let iso8601: UncheckedSendable<ISO8601DateFormatter> = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return UncheckedSendable(formatter)
extension DateFormatter {
fileprivate static func iso8601(includingFractionalSeconds: Bool) -> DateFormatter {
includingFractionalSeconds ? iso8601Fractional : iso8601Whole
}

fileprivate static let iso8601Fractional: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()

package static let iso8601WithFractionalSeconds: UncheckedSendable<ISO8601DateFormatter> = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return UncheckedSendable(formatter)
fileprivate static let iso8601Whole: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}

@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
extension Date.ISO8601FormatStyle {
fileprivate func currentTimestamp(includingFractionalSeconds: Bool) -> Self {
year().month().day()
.dateTimeSeparator(.standard)
.time(includingFractionalSeconds: includingFractionalSeconds)
}
}

extension Date {
package var iso8601String: String {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
return formatted(.iso8601.currentTimestamp(includingFractionalSeconds: true))
} else {
return DateFormatter.iso8601(includingFractionalSeconds: true).string(from: self)
}
}
}

extension String {
package var date: Date? {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
if let date = try? Date(
self,
strategy: .iso8601.currentTimestamp(includingFractionalSeconds: true)
) {
return date
}
return try? Date(
self,
strategy: .iso8601.currentTimestamp(includingFractionalSeconds: false)
)
} else {
guard
let date = DateFormatter.iso8601(includingFractionalSeconds: true).date(from: self)
?? DateFormatter.iso8601(includingFractionalSeconds: false).date(from: self)
else {
return nil
}
return date
}
}
}
3 changes: 1 addition & 2 deletions Sources/Helpers/SupabaseLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public struct SupabaseLogMessage: Codable, CustomStringConvertible, Sendable {
}

public var description: String {
let date = ISO8601DateFormatter.iso8601.value.string(
from: Date(timeIntervalSince1970: timestamp))
let date = Date(timeIntervalSince1970: timestamp).iso8601String
let file = fileID.split(separator: ".", maxSplits: 1).first.map(String.init) ?? fileID
var description = "\(date) [\(level)] [\(system)] [\(file).\(function):\(line)] \(message)"
if !additionalContext.isEmpty {
Expand Down
33 changes: 6 additions & 27 deletions Sources/PostgREST/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,18 @@ import Helpers
let version = Helpers.version

extension PostgrestClient.Configuration {
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
ISO8601DateFormatter.iso8601WithFractionalSeconds,
ISO8601DateFormatter.iso8601,
]

/// The default `JSONDecoder` instance for ``PostgrestClient`` responses.
public static let jsonDecoder = { () -> JSONDecoder in
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

for formatter in supportedDateFormatters {
if let date = formatter.value.date(from: string) {
return date
}
}

throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Invalid date format: \(string)"
)
}
return decoder
public static let jsonDecoder: JSONDecoder = {
JSONDecoder.supabase()
}()

/// The default `JSONEncoder` instance for ``PostgrestClient`` requests.
public static let jsonEncoder = { () -> JSONEncoder in
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
public static let jsonEncoder: JSONEncoder = {
JSONEncoder.supabase()
}()

/// The default headers for ``PostgrestClient`` requests.
public static let defaultHeaders: [String: String] = [
"X-Client-Info": "postgrest-swift/\(version)",
"X-Client-Info": "postgrest-swift/\(version)"
]
}
6 changes: 3 additions & 3 deletions Sources/Storage/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension JSONEncoder {

extension JSONDecoder {
@available(*, deprecated, message: "Access to storage decoder is going to be removed.")
public static var defaultStorageDecoder: JSONDecoder {
.default
}
public static let defaultStorageDecoder: JSONDecoder = {
JSONDecoder.supabase()
}()
}
Loading
Loading