diff --git a/Sources/Defaults/Defaults+Bridge.swift b/Sources/Defaults/Defaults+Bridge.swift index e65c38d..3485f45 100644 --- a/Sources/Defaults/Defaults+Bridge.swift +++ b/Sources/Defaults/Defaults+Bridge.swift @@ -337,6 +337,59 @@ extension Defaults { } } +extension Defaults { + public struct RangeBridge: Bridge { + public typealias Value = T + public typealias Serializable = [Any] + typealias Bound = T.Bound + + public func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + if Bound.isNativelySupportedType { + return [value.lowerBound, value.upperBound] + } + + guard + let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value), + let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value) + else { + return nil + } + + return [lowerBound, upperBound] + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let object = object else { + return nil + } + + if Bound.isNativelySupportedType { + guard + let lowerBound = object[safe: 0] as? Bound, + let upperBound = object[safe: 1] as? Bound + else { + return nil + } + + return .init(uncheckedBounds: (lower: lowerBound, upper: upperBound)) + } + + guard + let lowerBound = Bound.bridge.deserialize(object[safe: 0] as? Bound.Serializable) as? Bound, + let upperBound = Bound.bridge.deserialize(object[safe: 1] as? Bound.Serializable) as? Bound + else { + return nil + } + + return .init(uncheckedBounds: (lower: lowerBound, upper: upperBound)) + } + } +} + extension Defaults { @available(iOS 15.0, macOS 11.0, tvOS 15.0, watchOS 8.0, iOSApplicationExtension 15.0, macOSApplicationExtension 11.0, tvOSApplicationExtension 15.0, watchOSApplicationExtension 8.0, *) public struct ColorBridge: Bridge { diff --git a/Sources/Defaults/Defaults+Extensions.swift b/Sources/Defaults/Defaults+Extensions.swift index 1076fcd..8b636b3 100644 --- a/Sources/Defaults/Defaults+Extensions.swift +++ b/Sources/Defaults/Defaults+Extensions.swift @@ -146,6 +146,14 @@ extension Color: Defaults.Serializable { public static let bridge = Defaults.ColorBridge() } +extension Range: Defaults.RangeSerializable where Bound: Defaults.Serializable { + public static var bridge: Defaults.RangeBridge { Defaults.RangeBridge() } +} + +extension ClosedRange: Defaults.RangeSerializable where Bound: Defaults.Serializable { + public static var bridge: Defaults.RangeBridge { Defaults.RangeBridge() } +} + #if os(macOS) /** `NSColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge`. diff --git a/Sources/Defaults/Defaults+Protocol.swift b/Sources/Defaults/Defaults+Protocol.swift index 2cc086f..34dde75 100644 --- a/Sources/Defaults/Defaults+Protocol.swift +++ b/Sources/Defaults/Defaults+Protocol.swift @@ -128,3 +128,13 @@ By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it w */ public protocol DefaultsPreferRawRepresentable: RawRepresentable {} public protocol DefaultsPreferNSSecureCoding: NSSecureCoding {} + +// Essential properties for serializing and deserializing `ClosedRange` and `Range`. +public protocol DefaultsRange { + associatedtype Bound: Comparable, Defaults.Serializable + + var lowerBound: Bound { get } + var upperBound: Bound { get } + + init(uncheckedBounds: (lower: Bound, upper: Bound)) +} diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 4143e7f..2186b0a 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -24,6 +24,7 @@ public enum Defaults { public typealias PreferRawRepresentable = DefaultsPreferRawRepresentable public typealias PreferNSSecureCoding = DefaultsPreferNSSecureCoding public typealias Bridge = DefaultsBridge + public typealias RangeSerializable = DefaultsRange & DefaultsSerializable typealias CodableBridge = DefaultsCodableBridge // We cannot use `Key` as the container for keys because of "Static stored properties not supported in generic types". diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index b4fe795..2a2356b 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -160,6 +160,12 @@ extension Sequence { } } +extension Collection { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} + extension Defaults.Serializable { /** diff --git a/Tests/DefaultsTests/DefaultsRangeTests.swift b/Tests/DefaultsTests/DefaultsRangeTests.swift new file mode 100644 index 0000000..57683c6 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsRangeTests.swift @@ -0,0 +1,221 @@ +import Foundation +import Defaults +import XCTest + +private struct CustomDate { + let year: Int + let month: Int + let day: Int +} + +extension CustomDate: Defaults.Serializable { + public struct CustomDateBridge: Defaults.Bridge { + public typealias Value = CustomDate + public typealias Serializable = [Int] + + public func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return [value.year, value.month, value.day] + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let object = object else { + return nil + } + + return .init(year: object[0], month: object[1], day: object[2]) + } + } + + public static let bridge = CustomDateBridge() +} + +extension CustomDate: Comparable { + static func < (lhs: CustomDate, rhs: CustomDate) -> Bool { + if lhs.year != rhs.year { + return lhs.year < rhs.year + } else if lhs.month != rhs.month { + return lhs.month < rhs.month + } else { + return lhs.day < rhs.day + } + } + + static func == (lhs: CustomDate, rhs: CustomDate) -> Bool { + lhs.year == rhs.year && lhs.month == rhs.month + && lhs.day == rhs.day + } +} + +// Fixtures: +private let fixtureRange = 0..<10 +private let nextFixtureRange = 1..<20 +private let fixtureDateRange = CustomDate(year: 2022, month: 4, day: 0)..("independentRangeKey", default: fixtureRange) + XCTAssertEqual(fixtureRange.upperBound, Defaults[key].upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key].lowerBound) + Defaults[key] = nextFixtureRange + XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key].upperBound) + XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key].lowerBound) + + // Test serializable Range type + let dateKey = Defaults.Key>("independentRangeDateKey", default: fixtureDateRange) + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey].upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey].lowerBound) + Defaults[dateKey] = nextFixtureDateRange + XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey].upperBound) + XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey].lowerBound) + + // Test native support ClosedRange type + let closedKey = Defaults.Key("independentClosedRangeKey", default: fixtureClosedRange) + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey].upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey].lowerBound) + Defaults[closedKey] = nextFixtureClosedRange + XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey].upperBound) + XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey].lowerBound) + + // Test serializable ClosedRange type + let closedDateKey = Defaults.Key>("independentClosedRangeDateKey", default: fixtureDateClosedRange) + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey].upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey].lowerBound) + Defaults[closedDateKey] = nextFixtureDateClosedRange + XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey].upperBound) + XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey].lowerBound) + } + + func testOptionalKey() { + // Test native support Range type + let key = Defaults.Key?>("independentRangeOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureRange + XCTAssertEqual(fixtureRange.upperBound, Defaults[key]?.upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]?.lowerBound) + + // Test serializable Range type + let dateKey = Defaults.Key?>("independentRangeDateOptionalKey") + XCTAssertNil(Defaults[dateKey]) + Defaults[dateKey] = fixtureDateRange + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]?.upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]?.lowerBound) + + // Test native support ClosedRange type + let closedKey = Defaults.Key?>("independentClosedRangeOptionalKey") + XCTAssertNil(Defaults[closedKey]) + Defaults[closedKey] = fixtureClosedRange + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]?.upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]?.lowerBound) + + // Test serializable ClosedRange type + let closedDateKey = Defaults.Key?>("independentClosedRangeDateOptionalKey") + XCTAssertNil(Defaults[closedDateKey]) + Defaults[closedDateKey] = fixtureDateClosedRange + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]?.upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]?.lowerBound) + } + + func testArrayKey() { + // Test native support Range type + let key = Defaults.Key<[Range]>("independentRangeArrayKey", default: [fixtureRange]) + XCTAssertEqual(fixtureRange.upperBound, Defaults[key][0].upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key][0].lowerBound) + Defaults[key].append(nextFixtureRange) + XCTAssertEqual(fixtureRange.upperBound, Defaults[key][0].upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key][0].lowerBound) + XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key][1].upperBound) + XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key][1].lowerBound) + + // Test serializable Range type + let dateKey = Defaults.Key<[Range]>("independentRangeDateArrayKey", default: [fixtureDateRange]) + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey][0].upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey][0].lowerBound) + Defaults[dateKey].append(nextFixtureDateRange) + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey][0].upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey][0].lowerBound) + XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey][1].upperBound) + XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey][1].lowerBound) + + // Test native support ClosedRange type + let closedKey = Defaults.Key<[ClosedRange]>("independentClosedRangeArrayKey", default: [fixtureClosedRange]) + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey][0].upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey][0].lowerBound) + Defaults[closedKey].append(nextFixtureClosedRange) + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey][0].upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey][0].lowerBound) + XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey][1].upperBound) + XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey][1].lowerBound) + + // Test serializable ClosedRange type + let closedDateKey = Defaults.Key<[ClosedRange]>("independentClosedRangeDateArrayKey", default: [fixtureDateClosedRange]) + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey][0].upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey][0].lowerBound) + Defaults[closedDateKey].append(nextFixtureDateClosedRange) + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey][0].upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey][0].lowerBound) + XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey][1].upperBound) + XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey][1].lowerBound) + } + + func testDictionaryKey() { + // Test native support Range type + let key = Defaults.Key<[String: Range]>("independentRangeDictionaryKey", default: ["0": fixtureRange]) + XCTAssertEqual(fixtureRange.upperBound, Defaults[key]["0"]?.upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]["0"]?.lowerBound) + Defaults[key]["1"] = nextFixtureRange + XCTAssertEqual(fixtureRange.upperBound, Defaults[key]["0"]?.upperBound) + XCTAssertEqual(fixtureRange.lowerBound, Defaults[key]["0"]?.lowerBound) + XCTAssertEqual(nextFixtureRange.upperBound, Defaults[key]["1"]?.upperBound) + XCTAssertEqual(nextFixtureRange.lowerBound, Defaults[key]["1"]?.lowerBound) + + // Test serializable Range type + let dateKey = Defaults.Key<[String: Range]>("independentRangeDateDictionaryKey", default: ["0": fixtureDateRange]) + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]["0"]?.upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]["0"]?.lowerBound) + Defaults[dateKey]["1"] = nextFixtureDateRange + XCTAssertEqual(fixtureDateRange.upperBound, Defaults[dateKey]["0"]?.upperBound) + XCTAssertEqual(fixtureDateRange.lowerBound, Defaults[dateKey]["0"]?.lowerBound) + XCTAssertEqual(nextFixtureDateRange.upperBound, Defaults[dateKey]["1"]?.upperBound) + XCTAssertEqual(nextFixtureDateRange.lowerBound, Defaults[dateKey]["1"]?.lowerBound) + + // Test native support ClosedRange type + let closedKey = Defaults.Key<[String: ClosedRange]>("independentClosedRangeDictionaryKey", default: ["0": fixtureClosedRange]) + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]["0"]?.upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]["0"]?.lowerBound) + Defaults[closedKey]["1"] = nextFixtureClosedRange + XCTAssertEqual(fixtureClosedRange.upperBound, Defaults[closedKey]["0"]?.upperBound) + XCTAssertEqual(fixtureClosedRange.lowerBound, Defaults[closedKey]["0"]?.lowerBound) + XCTAssertEqual(nextFixtureClosedRange.upperBound, Defaults[closedKey]["1"]?.upperBound) + XCTAssertEqual(nextFixtureClosedRange.lowerBound, Defaults[closedKey]["1"]?.lowerBound) + + // Test serializable ClosedRange type + let closedDateKey = Defaults.Key<[String: ClosedRange]>("independentClosedRangeDateDictionaryKey", default: ["0": fixtureDateClosedRange]) + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]["0"]?.upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["0"]?.lowerBound) + Defaults[closedDateKey]["1"] = nextFixtureDateClosedRange + XCTAssertEqual(fixtureDateClosedRange.upperBound, Defaults[closedDateKey]["0"]?.upperBound) + XCTAssertEqual(fixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["0"]?.lowerBound) + XCTAssertEqual(nextFixtureDateClosedRange.upperBound, Defaults[closedDateKey]["1"]?.upperBound) + XCTAssertEqual(nextFixtureDateClosedRange.lowerBound, Defaults[closedDateKey]["1"]?.lowerBound) + } +} diff --git a/readme.md b/readme.md index 4c05f7a..103ffe1 100644 --- a/readme.md +++ b/readme.md @@ -96,8 +96,9 @@ Add `https://github.com/sindresorhus/Defaults` in the [“Swift Package Manager - `Color` (SwiftUI) - `Codable` - `NSSecureCoding` +- `Range`, `ClosedRange` -Defaults also support the above types wrapped in `Array`, `Set`, `Dictionary`, and even wrapped in nested types. For example, `[[String: Set<[String: Int]>]]`. +Defaults also support the above types wrapped in `Array`, `Set`, `Dictionary`, `Range`, `ClosedRange`, and even wrapped in nested types. For example, `[[String: Set<[String: Int]>]]`. For more types, see the [enum example](#enum-example), [`Codable` example](#codable-example), or [advanced Usage](#advanced-usage). For more examples, see [Tests/DefaultsTests](./Tests/DefaultsTests).