Skip to content

Commit

Permalink
NilEncodingStrategy
Browse files Browse the repository at this point in the history
  • Loading branch information
swhitty committed Jul 24, 2023
1 parent 345cfee commit 090df4e
Show file tree
Hide file tree
Showing 6 changed files with 442 additions and 79 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,47 @@ let meals = try KeyValuDecoder().decode([String].self, from: ["fish", 1])
let user = try KeyValuDecoder().decode(User.self, from: [["id": 1, "name": "Herbert"], ["id:" 2])
```

## Nil Encoding/Decoding Strategy

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

The default strategy preserves `Optional.none`:

```swift
let encoder = KeyValueEncoder()
encoder.strategy = .default

// [1, 2, nil, 3]
let any = try encoder.encode([1, 2, Int?.none, 3])
```

Compatibility with [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) is preserved using a placeholder string:

```swift
encoder.strategy = .stringNull

// [1, 2, "$null", 3]
let any = try encoder.encode([1, 2, Int?.none, 3])
```

Compatibility with [`JSONSerialization`](https://developer.apple.com/documentation/foundation/jsonserialization) is preserved using [`NSNull`](https://developer.apple.com/documentation/foundation/nsnull):

```swift
encoder.strategy = .nsNull

// [1, 2, NSNull(), 3]
let any = try encoder.encode([1, 2, Int?.none, 3])
```

Nil values can also be completely removed:

```swift
encoder.strategy = .removed

// [1, 2, 3]
let any = try encoder.encode([1, 2, Int?.none, 3])
```

## UserDefaults
Encode and decode [`Codable`](https://developer.apple.com/documentation/swift/codable) types with [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults):

Expand Down
59 changes: 30 additions & 29 deletions Sources/KeyValueDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,26 @@ import CoreFoundation
public final class KeyValueDecoder {

public var userInfo: [CodingUserInfoKey: Any]
public var nilDecodingStrategy: NilDecodingStrategy = .default

public init () {
self.userInfo = [:]
}

public func decode<T: Decodable>(_ type: T.Type, from value: Any) throws -> T {
let container = SingleContainer(value: value, codingPath: [], userInfo: userInfo)
let container = SingleContainer(value: value, codingPath: [], userInfo: userInfo, nilDecodingStrategy: nilDecodingStrategy)
return try container.decode(type)
}

public typealias NilDecodingStrategy = KeyValueEncoder.NilEncodingStrategy
}

extension KeyValueDecoder {

// To assist in representing Optional<Any> because `Any` always casts to Optional<Any>
static func isValueNil(_ value: Any) -> Bool {
guard Mirror(reflecting: value).displayStyle == .optional else {
return value is NSNull
}

if case Optional<Any>.some = value {
return false
} else {
return true
}
static func makePlistCompatible() -> KeyValueDecoder {
let decoder = KeyValueDecoder()
decoder.nilDecodingStrategy = .stringNull
return decoder
}
}

Expand All @@ -80,14 +76,15 @@ private extension KeyValueDecoder {
let keyed = try KeyedContainer<Key>(
codingPath: codingPath,
storage: container.decode([String: Any].self),
userInfo: userInfo
userInfo: userInfo,
nilDecodingStrategy: container.nilDecodingStrategy
)
return KeyedDecodingContainer(keyed)
}

func unkeyedContainer() throws -> UnkeyedDecodingContainer {
let storage = try container.decode([Any].self)
return UnkeyedContainer(codingPath: codingPath, storage: storage, userInfo: userInfo)
return UnkeyedContainer(codingPath: codingPath, storage: storage, userInfo: userInfo, nilDecodingStrategy: container.nilDecodingStrategy)
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
Expand All @@ -99,25 +96,23 @@ private extension KeyValueDecoder {

let codingPath: [CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let nilDecodingStrategy: NilDecodingStrategy

private var value: Any

init(value: Any, codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any]) {
init(value: Any, codingPath: [CodingKey], userInfo: [CodingUserInfoKey: Any], nilDecodingStrategy: NilDecodingStrategy) {
self.value = value
self.codingPath = codingPath
self.userInfo = userInfo
self.nilDecodingStrategy = nilDecodingStrategy
}

func decodeNil() -> Bool {
KeyValueDecoder.isValueNil(value)
nilDecodingStrategy.isNull(value)
}

private var valueDescription: String {
if value is NSNull {
return "NSNull"
} else {
return decodeNil() ? "nil" : String(describing: type(of: value))
}
nilDecodingStrategy.isNull(value) ? "nil" : String(describing: type(of: value))
}

func getValue<T>(of type: T.Type = T.self) throws -> T {
Expand Down Expand Up @@ -289,11 +284,13 @@ private extension KeyValueDecoder {
let storage: [String: Any]
let codingPath: [CodingKey]
private let userInfo: [CodingUserInfoKey: Any]
private let nilDecodingStrategy: NilDecodingStrategy

init(codingPath: [CodingKey], storage: [String: Any], userInfo: [CodingUserInfoKey: Any]) {
init(codingPath: [CodingKey], storage: [String: Any], userInfo: [CodingUserInfoKey: Any], nilDecodingStrategy: NilDecodingStrategy) {
self.codingPath = codingPath
self.storage = storage
self.userInfo = userInfo
self.nilDecodingStrategy = nilDecodingStrategy
}

var allKeys: [Key] {
Expand All @@ -313,7 +310,7 @@ private extension KeyValueDecoder {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Dictionary does not contain key \(keyPath)")
throw DecodingError.keyNotFound(key, context)
}
return SingleContainer(value: value, codingPath: path, userInfo: userInfo)
return SingleContainer(value: value, codingPath: path, userInfo: userInfo, nilDecodingStrategy: nilDecodingStrategy)
}

func contains(_ key: Key) -> Bool {
Expand Down Expand Up @@ -389,7 +386,8 @@ private extension KeyValueDecoder {
let keyed = try KeyedContainer<NestedKey>(
codingPath: container.codingPath,
storage: container.decode([String: Any].self),
userInfo: userInfo
userInfo: userInfo,
nilDecodingStrategy: nilDecodingStrategy
)
return KeyedDecodingContainer<NestedKey>(keyed)
}
Expand All @@ -399,12 +397,13 @@ private extension KeyValueDecoder {
return try UnkeyedContainer(
codingPath: container.codingPath,
storage: container.decode([Any].self),
userInfo: userInfo
userInfo: userInfo,
nilDecodingStrategy: nilDecodingStrategy
)
}

func superDecoder() throws -> Swift.Decoder {
let container = SingleContainer(value: storage, codingPath: codingPath, userInfo: userInfo)
let container = SingleContainer(value: storage, codingPath: codingPath, userInfo: userInfo, nilDecodingStrategy: nilDecodingStrategy)
return Decoder(container: container)
}

Expand All @@ -419,11 +418,13 @@ private extension KeyValueDecoder {

let storage: [Any]
private let userInfo: [CodingUserInfoKey: Any]
private let nilDecodingStrategy: NilDecodingStrategy

init(codingPath: [CodingKey], storage: [Any], userInfo: [CodingUserInfoKey: Any]) {
init(codingPath: [CodingKey], storage: [Any], userInfo: [CodingUserInfoKey: Any], nilDecodingStrategy: NilDecodingStrategy) {
self.codingPath = codingPath
self.storage = storage
self.userInfo = userInfo
self.nilDecodingStrategy = nilDecodingStrategy
self.currentIndex = storage.startIndex
}

Expand All @@ -444,7 +445,7 @@ private extension KeyValueDecoder {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Array does not contain index \(keyPath)")
throw DecodingError.keyNotFound(AnyCodingKey(intValue: currentIndex), context)
}
return SingleContainer(value: storage[currentIndex], codingPath: path, userInfo: userInfo)
return SingleContainer(value: storage[currentIndex], codingPath: path, userInfo: userInfo, nilDecodingStrategy: nilDecodingStrategy)
}

mutating func decodeNext<T: Decodable>(of type: T.Type = T.self) throws -> T {
Expand Down Expand Up @@ -532,7 +533,7 @@ private extension KeyValueDecoder {
}

mutating func superDecoder() -> Swift.Decoder {
let container = SingleContainer(value: storage, codingPath: codingPath, userInfo: userInfo)
let container = SingleContainer(value: storage, codingPath: codingPath, userInfo: userInfo, nilDecodingStrategy: nilDecodingStrategy)
return Decoder(container: container)
}
}
Expand Down
Loading

0 comments on commit 090df4e

Please sign in to comment.