I have been playing around with Codable
and reading and writing JSON from and to a file. Now I would like to write a custom Coder
that can read and wri
A bit late to the party here but I feel this might be helpful/informative to others given the question high vote count. (But I won't really get into the actual usefulness of such code in practice—please check the comments above for that.)
Unfortunately, given the coding stack flexibility and type-safeness, implementing a new encoding and decoding solution, for an alternative external representation, is far from a trivial task...
Let's start by implementing the encoding part for the desired strings file external representation. (The necessary types will be introduced in a top-down approach.)
Like the standard JSONEncoder
class we need to introduce a class to expose/drive our new encoding API. Let's call that StringsEncoder
:
/// An object that encodes instances of a data type
/// as strings following the simple strings file format.
public class StringsEncoder {
/// Returns a strings file-encoded representation of the specified value.
public func encode<T: Encodable>(_ value: T) throws -> String {
let stringsEncoding = StringsEncoding()
try value.encode(to: stringsEncoding)
return dotStringsFormat(from: stringsEncoding.data.strings)
}
private func dotStringsFormat(from strings: [String: String]) -> String {
var dotStrings = strings.map { "\"\($0)\" = \"\($1)\";" }
dotStrings.sort()
dotStrings.insert("/* Generated by StringsEncoder */", at: 0)
return dotStrings.joined(separator: "\n")
}
}
Next, we need to provide a type (e.g., a struct
) conforming to the core Encoder
protocol:
fileprivate struct StringsEncoding: Encoder {
/// Stores the actual strings file data during encoding.
fileprivate final class Data {
private(set) var strings: [String: String] = [:]
func encode(key codingKey: [CodingKey], value: String) {
let key = codingKey.map { $0.stringValue }.joined(separator: ".")
strings[key] = value
}
}
fileprivate var data: Data
init(to encodedData: Data = Data()) {
self.data = encodedData
}
var codingPath: [CodingKey] = []
let userInfo: [CodingUserInfoKey : Any] = [:]
func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
var container = StringsKeyedEncoding<Key>(to: data)
container.codingPath = codingPath
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath
return container
}
func singleValueContainer() -> SingleValueEncodingContainer {
var container = StringsSingleValueEncoding(to: data)
container.codingPath = codingPath
return container
}
}
Finally, we need to handle all 3 encoding containers types:
KeyedEncodingContainer
UnkeyedEncodingContainer
SingleValueEncodingContainer
fileprivate struct StringsKeyedEncoding<Key: CodingKey>: KeyedEncodingContainerProtocol {
private let data: StringsEncoding.Data
init(to data: StringsEncoding.Data) {
self.data = data
}
var codingPath: [CodingKey] = []
mutating func encodeNil(forKey key: Key) throws {
data.encode(key: codingPath + [key], value: "nil")
}
mutating func encode(_ value: Bool, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: String, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value)
}
mutating func encode(_ value: Double, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Float, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Int, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Int8, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Int16, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Int32, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: Int64, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: UInt, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: UInt8, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: UInt16, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: UInt32, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode(_ value: UInt64, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}
mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath.append(key)
try value.encode(to: stringsEncoding)
}
mutating func nestedContainer<NestedKey: CodingKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
var container = StringsKeyedEncoding<NestedKey>(to: data)
container.codingPath = codingPath + [key]
return KeyedEncodingContainer(container)
}
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath + [key]
return container
}
mutating func superEncoder() -> Encoder {
let superKey = Key(stringValue: "super")!
return superEncoder(forKey: superKey)
}
mutating func superEncoder(forKey key: Key) -> Encoder {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath + [key]
return stringsEncoding
}
}
fileprivate struct StringsUnkeyedEncoding: UnkeyedEncodingContainer {
private let data: StringsEncoding.Data
init(to data: StringsEncoding.Data) {
self.data = data
}
var codingPath: [CodingKey] = []
private(set) var count: Int = 0
private mutating func nextIndexedKey() -> CodingKey {
let nextCodingKey = IndexedCodingKey(intValue: count)!
count += 1
return nextCodingKey
}
private struct IndexedCodingKey: CodingKey {
let intValue: Int?
let stringValue: String
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = intValue.description
}
init?(stringValue: String) {
return nil
}
}
mutating func encodeNil() throws {
data.encode(key: codingPath + [nextIndexedKey()], value: "nil")
}
mutating func encode(_ value: Bool) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: String) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value)
}
mutating func encode(_ value: Double) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Float) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Int) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Int8) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Int16) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Int32) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: Int64) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: UInt) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: UInt8) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: UInt16) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: UInt32) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode(_ value: UInt64) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}
mutating func encode<T: Encodable>(_ value: T) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath + [nextIndexedKey()]
try value.encode(to: stringsEncoding)
}
mutating func nestedContainer<NestedKey: CodingKey>(
keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
var container = StringsKeyedEncoding<NestedKey>(to: data)
container.codingPath = codingPath + [nextIndexedKey()]
return KeyedEncodingContainer(container)
}
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath + [nextIndexedKey()]
return container
}
mutating func superEncoder() -> Encoder {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath.append(nextIndexedKey())
return stringsEncoding
}
}
fileprivate struct StringsSingleValueEncoding: SingleValueEncodingContainer {
private let data: StringsEncoding.Data
init(to data: StringsEncoding.Data) {
self.data = data
}
var codingPath: [CodingKey] = []
mutating func encodeNil() throws {
data.encode(key: codingPath, value: "nil")
}
mutating func encode(_ value: Bool) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: String) throws {
data.encode(key: codingPath, value: value)
}
mutating func encode(_ value: Double) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Float) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Int) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Int8) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Int16) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Int32) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: Int64) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: UInt) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: UInt8) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: UInt16) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: UInt32) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode(_ value: UInt64) throws {
data.encode(key: codingPath, value: value.description)
}
mutating func encode<T: Encodable>(_ value: T) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath
try value.encode(to: stringsEncoding)
}
}
Obviously, I made some design decisions regarding how to encode nested types using the (very!) simple strings file format. Hopefully, my code is clear enough that it should be easy to tweak the encoding details if so desired.
A simple test for a trivial Codable
type:
struct Product: Codable {
var name: String
var price: Float
var info: String
}
let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")
let stringsEncoder = StringsEncoder()
do {
let stringsFile = try stringsEncoder.encode(iPhone)
print(stringsFile)
} catch {
print("Encoding failed: \(error)")
}
Output:
/* Generated by StringsEncoder */
"info" = "Our best iPhone yet!";
"name" = "iPhone X";
"price" = "1000.0";
A more complex test with nested structs and arrays:
struct Product: Codable {
var name: String
var price: Float
var info: String
}
struct Address: Codable {
var street: String
var city: String
var state: String
}
struct Store: Codable {
var name: String
var address: Address // nested struct
var products: [Product] // array
}
let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")
let macBook = Product(name: "Mac Book Pro", price: 2_000, info: "Early 2019")
let watch = Product(name: "Apple Watch", price: 500, info: "Series 4")
let appleStore = Store(
name: "Apple Store",
address: Address(street: "300 Post Street", city: "San Francisco", state: "CA"),
products: [iPhone, macBook, watch]
)
let stringsEncoder = StringsEncoder()
do {
let stringsFile = try stringsEncoder.encode(appleStore)
print(stringsFile)
} catch {
print("Encoding failed: \(error)")
}
Output:
/* Generated by StringsEncoder */
"address.city" = "San Francisco";
"address.state" = "CA";
"address.street" = "300 Post Street";
"name" = "Apple Store";
"products.0.info" = "Our best iPhone yet!";
"products.0.name" = "iPhone X";
"products.0.price" = "1000.0";
"products.1.info" = "Early 2019";
"products.1.name" = "Mac Book Pro";
"products.1.price" = "2000.0";
"products.2.info" = "Series 4";
"products.2.name" = "Apple Watch";
"products.2.price" = "500.0";
Given how big this answer already is, I'm gonna to leave the decoding part (i.e., creating the StringsDecoder
class, conforming to the Decoder
protocol, etc) as an exercise to the reader... please let me know if you guys need any help with that and I will post a complete solution later ;)