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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ let user = try KeyValueDecoder().decode(User.self, from: [["id": 1, "name": "Her
let ascii = try KeyValueDecoder().decode([UInt8].self, from: [10, 100, 1000])
```


## Date Encoding/Decoding Strategy

The encoding of `Date` can be adjusted by setting the strategy.

By default `Date` instances are encoded by simply casting to `Any` but this adjusted by setting the strategy.

The default strategy casts to `Any` leaving the instance unchanged:

```swift
var encoder = KeyValueEncoder()
encoder.dateEncodingStrategy = .date

// Date()
let any = try encoder.encode(Date())
```

ISO8601 compatible strings can be used:

```swift
encoder.dateEncodingStrategy = .iso8601()

// "1970-01-01T00:00:00Z"
let any = try encoder.encode(Date(timeIntervalSince1970: 0))
```

Epochs are also supported using `.secondsSince1970` and `millisecondsSince1970`.

## Nil Encoding/Decoding Strategy

The encoding of `Optional.none` can be adjusted by setting the strategy.
Expand Down
44 changes: 40 additions & 4 deletions Sources/KeyValueDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ public struct KeyValueDecoder: Sendable {
/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey: any Sendable]

/// The strategy to use for decoding `nil`. Defaults to `Optional<Any>.none` which can be decoded to any optional type.
public var nilDecodingStrategy: NilDecodingStrategy = .default
/// The strategy to use for decoding Date types.
public var dateDecodingStrategy: DateDecodingStrategy = .date

/// The strategy to use for decoding BinaryInteger types. Defaults to `.exact` for lossless conversion between types.
public var intDecodingStrategy: IntDecodingStrategy = .exact

/// The strategy to use for decoding each types keys.
public var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

/// The strategy to use for decoding `nil`. Defaults to `Optional<Any>.none` which can be decoded to any optional type.
public var nilDecodingStrategy: NilDecodingStrategy = .default

/// Initializes `self` with default strategy.
public init () {
self.userInfo = [:]
Expand Down Expand Up @@ -94,6 +97,21 @@ public struct KeyValueDecoder: Sendable {
/// A key encoding strategy that doesn’t change key names during encoding.
case useDefaultKeys
}

public enum DateDecodingStrategy: 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
}
}

#if canImport(Combine)
Expand All @@ -118,13 +136,15 @@ private extension KeyValueDecoder {
var optionals: NilDecodingStrategy
var integers: IntDecodingStrategy
var keys: KeyDecodingStrategy
var dates: DateDecodingStrategy
}

var strategy: DecodingStrategy {
DecodingStrategy(
optionals: nilDecodingStrategy,
integers: intDecodingStrategy,
keys: keyDecodingStrategy
keys: keyDecodingStrategy,
dates: dateDecodingStrategy
)
}

Expand Down Expand Up @@ -336,7 +356,23 @@ private extension KeyValueDecoder {
}

func decode(_ type: Date.Type) throws -> Date {
try getValue()
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)))
}
}

func decode(_ type: Data.Type) throws -> Data {
Expand Down
79 changes: 56 additions & 23 deletions Sources/KeyValueEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ public struct KeyValueEncoder: Sendable {
/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey: any Sendable]

/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
public var nilEncodingStrategy: NilEncodingStrategy = .default
/// The strategy to use for encoding Date types.
public var dateEncodingStrategy: DateEncodingStrategy = .date

/// The strategy to use for encoding each types keys.
public var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys

/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
public var nilEncodingStrategy: NilEncodingStrategy = .default


/// Initializes `self` with default strategies.
public init () {
self.userInfo = [:]
Expand All @@ -67,6 +71,21 @@ public struct KeyValueEncoder: Sendable {
/// A key encoding strategy that doesn’t change key names during encoding.
case useDefaultKeys
}

public enum DateEncodingStrategy: 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
}
}

/// Strategy used to encode and decode nil values.
Expand Down Expand Up @@ -96,6 +115,12 @@ extension KeyValueEncoder: TopLevelEncoder {

extension KeyValueEncoder {

struct EncodingStrategy {
var optionals: NilEncodingStrategy
var keys: KeyEncodingStrategy
var dates: DateEncodingStrategy
}

static func makePlistCompatible() -> KeyValueEncoder {
var encoder = KeyValueEncoder()
encoder.nilEncodingStrategy = .stringNull
Expand Down Expand Up @@ -161,15 +186,11 @@ private extension KeyValueEncoder.NilEncodingStrategy {

private extension KeyValueEncoder {

struct EncodingStrategy {
var optionals: NilEncodingStrategy
var keys: KeyEncodingStrategy
}

var strategy: EncodingStrategy {
EncodingStrategy(
optionals: nilEncodingStrategy,
keys: keyEncodingStrategy
keys: keyEncodingStrategy,
dates: dateEncodingStrategy
)
}

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

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

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

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

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

extension KeyValueEncoder.EncodedValue {

static func isSupportedValue(_ value: Any) -> Bool {
switch value {
case is Data: return true
case is Date: return true
case is URL: return true
case is Decimal: return true
default: return false
static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) -> Self? {
if let dataValue = value as? Data {
return .value(dataValue)
} else if let dateValue = value as? Date {
return makeValue(for: dateValue, using: strategy.dates)
} else if let urlValue = value as? URL {
return .value(urlValue)
} else if let decimalValue = value as? Decimal {
return .value(decimalValue)
} else {
return nil
}
}

init?(_ value: Any) {
guard Self.isSupportedValue(value) else {
return nil
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) -> 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 .millisecondsSince1970:
return .value(Int(date.timeIntervalSince1970 * 1000))
case .secondsSince1970:
return .value(Int(date.timeIntervalSince1970))
}
self = .value(value)
}
}
30 changes: 27 additions & 3 deletions Tests/KeyValueDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,35 @@ struct KeyValueDecoderTests {

@Test
func decodes_Date() throws {
let decoder = KeyValueDecoder()
var decoder = KeyValueDecoder()
let referenceDate = Date(timeIntervalSinceReferenceDate: 0)

decoder.dateDecodingStrategy = .date
#expect(
try decoder.decode(Date.self, from: referenceDate) == referenceDate
)

decoder.dateDecodingStrategy = .iso8601()
#expect(
try decoder.decode(Date.self, from: "2001-01-01T00:00:00Z") == referenceDate
)
#expect(throws: DecodingError.self) {
try decoder.decode(Date.self, from: "2001-01-01")
}

decoder.dateDecodingStrategy = .iso8601(options: [.withInternetDateTime, .withFractionalSeconds])
#expect(
try decoder.decode(Date.self, from: "2001-01-01T00:00:00.000Z") == referenceDate
)

decoder.dateDecodingStrategy = .millisecondsSince1970
#expect(
try decoder.decode(Date.self, from: 978307200000) == referenceDate
)

let date = Date(timeIntervalSinceReferenceDate: 0)
decoder.dateDecodingStrategy = .secondsSince1970
#expect(
try decoder.decode(Date.self, from: date) == date
try decoder.decode(Date.self, from: 978307200) == referenceDate
)
}

Expand Down
Loading