Created
December 4, 2021 02:41
-
-
Save jarrodparkes/ab31df65840e2052ca340d48e5a817b2 to your computer and use it in GitHub Desktop.
ExplicitNullEncodable.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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