I am making a command line app that loads a JSON config file in which the user can specify various options.
In my code these options are defined as an OptionSet.
Initially, I had problems decoding the JSON file as I wanted to use more user friendly syntax in the JSON file such that I could replace "options": 5 with "options": [ ".optA", ".optC" ]. I managed to get this part to work using a custom init(from: Decoder) and mapping json string to the required option.
But I still have a problem with the encode side as I’m not sure how to generate the option strings in the encoded JSON. Pretty sure I need to implement CodingKeys but I’m not sure how.
Any help / suggestions would be very welcome.
Xcode Playground code…
import Foundation
struct AppOptions: OptionSet {
let rawValue: Int
static let optA = AppOptions(rawValue: 1 << 0)
static let optB = AppOptions(rawValue: 1 << 1)
static let optC = AppOptions(rawValue: 1 << 2)
static let all: AppOptions = [.optA, .optB, .optC]
}
extension AppOptions: Codable {
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var result: AppOptions = []
while !container.isAtEnd {
let optionName = try container.decode(String.self)
guard let opt = AppOptions.mapping[optionName] else {
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Option not recognised: \(optionName)")
throw DecodingError.typeMismatch(String.self, context)
}
result.insert(opt)
}
self = result
}
// func encode(to encoder: Encoder) throws {
// // What to do here?
// }
private static let mapping: [String: AppOptions] = [
".optA" : .optA,
".optB" : .optB,
".optC" : .optC,
".all" : .all
]
}
struct AppConfig: Codable {
var configName: String
var options: AppOptions
}
var json = """
{
"configName": "SomeConfig",
"options": [ ".optA", ".optC" ]
}
"""
let decoder = JSONDecoder()
var appCfg = try decoder.decode(AppConfig.self, from: Data(json.utf8))
print(appCfg)
//Correct -> AppConfig(configName: "SomeConfig", options: __lldb_expr_115.AppOptions(rawValue: 5))
let encoder = JSONEncoder()
appCfg.options = [ .optA, .optC ]
let data = try encoder.encode(appCfg)
print(String(decoding: data, as: UTF8.self) )
//Wrong -> {"configName":"SomeConfig", "options":5}
//Needs to be -> {"configName":"SomeConfig", "options": [".optA", ".optC"]}
Thanks for answering, but maybe I should rephrase the question/subject of the post.
The problem that I want to solve is: How to implement Encodable for an OptionSet data type, to allow a more user-friendly syntax in a JSON file?.
Your suggested solution, although it does provide a work-around to get data in and out of the JSON file in the format that I want by using an enum, but it specifically avoids using the OptionSet data type.
I’d prefer to find a solution that uses OptionSet, otherwise I may have quite a lot of code refactoring to do!
Before posting the OP, I did figure out how to implement decode for OptionSet such that I can read the JSON file correctly (code for that is in the OP), it is the encode part is giving me the problem.
Currently my code will successfully decode the options in the JSON file into the options: OptionSet variable in my app, but when going the other way to encode the options variable (e.g. where the selected options are .optA and .optC) I get the following, which is not desirable or consistent.
The (dot) . is what will cause a parse error. @jayantvarma 's example addresses the parsing, giving you a best possible scenario, which is similar to what you want, without the dot prefix.
Using (dot). in the JSON config file is not the issue, that already works and is parsed without a problem.
TBH, I’m not fixed on the (dot). notation in the JSON file, I could very easily switch .optA for just optA in the JSON file, the mapping array used in decode just needs to be updated accordingly.
But getting back to the original question.
Suppose I have a variable of type OptionSet, appConfig.options = [ .optA, .optC ].
If I encode this using the default encoding behaviour I would get "options": 5 in the JSON file, whereas I’d like to get "options": [ "optA", "optC" ], so how would I write a custom encode(to: ) to make this work?