sindresorhus / Defaults

💾 Swifty and modern UserDefaults
https://swiftpackageindex.com/sindresorhus/Defaults/documentation/defaults
MIT License
1.93k stars 115 forks source link

Improve the API for choosing the type of serialization #162

Open sindresorhus opened 5 months ago

sindresorhus commented 5 months ago

Current:

extension Foo: Defaults.Serializable, Defaults.PreferRawRepresentable {}

I think this may be better:

extension Foo: Defaults.Serializable.RawRepresentable {}

This is shorter, but more importantly, it makes the choice explicit. Users should prefer this over Defaults.Serializable. Defaults.Serializable has a big flaw. For example, you have a struct X: Codable, Defaults.Serializable. You save data with it, but later on, you add a RawRepresentable conformance. The previous persisted version of the struct can no longer be loaded as it's saved using the Codable bridge, but now tries to load using the RawRepresentable bridge.

// @hank121314 Thoughts? Any better way to prevent this problem?

hank121314 commented 5 months ago

You save data with it, but later on, you add a RawRepresentable conformance. The previous persisted version of the struct can no longer be loaded as it's saved using the Codable bridge.

For this case, we could consider enhancing the RawRepresentableCodableBridge. Since its Value conforms to both Codable and RawRepresentable, we could test which deserialization can be loaded. If the user switches the protocol from Codable to RawRepresentable, there may be a need for some migration.

sindresorhus commented 5 months ago

Since its Value conforms to both Codable and RawRepresentable, we could test which deserialization can be loaded.

A value could potentially be loadable by both. This creates ambiguity and potential data-loss.

If the user switches the protocol from Codable to RawRepresentable, there may be a need for some migration.

Yes, that's a different case and unrelated to this issue.

hank121314 commented 5 months ago

A value could potentially be loadable by both. This creates ambiguity and potential data-loss.

How about change the deserialize function in RawRepresentableCodableBridge to this?

public func deserialize(_ object: Serializable?) -> Value? {
        // Check whether it can deserialize with `CodableBridge`
        if 
            let jsonString = object as? String,
            let value = try? Value(jsonString: jsonString) 
        {
                return value
        }
        guard let object = object as Value.RawValue else {
            return nil
        }
        return Value(rawValue: object)
}

It will accept serialization values from both CodableBridge and RawRepresentableBridge.

sindresorhus commented 5 months ago

To clarify my point. The potential problem is that the raw serialized value could be a string from a RawRepresentable serialization, but when deserialized using Codable it turns into a different value. I don't think we should guess the user's intent. It's better to be explicit about what you want.

In addition, trying to use Codable each time deserialize() is called would add a lot of unnecessary overhead, since Codable is slow.

hank121314 commented 5 months ago

Get your point. I think we might need to allow the user to choose the Bridge they want to use for serialization and deserialization. Here are the few solutions I come up:

  1. Remove all ambiguous conformance in Defaults, and expose the internal bridge publicly to allow users to choose it.
private enum FixtureCodableEnum: String, Hashable, Codable, Defaults.Serializable {
    case tenMinutes = "10 Minutes"
    case halfHour = "30 Minutes"
    case oneHour = "1 Hour"
}

extension FixtureCodableEnum {
    typealias Bridge = Defaults.RawRepresentableCodableBridge<Self>;
}
  1. Check conformance during Defaults.Key.init and issue an Xcode warning advising users to utilize Defaults.Serializable.RawRepresentable instead.
// in `Defaults.Key.init`
if defaultValue is Codable, defaultValue is any RawRepresentable {
    runtimeWarn(false, "Please consider using `Defaults.Serializable.RawRepresentable` for serialization/deserialization stability")
}
sindresorhus commented 5 months ago

Why not?

private enum FixtureCodableEnum: String, Hashable, Codable, Defaults.Serializable.RawRepresentable {
    case tenMinutes = "10 Minutes"
    case halfHour = "30 Minutes"
    case oneHour = "1 Hour"
}
hank121314 commented 5 months ago

Why not?

Yeah, of course we can do this, but we might need to warn user when they are using ambiguous bridge to do the serialization and deserialization. With this user will need to specify the bridge they want to use.

sindresorhus commented 5 months ago

I think 2 would be the best choice.

hank121314 commented 5 months ago

I think 2 would be the best choice.

Got it! Many thanks! With option 2, I think there might be a need to also having a Defaults.Serializable.Codable?

sindresorhus commented 5 months ago

With option 2, I think there might be a need to also having a Defaults.Serializable.Codable?

Yes. And Defaults.Serializable.NSSecureCoding.