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

Reply via email to