0

I have an application that stores some configuration options which I'd like to write out to a JSON file.

Simplified version of my app's config/options structs and JSON encoding ...

struct AppOptions: OptionSet, Encodable {
  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]
}

struct AppConfig: Encodable {
  var configName: String
  var options: AppOptions
}

let appCfg = AppConfig(configName: "SomeConfig", options: [ .optA, .optC ])

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(appCfg)
print(String(decoding: data, as: UTF8.self) )
//  {
//    "configName" : "SomeConfig",
//    "options" : 5
//  }

So although this works the generated JSON file is not particularly user friendly as the options are just saved as the variables raw value -> "options": 5.

I'd prefer the encoder to generate more user friendly JSON such that the options are written more like an array of elements, as follows...

  {
    "configName" : "SomeConfig",
    "options" : [ "optA", "optC" ]
  }

I am somewhat at a dead-end figuring out how to create the custom encode(to: ) required to achieve this, suggestions or solutions please.


Just for some additional context, I have already got a solution for the decode part to read the config from JSON file into my app, it is just the encode back to a JSON file that I need a solution for.

Code, including decode part ...

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()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(appCfg)
print(String(decoding: data, as: UTF8.self) )
//  {
//    "configName" : "SomeConfig",
//    "options" : 5
//  }
//  needs to be...
//  {
//    "configName" : "SomeConfig",
//    "options" : [ "optA", "optC" ]
//  }
KieranC
  • 57
  • 6

1 Answers1

1

You could do something like this but I'm not sure if you are using an OptionSet the way it's supposed to.

func encode(to encoder: Encoder) throws {
  var container = encoder.unkeyedContainer()

  let optionsRaw: [String]
  if self == .all {
    optionsRaw = ["all"]
  } else {
    optionsRaw = Self.mapping
      .filter { $0.key != "all" }
      .compactMap { self.contains($0.value) ? $0.key : nil }
      .sorted() // if sorting is important
  }
  try container.encode(contentsOf:  optionsRaw)
}

This handles the all in a way that only ["all"] is encoded and also sorts the keys in case it's a subset.

fruitcoder
  • 1,073
  • 8
  • 24
  • This solution works for me. Just to clarify, I am using OptionSet as intended, these options are set by the user in a config file (config filename passed as command line arg) to tell the app which output data files to generate following some data crunching by the app. So although my app only needs to decode the JSON config file to get the user's preferences, I have implemented a feature where the app will generate a 'example' config file in the event that the user does not specify any config file in the command line args. Sorting the options is not strictly necessary, but is a nice touch. – KieranC Nov 04 '22 at 15:41