Skip to content

Commit

Permalink
Leif/property wrappers (#7)
Browse files Browse the repository at this point in the history
* Add RequiredKeysCache and tests

* Add global caches

* Add property wrappers and tests
  • Loading branch information
0xLeif authored Jun 29, 2023
1 parent 5a797f4 commit 619ca28
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Sources/Cache/Cache/RequiredKeysCache+subscript.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
extension RequiredKeysCache {
/**
Accesses the value associated with the given required key for reading and writing, optionally using a default value if the key is missing.

- Parameters:
- requiredKey: The required key to retrieve the value for.
- default: The default value to be returned if the key is missing.
- Returns: The value stored in the cache for the given key, or the default value if it doesn't exist.
*/
public subscript(requiredKey key: Key) -> Value {
get {
resolve(requiredKey: key, as: Value.self)
}
set(newValue) {
set(value: newValue, forKey: key)
}
}
}
213 changes: 213 additions & 0 deletions Sources/Cache/Cache/RequiredKeysCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/// The `RequiredKeysCache` class is a subclass of `Cache` that allows you to define a set of required keys. This cache ensures that the required keys are always present and throws an error if any of them are missing.
public class RequiredKeysCache<Key: Hashable, Value>: Cache<Key, Value> {

/// The set of keys that must always be present in the cache.
public var requiredKeys: Set<Key> {
didSet {
for key in requiredKeys {
_ = resolve(requiredKey: key)
}
}
}

/**
Initializes a new instance of `RequiredKeysCache` with the specified required keys and initial values.

- Parameters:
- requiredKeys: A set of keys that must always be present in the cache.
- initialValues: A dictionary of initial key-value pairs to populate the cache.
*/
public init(
requiredKeys: Set<Key>,
initialValues: [Key: Value]
) {
self.requiredKeys = requiredKeys

super.init(initialValues: initialValues)

do {
_ = try require(keys: requiredKeys)
}

catch { fatalError(error.localizedDescription) }
}

/**
Initializes a new instance of `RequiredKeysCache` with the specified initial values. The required keys are automatically inferred from the initial values.

- Parameters:
- initialValues: A dictionary of initial key-value pairs to populate the cache. The keys are considered as required keys.
*/
public required convenience init(initialValues: [Key: Value] = [:]) {
self.init(requiredKeys: Set(initialValues.keys), initialValues: initialValues)
}

/**
Removes a key-value pair from the cache. If the key is one of the required keys, it is not removed.

- Parameters:
- key: The key of the value to remove.
*/
public override func remove(_ key: Key) {
guard requiredKeys.contains(key) == false else { return }

super.remove(key)
}

/**
Resolves a required key from the cache, ensuring its presence and returning its value.

- Parameters:
- requiredKey: The required key to resolve.
- as: The type to expect as the value of the key. The default is `Output.self`.

- Returns: The resolved value for the required key.
- Throws: A runtime error if the required key is not present in the cache or if the expected value type is incorrect.
*/
public func resolve<Output>(requiredKey: Key, as: Output.Type = Output.self) -> Output {
guard
requiredKeys.contains(requiredKey)
else { fatalError("The key '\(requiredKey)' is not a Required Key.") }

guard
contains(requiredKey)
else { fatalError("Required Key Missing: '\(requiredKey)'") }

do {
return try resolve(requiredKey, as: Output.self)
}

catch { fatalError(error.localizedDescription) }
}

/**
Resolves a required key from the cache, ensuring its presence and returning its value.

- Parameter requiredKey: The required key to resolve.

- Returns: The resolved value for the required key.
*/
public func resolve(requiredKey: Key) -> Value {
resolve(requiredKey: requiredKey, as: Value.self)
}

/**
Updates the value of a required key in the cache using a closure.

- Parameters:
- requiredKey: The required key to update.
- as: The type to expect as the value of the key. The default is `CacheValue.self`.
- block: A closure that takes the current value of the required key and returns the new value.

- Returns: The updated value for the required key.
*/
@discardableResult
public func update<CacheValue>(
requiredKey key: Key,
as: CacheValue.Type = CacheValue.self,
block: (CacheValue) -> Value
) -> Value {
let newValue = block(resolve(requiredKey: key, as: CacheValue.self))

set(value: newValue, forKey: key)

return newValue
}

/**
Updates the value of a required key in the cache using a closure.

- Parameters:
- requiredKey: The required key to update.
- block: A closure that takes the current value of the required key and returns the new value.

- Returns: The updated value for the required key.
*/
@discardableResult
public func update(
requiredKey key: Key,
block: (Value) -> Value
) -> Value {
update(
requiredKey: key,
as: Value.self,
block: block
)
}

/**
Uses the value of a required key from the cache in a closure and returns a result.

- Parameters:
- requiredKey: The required key to use.
- as: The type to expect as the value of the key. The default is `CacheValue.self`.
- block: A closure that takes the value of the required key and returns a result.

- Returns: The result of the closure evaluation.
*/
@discardableResult
public func use<CacheValue, Output>(
requiredKey key: Key,
as: CacheValue.Type = CacheValue.self,
block: (CacheValue) -> Output?
) -> Output? {
block(resolve(requiredKey: key, as: CacheValue.self))
}

/**
Uses the value of a required key from the cache in a closure.

- Parameters:
- requiredKey: The required key to use.
- as: The type to expect as the value of the key. The default is `CacheValue.self`.
- block: A closure that takes the value of the required key.

*/
public func use<CacheValue>(
requiredKey key: Key,
as: CacheValue.Type = CacheValue.self,
block: (CacheValue) -> Void
) {
block(resolve(requiredKey: key, as: CacheValue.self))
}

/**
Uses the value of a required key from the cache in a closure and returns a result.

- Parameters:
- requiredKey: The required key to use.
- block: A closure that takes the value of the required key and returns a result.

- Returns: The result of the closure evaluation.
*/
@discardableResult
public func use<Output>(
requiredKey key: Key,
block: (Value) -> Output?
) -> Output? {
use(
requiredKey: key,
as: Value.self,
block: block
)
}

/**
Uses the value of a required key from the cache in a closure.

- Parameters:
- requiredKey: The required key to use.
- block: A closure that takes the value of the required key.

*/
public func use(
requiredKey key: Key,
block: (Value) -> Void
) {
use(
requiredKey: key,
as: Value.self,
block: block
)
}
}
4 changes: 4 additions & 0 deletions Sources/Cache/Global/Global+cache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extension Global {
/// The global cache for storing values.
public static var cache: Cache<AnyHashable, Any> = Cache()
}
4 changes: 4 additions & 0 deletions Sources/Cache/Global/Global+dependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extension Global {
/// The global cache for storing required dependencies.
public static var dependencies: RequiredKeysCache<AnyHashable, Any> = RequiredKeysCache()
}
16 changes: 16 additions & 0 deletions Sources/Cache/Global/Global+images.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#if canImport(SwiftUI)
import SwiftUI

extension Global {
#if os(macOS)
/// A typealias for `NSImage`.
public typealias CacheImage = NSImage
#else
/// A typealias for `UIImage`.
public typealias CacheImage = UIImage
#endif

/// The global cache for storing images.
public static var images: Cache<URL, CacheImage> = Cache()
}
#endif
2 changes: 2 additions & 0 deletions Sources/Cache/Global/Global.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// The `Global` enum is a container for various global properties or caches used within the application.
public enum Global { }
62 changes: 62 additions & 0 deletions Sources/Cache/PropertyWrappers/Cached.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
The `Cached` property wrapper provides a convenient way to access values from a cache. It allows you to specify a key, cache instance, and a default value. The property wrapper ensures that the value is always retrieved from the cache and provides type safety for accessing the value.

Usage:
```swift
@Cached(key: "myKey", cache: myCache, defaultValue: 0)
var myValue: Int

// Accessing the value
let currentValue = myValue

// Updating the value
myValue = 42
```

- Parameters:
- key: The key associated with the value in the cache.
- cache: The cache instance to retrieve the value from.
- defaultValue: The default value to be used if the value is not present in the cache.

The property wrapper provides a `wrappedValue` that can be accessed and mutated like a regular property. When accessed, the `wrappedValue` retrieves the value from the cache based on the specified key. If the value is not present in the cache, the `defaultValue` is used. When mutated, the `wrappedValue` sets the new value into the cache using the specified key.

- Note: The `Cached` property wrapper relies on a cache instance that conforms to the `Cache` protocol, in order to retrieve and store the values efficiently.
*/
@propertyWrapper public struct Cached<Key: Hashable, Value> {
/// The key associated with the value in the cache.
public let key: Key

/// The cache instance to retrieve the value from.
public let cache: Cache<Key, Any>

/// The default value to be used if the value is not present in the cache.
public let defaultValue: Value

/// The wrapped value that can be accessed and mutated by the property wrapper.
public var wrappedValue: Value {
get {
cache.get(key, as: Value.self) ?? defaultValue
}
set {
cache.set(value: newValue, forKey: key)
}
}

/**
Initializes a new instance of the `Cached` property wrapper.

- Parameters:
- key: The key associated with the value in the cache.
- cache: The cache instance to retrieve the value from. The default is `Global.cache`.
- defaultValue: The default value to be used if the value is not present in the cache.
*/
public init(
key: Key,
using cache: Cache<Key, Any> = Global.cache,
defaultValue: Value
) {
self.key = key
self.cache = cache
self.defaultValue = defaultValue
}
}
63 changes: 63 additions & 0 deletions Sources/Cache/PropertyWrappers/OptionallyCached.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
The `OptionallyCached` property wrapper provides a convenient way to optionally access values from a cache. It allows you to specify a key and a cache instance. The property wrapper ensures that the value is retrieved from the cache if present, and provides type safety for accessing the value.

Usage:
```swift
@OptionallyCached(key: "myKey", cache: myCache)
var myValue: Int?

// Accessing the value
let currentValue = myValue

// Setting the value
myValue = 42

// Removing the value from the cache
myValue = nil
```

- Parameters:
- key: The key associated with the value in the cache.
- cache: The cache instance to retrieve the value from.

The property wrapper provides a `wrappedValue` that can be accessed and mutated like a regular optional property. When accessed, the `wrappedValue` retrieves the value from the cache based on the specified key. If the value is not present in the cache, `nil` is returned. When mutated, the `wrappedValue` sets the new value into the cache using the specified key. If the new value is `nil`, the key-value pair is removed from the cache.

- Note: The `OptionallyCached` property wrapper relies on a cache instance that conforms to the `Cache` protocol, in order to retrieve and store the values efficiently.

*/
@propertyWrapper public struct OptionallyCached<Key: Hashable, Value> {
/// The key associated with the value in the cache.
public let key: Key

/// The cache instance to retrieve the value from.
public let cache: Cache<Key, Any>

/// The wrapped value that can be accessed and mutated by the property wrapper.
public var wrappedValue: Value? {
get {
cache.get(key, as: Value.self)
}
set {
guard let newValue else {
return cache.remove(key)
}

cache.set(value: newValue, forKey: key)
}
}

/**
Initializes a new instance of the `OptionallyCached` property wrapper.

- Parameters:
- key: The key associated with the value in the cache.
- cache: The cache instance to retrieve the value from. The default is `Global.cache`.
*/
public init(
key: Key,
using cache: Cache<Key, Any> = Global.cache
) {
self.key = key
self.cache = cache
}
}
Loading

0 comments on commit 619ca28

Please sign in to comment.