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 suite: UserDefaults
|
||||
|
||||
@_alwaysEmitIntoClient
|
||||
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.suite = suite
|
||||
}
|
||||
|
@ -88,7 +94,8 @@ extension Defaults {
|
|||
- `UserDefaults.object(forKey: string)` returns `nil`
|
||||
- A `bridge` cannot deserialize `Value` from `UserDefaults`
|
||||
*/
|
||||
private let defaultValueGetter: () -> Value
|
||||
@usableFromInline
|
||||
internal let defaultValueGetter: () -> Value
|
||||
|
||||
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.
|
||||
*/
|
||||
@_alwaysEmitIntoClient
|
||||
public init(
|
||||
_ key: String,
|
||||
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.
|
||||
*/
|
||||
@_alwaysEmitIntoClient
|
||||
public init(
|
||||
_ key: String,
|
||||
suite: UserDefaults = .standard,
|
||||
|
@ -144,18 +153,22 @@ extension Defaults {
|
|||
|
||||
super.init(name: key, suite: suite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create a key with an optional value.
|
||||
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.
|
||||
|
||||
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||
*/
|
||||
public convenience init<T>(
|
||||
_ key: String,
|
||||
suite: UserDefaults = .standard
|
||||
) where Value == T? {
|
||||
self.init(key, default: nil, suite: suite)
|
||||
}
|
||||
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||
*/
|
||||
@_transparent
|
||||
public convenience init<T>(
|
||||
_ key: String,
|
||||
suite: UserDefaults = .standard
|
||||
) where Value == T? {
|
||||
self.init(key, default: nil, suite: suite)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
#if DEBUG
|
||||
#if canImport(OSLog)
|
||||
import OSLog
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extension Decodable {
|
||||
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 {
|
||||
/**
|
||||
|
@ -200,7 +211,8 @@ extension Defaults.Serializable {
|
|||
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 {
|
||||
return value
|
||||
}
|
||||
|
@ -217,3 +229,54 @@ extension Defaults.Serializable {
|
|||
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 Combine
|
||||
import XCTest
|
||||
import Defaults
|
||||
@testable import Defaults
|
||||
|
||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||
let fixtureFileURL = URL(string: "file://~/icon.png")!
|
||||
|
@ -37,6 +37,15 @@ final class DefaultsTests: XCTestCase {
|
|||
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() {
|
||||
let key = Defaults.Key<Bool?>("independentOptionalKey")
|
||||
let url = Defaults.Key<URL?>("independentOptionalURLKey")
|
||||
|
|
Loading…
Reference in New Issue