Skip to content

Instantly share code, notes, and snippets.

@jarrodparkes
Created December 4, 2021 02:41
Show Gist options
  • Save jarrodparkes/ab31df65840e2052ca340d48e5a817b2 to your computer and use it in GitHub Desktop.
Save jarrodparkes/ab31df65840e2052ca340d48e5a817b2 to your computer and use it in GitHub Desktop.
ExplicitNullEncodable.swift
import Foundation
// Proposal...
/// A value that can be included in a payload (`.explicitNone` or `.some`)
/// or completely absent (`.none`). Intended for request payloads.
public enum ExplicitNullEncodable<Wrapped> {
case none
case explicitNone
case some(Wrapped)
}
extension ExplicitNullEncodable: Codable where Wrapped: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Wrapped.self) {
self = .some(value)
} else if container.decodeNil() {
self = .explicitNone
} else {
self = .none
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .none: return
case .explicitNone: try container.encodeNil()
case .some(let wrapped): try container.encode(wrapped)
}
}
}
// Tester methods...
func testEncodable<E: Encodable>(encodable: E, printJson: Bool = false) throws -> Data {
let encoder = JSONEncoder()
let data = try encoder.encode(encodable)
if let jsonString = String(data: data, encoding: .utf8), printJson {
print(jsonString)
}
return data
}
func testDecodable<D: Decodable>(type: D.Type, data: Data) throws -> D {
let decoder = JSONDecoder()
return try decoder.decode(type, from: data)
}
// Example payload...
struct TaskUpdatePayload: Codable, CustomDebugStringConvertible {
let dueAt: ExplicitNullEncodable<String>
public enum CodingKeys: String, CodingKey, CaseIterable {
case dueAt = "due_at"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch dueAt {
case .none:
break
case .explicitNone, .some:
try container.encode(dueAt, forKey: .dueAt)
}
}
var debugDescription: String {
return "{\"due_at\": .\(dueAt)}"
}
}
// Test...
do {
// `dueAt` = `.none` => {}
var payload = TaskUpdatePayload(dueAt: .none)
var data = try testEncodable(encodable: payload, printJson: true)
if let _ = try? testDecodable(type: TaskUpdatePayload.self, data: data) {
print("shouldn't happen, because unable to decode since 'dueAt' key does not exist")
} else {
print("{\"due_at\": .none}\n")
}
// `dueAt` = `.explicitNone` => {"due_at":null}
payload = TaskUpdatePayload(dueAt: .explicitNone)
data = try testEncodable(encodable: payload, printJson: true)
print("\(try testDecodable(type: TaskUpdatePayload.self, data: data))\n")
// `dueAt` = `.some("2021-06-30T21:30:00Z")` => {"due_at":"2021-06-30T21:30:00Z"}
payload = TaskUpdatePayload(dueAt: .some("2021-06-30T21:30:00Z"))
data = try testEncodable(encodable: payload, printJson: true)
print("\(try testDecodable(type: TaskUpdatePayload.self, data: data))\n")
} catch(let error) {
print(error)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment