Validate the default key name and emit with Xcode runtime warning (#126)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
5cf1178d34
commit
5d1d7932a9
|
@ -49,7 +49,13 @@ extension Defaults {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let suite: UserDefaults
|
public let suite: UserDefaults
|
||||||
|
|
||||||
|
@_alwaysEmitIntoClient
|
||||||
fileprivate init(name: String, suite: UserDefaults) {
|
fileprivate init(name: String, suite: UserDefaults) {
|
||||||
|
runtimeWarn(
|
||||||
|
isValidKeyPath(name: name),
|
||||||
|
"The key name must be ASCII, not start with @, and cannot contain a dot (.)."
|
||||||
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.suite = suite
|
self.suite = suite
|
||||||
}
|
}
|
||||||
|
@ -88,7 +94,8 @@ extension Defaults {
|
||||||
- `UserDefaults.object(forKey: string)` returns `nil`
|
- `UserDefaults.object(forKey: string)` returns `nil`
|
||||||
- A `bridge` cannot deserialize `Value` from `UserDefaults`
|
- A `bridge` cannot deserialize `Value` from `UserDefaults`
|
||||||
*/
|
*/
|
||||||
private let defaultValueGetter: () -> Value
|
@usableFromInline
|
||||||
|
internal let defaultValueGetter: () -> Value
|
||||||
|
|
||||||
public var defaultValue: Value { defaultValueGetter() }
|
public var defaultValue: Value { defaultValueGetter() }
|
||||||
|
|
||||||
|
@ -99,6 +106,7 @@ extension Defaults {
|
||||||
|
|
||||||
The `default` parameter should not be used if the `Value` type is an optional.
|
The `default` parameter should not be used if the `Value` type is an optional.
|
||||||
*/
|
*/
|
||||||
|
@_alwaysEmitIntoClient
|
||||||
public init(
|
public init(
|
||||||
_ key: String,
|
_ key: String,
|
||||||
default defaultValue: Value,
|
default defaultValue: Value,
|
||||||
|
@ -135,6 +143,7 @@ extension Defaults {
|
||||||
|
|
||||||
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
|
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
|
||||||
*/
|
*/
|
||||||
|
@_alwaysEmitIntoClient
|
||||||
public init(
|
public init(
|
||||||
_ key: String,
|
_ key: String,
|
||||||
suite: UserDefaults = .standard,
|
suite: UserDefaults = .standard,
|
||||||
|
@ -144,19 +153,23 @@ extension Defaults {
|
||||||
|
|
||||||
super.init(name: key, suite: suite)
|
super.init(name: key, suite: suite)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Defaults.Key {
|
||||||
|
// We cannot declare this convenience initializer in class directly because of "@_transparent' attribute is not supported on declarations within classes".
|
||||||
/**
|
/**
|
||||||
Create a key with an optional value.
|
Create a key with an optional value.
|
||||||
|
|
||||||
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||||
*/
|
*/
|
||||||
|
@_transparent
|
||||||
public convenience init<T>(
|
public convenience init<T>(
|
||||||
_ key: String,
|
_ key: String,
|
||||||
suite: UserDefaults = .standard
|
suite: UserDefaults = .standard
|
||||||
) where Value == T? {
|
) where Value == T? {
|
||||||
self.init(key, default: nil, suite: suite)
|
self.init(key, default: nil, suite: suite)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Defaults {
|
extension Defaults {
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
#if DEBUG
|
||||||
|
#if canImport(OSLog)
|
||||||
|
import OSLog
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
extension Decodable {
|
extension Decodable {
|
||||||
init?(jsonData: Data) {
|
init?(jsonData: Data) {
|
||||||
|
@ -160,6 +164,13 @@ extension Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Defaults {
|
||||||
|
@usableFromInline
|
||||||
|
internal static func isValidKeyPath(name: String) -> Bool {
|
||||||
|
// The key must be ASCII, not start with @, and cannot contain a dot.
|
||||||
|
return !name.starts(with: "@") && name.allSatisfy { $0 != "." && $0.isASCII }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Defaults.Serializable {
|
extension Defaults.Serializable {
|
||||||
/**
|
/**
|
||||||
|
@ -200,7 +211,8 @@ extension Defaults.Serializable {
|
||||||
set(Value.toSerialize(value), forKey: key)
|
set(Value.toSerialize(value), forKey: key)
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
|
@usableFromInline
|
||||||
|
internal static func toSerializable<T: Defaults.Serializable>(_ value: T) -> Any? {
|
||||||
if T.isNativelySupportedType {
|
if T.isNativelySupportedType {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
@ -217,3 +229,54 @@ extension Defaults.Serializable {
|
||||||
return toSerializable(next)
|
return toSerializable(next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
/**
|
||||||
|
Get SwiftUI dynamic shared object.
|
||||||
|
|
||||||
|
Reference: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dyld.3.html
|
||||||
|
*/
|
||||||
|
@usableFromInline
|
||||||
|
internal let dynamicSharedObject: UnsafeMutableRawPointer = {
|
||||||
|
let imageCount = _dyld_image_count()
|
||||||
|
for imageIndex in 0..<imageCount {
|
||||||
|
guard
|
||||||
|
let name = _dyld_get_image_name(imageIndex),
|
||||||
|
// Use `/SwiftUI` instead of `SwiftUI` to prevent any library named `XXSwiftUI`.
|
||||||
|
String(cString: name).hasSuffix("/SwiftUI"),
|
||||||
|
let header = _dyld_get_image_header(imageIndex)
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnsafeMutableRawPointer(mutating: header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnsafeMutableRawPointer(mutating: #dsohandle)
|
||||||
|
}()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
@usableFromInline
|
||||||
|
internal func runtimeWarn(
|
||||||
|
_ condition: @autoclosure() -> Bool, _ message: @autoclosure () -> String
|
||||||
|
) {
|
||||||
|
#if DEBUG
|
||||||
|
#if canImport(OSLog)
|
||||||
|
let message = message()
|
||||||
|
let condition = condition()
|
||||||
|
if !condition {
|
||||||
|
os_log(
|
||||||
|
.fault,
|
||||||
|
// A token that identifies the containing executable or dylib image.
|
||||||
|
dso: dynamicSharedObject,
|
||||||
|
log: OSLog(subsystem: "com.apple.runtime-issues", category: "Defaults"),
|
||||||
|
"%@",
|
||||||
|
message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
assert(condition, message)
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import XCTest
|
import XCTest
|
||||||
import Defaults
|
@testable import Defaults
|
||||||
|
|
||||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
let fixtureFileURL = URL(string: "file://~/icon.png")!
|
let fixtureFileURL = URL(string: "file://~/icon.png")!
|
||||||
|
@ -37,6 +37,15 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTAssertTrue(Defaults[key])
|
XCTAssertTrue(Defaults[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testValidKeyName() {
|
||||||
|
let validKey = Defaults.Key<Bool>("test", default: false)
|
||||||
|
let containsDotKey = Defaults.Key<Bool>("test.a", default: false)
|
||||||
|
let startsWithAtKey = Defaults.Key<Bool>("@test", default: false)
|
||||||
|
XCTAssertTrue(Defaults.isValidKeyPath(name: validKey.name))
|
||||||
|
XCTAssertFalse(Defaults.isValidKeyPath(name: containsDotKey.name))
|
||||||
|
XCTAssertFalse(Defaults.isValidKeyPath(name: startsWithAtKey.name))
|
||||||
|
}
|
||||||
|
|
||||||
func testOptionalKey() {
|
func testOptionalKey() {
|
||||||
let key = Defaults.Key<Bool?>("independentOptionalKey")
|
let key = Defaults.Key<Bool?>("independentOptionalKey")
|
||||||
let url = Defaults.Key<URL?>("independentOptionalURLKey")
|
let url = Defaults.Key<URL?>("independentOptionalURLKey")
|
||||||
|
|
Loading…
Reference in New Issue