Skip to content
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
4 changes: 0 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: 🔍 Xcode Select
run: |
XCODE_PATH=`mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode' && kMDItemVersion == '16.*'" -onlyin /Applications | head -1`
echo "DEVELOPER_DIR=$XCODE_PATH/Contents/Developer" >> $GITHUB_ENV
- name: Version
run: swift --version
- name: Build
Expand Down
43 changes: 32 additions & 11 deletions Sources/KeyValueDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,36 @@ public struct KeyValueDecoder: Sendable {
/// Decodes dates by casting from Any.
case date

/// Decodes dates from ISO8601 strings.
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])

/// Decodes dates in terms of milliseconds since midnight UTC on January 1st, 1970.
case millisecondsSince1970

/// Decodes dates in terms of seconds since midnight UTC on January 1st, 1970.
case secondsSince1970

/// Decodes dates from Any using a closure
case custom(@Sendable (Any) throws -> Date)

/// Decodes dates from ISO8601 strings.
static func iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime]) -> Self {
.custom {
guard let string = $0 as? String else {
throw Error("Expected String but found \(type(of: $0))")
}
let formatter = ISO8601DateFormatter()
formatter.formatOptions = options
guard let date = formatter.date(from: string) else {
throw Error("Failed to decode Date from ISO8601 string \(string)")
}
return date
}
}
}

struct Error: LocalizedError {
var errorDescription: String?
init(_ message: String) {
self.errorDescription = message
}
}
}

Expand Down Expand Up @@ -359,19 +381,18 @@ private extension KeyValueDecoder {
switch strategy.dates {
case .date:
return try getValue()
case .iso8601(options: let options):
let string = try decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = options
guard let date = formatter.date(from: string) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Failed to decode Date from ISO8601 string \(string)"))
}
return date
case .millisecondsSince1970:
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)) / 1000)

case .secondsSince1970:
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)))

case .custom(let transform):
do {
return try transform(self.value)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: error.localizedDescription))
}
}
}

Expand Down
49 changes: 35 additions & 14 deletions Sources/KeyValueEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,23 @@ public struct KeyValueEncoder: Sendable {
/// Encodes dates by directly casting to Any.
case date

/// Encodes dates from ISO8601 strings.
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])

/// Encodes dates to Int in terms of milliseconds since midnight UTC on January 1, 1970.
case millisecondsSince1970

/// Encodes dates to Int in terms of seconds since midnight UTC on January 1, 1970.
case secondsSince1970

/// Encodes dates to Any using a closure
case custom(@Sendable (Date) throws -> Any)

/// Encodes dates to ISO8601 strings.
static func iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime]) -> Self {
.custom {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = options
return formatter.string(from: $0)
}
}
}
}

Expand Down Expand Up @@ -238,7 +247,7 @@ private extension KeyValueEncoder {
}

func encodeToValue<T>(_ value: T) throws -> EncodedValue where T: Encodable {
guard let encoded = EncodedValue.makeValue(for: value, using: strategy) else {
guard let encoded = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) else {
try value.encode(to: self)
return try getEncodedValue()
}
Expand Down Expand Up @@ -338,7 +347,7 @@ private extension KeyValueEncoder {
}

func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
if let val = EncodedValue.makeValue(for: value, using: strategy) {
if let val = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) {
setValue(val, forKey: key)
return
}
Expand Down Expand Up @@ -468,7 +477,7 @@ private extension KeyValueEncoder {
}

func encode<T: Encodable>(_ value: T) throws {
if let val = EncodedValue.makeValue(for: value, using: strategy) {
if let val = try EncodedValue.makeValue(for: value, at: codingPath.appending(index: count), using: strategy) {
appendValue(val)
return
}
Expand Down Expand Up @@ -587,7 +596,7 @@ private extension KeyValueEncoder {
}

func encode<T>(_ value: T) throws where T: Encodable {
if let encoded = EncodedValue.makeValue(for: value, using: strategy) {
if let encoded = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) {
self.value = encoded
return
}
Expand Down Expand Up @@ -708,11 +717,25 @@ struct AnyCodingKey: CodingKey {

extension KeyValueEncoder.EncodedValue {

static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) -> Self? {
static func makeValue(for value: Any, at codingPath: [any CodingKey], using strategy: KeyValueEncoder.EncodingStrategy) throws -> Self? {
do {
return try makeValue(for: value, using: strategy)
} catch {
let valueDescription = strategy.optionals.isNull(value) ? "nil" : String(describing: type(of: value))
let context = EncodingError.Context(
codingPath: codingPath,
debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()) cannot be encoded. \(error.localizedDescription)",
underlyingError: error
)
throw EncodingError.invalidValue(value, context)
}
}

static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) throws -> Self? {
if let dataValue = value as? Data {
return .value(dataValue)
} else if let dateValue = value as? Date {
return makeValue(for: dateValue, using: strategy.dates)
return try makeValue(for: dateValue, using: strategy.dates)
} else if let urlValue = value as? URL {
return .value(urlValue)
} else if let decimalValue = value as? Decimal {
Expand All @@ -722,14 +745,12 @@ extension KeyValueEncoder.EncodedValue {
}
}

static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) -> Self? {
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) throws -> Self? {
switch strategy {
case .date:
return .value(date)
case .iso8601(options: let options):
let f = ISO8601DateFormatter()
f.formatOptions = options
return .value(f.string(from: date))
case .custom(let transform):
return try .value(transform(date))
case .millisecondsSince1970:
return .value(Int(date.timeIntervalSince1970 * 1000))
case .secondsSince1970:
Expand Down
33 changes: 31 additions & 2 deletions Tests/KeyValueEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,19 @@ struct KeyValueEncodedTests {
#expect(
try encoder.encode(referenceDate) as? Int == 978307200
)

#if compiler(>=6.1)
encoder.dateEncodingStrategy = .custom { _ in throw KeyValueDecoder.Error("🐟") }
var error = #expect(throws: EncodingError.self) {
try encoder.encode(referenceDate)
}
#expect(error?.context?.debugDescription == "Date at SELF cannot be encoded. 🐟")

error = #expect(throws: EncodingError.self) {
try encoder.encode(["calendar": [Date?.none, referenceDate]])
}
#expect(error?.context?.debugDescription == "Optional<Date> at SELF.calendar[1] cannot be encoded. 🐟")
#endif
}

@Test
Expand All @@ -708,7 +721,7 @@ struct KeyValueEncodedTests {
}

@Test
func aa() {
func isOptionalNone() {
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int?.none as Any))
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int??.none as Any))
}
Expand Down Expand Up @@ -831,7 +844,11 @@ extension KeyValueEncoder.EncodedValue {

private extension KeyValueEncoder.EncodedValue {
static func isSupportedValue(_ value: Any) -> Bool {
Self.makeValue(for: value, using: .default) != nil
do {
return try Self.makeValue(for: value, using: .default) != nil
} catch {
return false
}
}
}

Expand Down Expand Up @@ -869,4 +886,16 @@ private struct Null: Encodable {
try container.encodeNil()
}
}

private extension EncodingError {

var context: Context? {
switch self {
case .invalidValue(_, let context):
return context
default:
return nil
}
}
}
#endif
6 changes: 5 additions & 1 deletion Tests/KeyValueEncoderXCTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,11 @@ extension KeyValueEncoder.EncodedValue {

private extension KeyValueEncoder.EncodedValue {
static func isSupportedValue(_ value: Any) -> Bool {
Self.makeValue(for: value, using: .default) != nil
do {
return try Self.makeValue(for: value, using: .default) != nil
} catch {
return false
}
}
}

Expand Down