It seems like it would be cleaner to extend CodingKey. There might be a more
general way of doing this than just requiring a Dictionary, but it seems to
work.
protocol DefaultingCodingKey: CodingKey, Hashable {
static var defaults: [Self: Any] { get }
}
// Implementing the other overrides left as an exercise to the reader
extension KeyedDecodingContainer where Key: DefaultingCodingKey {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
if let t = try self.decodeIfPresent(type, forKey: key) {
return t
} else {
return Swift.type(of: key).defaults[key] as! String
}
}
func decode<T: Codable>(_ type: T.Type, forKey key: Key) throws -> T {
if let t = try self.decodeIfPresent(type, forKey: key) {
return t
} else {
return Swift.type(of: key).defaults[key] as! T
}
}
}
extension KeyedEncodingContainer where Key: DefaultingCodingKey {
mutating func encode(_ value: String, forKey key: Key) throws {
guard value != type(of: key).defaults[key] as! String else { return }
try self.encodeIfPresent(value, forKey: key)
}
mutating func encode<T: Encodable & Equatable>(_ value: [T], forKey key:
Key) throws {
guard value != type(of: key).defaults[key] as! [T] else { return }
try self.encodeIfPresent(value, forKey: key)
}
mutating func encode<T: Encodable & Equatable>(_ value: T, forKey key: Key)
throws {
guard value != type(of: key).defaults[key] as! T else { return }
try self.encodeIfPresent(value, forKey: key)
}
}
class ReferencePieceFromModel: Codable {
public var name: String = ""
public var styles: [String] = []
private enum CodingKeys: String, DefaultingCodingKey {
case name, styles
static let defaults: [CodingKeys: Any] = [
.name: "",
.styles: [String]()
]
}
}
Putting all of this into a playground….
let x = ReferencePieceFromModel()
let encoder = JSONEncoder()
let json = try! encoder.encode(x)
print(String(data: json, encoding: .utf8)!)
let decoder = JSONDecoder()
let a = try! decoder.decode(ReferencePieceFromModel.self, from: json)
print(a.name)
print(a.styles)
let refWithName = "{\"name\": \"Randy\"}"
let b = try! decoder.decode(ReferencePieceFromModel.self, from:
refWithName.data(using: .utf8)!)
print(b.name)
print(b.styles)
let ref = "{\"name\": \"Randy\", \"styles\": [\"Swifty\"]}"
let c = try! decoder.decode(ReferencePieceFromModel.self, from: ref.data(using:
.utf8)!)
print(c.name)
print(c.styles)
Prints out…
{}
[]
Randy
[]
Randy
["Swifty"]
--
Randy
> On Jul 10, 2017, at 8:16 PM, William Shipley via swift-evolution
> <[email protected]> wrote:
>
> Automatic substitution / removal of default values is very useful when
> reading or writing a file, respectively, and should be supported by the
> <Codable> family of protocols and objects:
>
> • When reading, swapping in a default value for missing or corrupted values
> makes it so hand-created or third-party-created files don’t have to write
> every single value to make a valid file, and allows slightly corrupted files
> to auto-repair (or get close, and let the user fix up any data that needs it
> after) rather than completely fail to load. (Repairing on read creates a
> virtuous cycle with user-created files, as the user will get _some_ feedback
> on her input even if she’s messed up, for example, the type of one of the
> properties.)
>
> • When writing, providing a default value allows the container to skip keys
> that don’t contain useful information. This can dramatically reduce file
> sizes, but I think its other advantages are bigger wins: just like having
> less source code makes a program easier to debug, having less “data code”
> makes files easier to work with in every way — they’re easier to see
> differences in, easier to determine corruption in, easier to edit by hand,
> and easier to learn from.
>
>
> My first pass attempt at adding defaults to Codable looks like this:
>
>
> public class ReferencePieceFromModel : Codable {
>
> // MARK: properties
> public let name: String = ""
> public let styles: [String] = []
>
>
> // MARK: <Codable>
> public required init(from decoder: Decoder) throws {
> let container = try decoder.container(keyedBy: CodingKeys.self)
>
> self.name = container.decode(String.self, forKey: .name, defaults:
> type(of: self).defaultsByCodingKey)
> self.styles = container.decode([String].self, forKey: .styles,
> defaults: type(of: self).defaultsByCodingKey)
> }
> public func encode(to encoder: Encoder) throws {
> var container = encoder.container(keyedBy: CodingKeys.self)
>
> try container.encode(name, forKey: .name, defaults: type(of:
> self).defaultsByCodingKey)
> try container.encode(styles, forKey: .styles, defaults: type(of:
> self).defaultsByCodingKey)
> }
> private static let defaultsByCodingKey: [CodingKeys : Any] = [
> .name : "",
> .styles : [String]()
> ]
>
>
> // MARK: private
> private enum CodingKeys : String, CodingKey {
> case name
> case styles
> }
> }
>
> With just a couple additions to the Swift libraries:
>
> extension KeyedDecodingContainer where Key : Hashable {
> func decode<T>(_ type: T.Type, forKey key: Key, defaults: [Key : Any]) ->
> T where T : Decodable {
> if let typedValueOptional = try? decodeIfPresent(T.self, forKey:
> key), let typedValue = typedValueOptional {
> return typedValue
> } else {
> return defaults[key] as! T
> }
> }
> }
>
> extension KeyedEncodingContainer where Key : Hashable {
> mutating func encode<T>(_ value: T, forKey key: Key, defaults: [Key :
> Any]) throws where T : Encodable & Equatable {
> if value != (defaults[key] as! T) {
> try encode(value, forKey: key)
> }
> }
>
> mutating func encode<T>(_ value: [T], forKey key: Key, defaults: [Key :
> Any]) throws where T : Encodable & Equatable { // I AM SO SORRY THIS IS ALL I
> COULD FIGURE OUT TO MAKE [String] WORK!
> if value != (defaults[key] as! [T]) {
> try encode(value, forKey: key)
> }
> }
> }
>
>
> (Note the horrible hack on KeyedEncodingContainer where I had to special-case
> arrays of <Equatable>s, I guess because the compiler doesn’t know an array of
> <Equatable>s is Equatable itself?)
>
>
> Problems with this technique I’ve identified are:
>
> ⑴ It doesn’t allow one to add defaults without manually writing the
> init(from:) and encode(to:), ugh.
> ⑵ The programmer has to add 'type(of: self).defaultsByCodingKey’ to every
> call, ugh.
>
> Both of these could possibly be worked around if we could add an optional
> method to the <Codable> protocol, that would look something like:
>
> public static func default<Key>(keyedBy type: Key.Type, key: Key) -> Any?
> where Key : CodingKey
>
> (the above line isn’t tested and doubtlessly won’t work as typed and has tons
> of think-os.)
>
> This would get called by KeyedEncodingContainers and KeyedDecodingContainers
> only for keys that are Hashable (which I think is all keys, but you can stick
> un-keyed sub-things in Keyed containers and obviously those can’t have
> defaults just for them) and the container would be asked to do the comparison
> itself, with ‘==‘.
>
> Something I haven’t tried to address here is what to do if values are NOT
> <Equatable> — then of course ‘==‘ won’t work. One approach to this would be
> to provide a way for the static func above to return ‘Hey, I don’t have
> anything meaningful for you for this particular property, because it’s not
> Equatable.’ This could be as simple as returning ‘nil’, which would also be a
> decent way to say, “This property has no meaningful default” which is also
> needed.
>
> Alternatively, one could imagine adding TWO callbacks in the <Codable> for
> this kind of case, which are essentially *WAVES HANDS*:
>
> public static func isThisValueTheDefault(_ value: Any, forKey key:
> Self.Key) throws -> Any?
> public static func defaultValue<Key>(keyedBy type: Key.Type, key: Key)
> -> Any? where Key : CodingKey
>
> These might also need a 'keyedBy type: Key.Type’ parameter — to be honest I
> haven’t messed with different key spaces so I’m not sure how they work. Also
> I’m not the best at generics yet. (At this point I’m not even sure if
> protocols can contain ‘class’ functions, so maybe none of this would work.)
>
> Another advantage to the two-method approach (besides not requiring the
> values to be < Equatable >) is that it allows one to provide defaults for
> floating values, which can often be changed just by floating-point error by
> like 0.00000000001 and then end up registering false changes. In the
> isValueDefault(…) the programmer could implement a comparison with a ‘slop’
> so if the encoder were about to write 0.000000000001 and the default were 0
> nothing would be written.
>
>
> -Wil
>
> _______________________________________________
> swift-evolution mailing list
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution