Last active
September 4, 2024 18:44
-
-
Save fenixkim/fa79329b4d10fc5c1e9a4adb9ee60a9a to your computer and use it in GitHub Desktop.
A flexible container that stores values of any type, indexed by an enum-based key.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// EnumKeyedContainer.swift | |
// Salpicon | |
// | |
// Created by Alexander Ruiz Ponce on 4/09/24. | |
// | |
import Foundation | |
/// A flexible container that stores values of any type, indexed by an enum-based key. | |
/// | |
/// `EnumKeyedContainer` provides a type-safe way to store and retrieve values of various types | |
/// using enum cases as keys. It combines the flexibility of dynamic member lookup with the | |
/// safety of enum-based keys. | |
/// | |
/// Example: | |
/// | |
/// enum UserProperty: String { | |
/// case name, age, email | |
/// } | |
/// | |
/// let user = EnumKeyedContainer<UserProperty>() | |
/// user.name = "Alice" | |
/// user.age = 30 | |
/// | |
/// if let name: String = user.name { | |
/// print("User name: \(name)") | |
/// } | |
@dynamicMemberLookup | |
class EnumKeyedContainer<Key: RawRepresentable & Hashable> where Key.RawValue == String { | |
private var storage: [Key: Any] = [:] | |
// MARK: - Subscripts | |
/// Accesses the value associated with the given dynamic member. | |
/// | |
/// This subscript allows you to access values using dot notation, providing a more natural syntax. | |
/// | |
/// - Parameter member: The string representation of the enum key. | |
/// - Returns: The value associated with the key, if it exists and can be cast to the expected type. | |
/// | |
/// Example: | |
/// | |
/// user.name = "Alice" | |
/// print(user.name as String?) // Prints: Optional("Alice") | |
subscript<T>(dynamicMember member: String) -> T? { | |
get { | |
guard let key = Key(rawValue: member) else { return nil } | |
return storage[key] as? T | |
} | |
set { | |
guard let key = Key(rawValue: member) else { return } | |
storage[key] = newValue | |
} | |
} | |
/// Accesses the value associated with the given enum key. | |
/// | |
/// This subscript allows you to get, set, or remove values using enum keys. | |
/// Setting a value to `nil` removes the key-value pair. | |
/// | |
/// - Parameter key: The enum key. | |
/// - Returns: The value associated with the key, if it exists and can be cast to the expected type. | |
/// | |
/// Example: | |
/// | |
/// user[.age] = 30 | |
/// print(user[.age] as Int?) // Prints: Optional(30) | |
/// user[.age] = nil // Removes the age key-value pair | |
subscript<T>(key: Key) -> T? { | |
get { storage[key] as? T } | |
set { | |
if let newValue = newValue { | |
storage[key] = newValue | |
} else { | |
storage.removeValue(forKey: key) | |
} | |
} | |
} | |
// MARK: - Basic Operations | |
/// Sets a value for the given key. | |
/// | |
/// - Parameters: | |
/// - value: The value to store. | |
/// - key: The enum key to associate with the value. | |
/// | |
/// Example: | |
/// | |
/// user.set("[email protected]", forKey: .email) | |
func set<T>(_ value: T, forKey key: Key) { | |
storage[key] = value | |
} | |
/// Retrieves the value associated with the given key. | |
/// | |
/// - Parameter key: The enum key of the value to retrieve. | |
/// - Returns: The value associated with the key, if it exists and can be cast to the expected type. | |
/// | |
/// Example: | |
/// | |
/// if let email: String = user.get(.email) { | |
/// print("User email: \(email)") | |
/// } | |
func get<T>(_ key: Key) -> T? { | |
return storage[key] as? T | |
} | |
/// Retrieves the value associated with the given key, or returns a default value if the key doesn't exist. | |
/// | |
/// - Parameters: | |
/// - key: The enum key of the value to retrieve. | |
/// - defaultValue: The value to return if the key doesn't exist or the value can't be cast to the expected type. | |
/// - Returns: The value associated with the key, or the default value. | |
/// | |
/// Example: | |
/// | |
/// let isActive = user.get(.isActive, defaultValue: false) | |
/// print("User is active: \(isActive)") | |
func get<T>(_ key: Key, defaultValue: T) -> T { | |
return (storage[key] as? T) ?? defaultValue | |
} | |
/// Removes the value associated with the given key. | |
/// | |
/// - Parameter key: The enum key of the value to remove. | |
/// | |
/// Example: | |
/// | |
/// user.remove(.email) | |
func remove(_ key: Key) { | |
storage.removeValue(forKey: key) | |
} | |
// MARK: - Utility Methods | |
/// The keys of all stored values. | |
var keys: [Key] { | |
return Array(storage.keys) | |
} | |
/// A Boolean value indicating whether the container has no key-value pairs. | |
var isEmpty: Bool { | |
return storage.isEmpty | |
} | |
/// The number of key-value pairs in the container. | |
var count: Int { | |
return storage.count | |
} | |
/// Returns a Boolean value indicating whether the given key exists in the container. | |
/// | |
/// - Parameter key: The key to look for. | |
/// - Returns: `true` if the key exists, `false` otherwise. | |
/// | |
/// Example: | |
/// | |
/// if user.contains(.email) { | |
/// print("User has an email") | |
/// } | |
func contains(_ key: Key) -> Bool { | |
return storage.keys.contains(key) | |
} | |
/// Removes all key-value pairs from the container. | |
/// | |
/// Example: | |
/// | |
/// user.removeAll() | |
/// print(user.isEmpty) // Prints: true | |
func removeAll() { | |
storage.removeAll() | |
} | |
/// Merges the key-value pairs from another container into this one. | |
/// | |
/// If a key already exists in this container, its value is replaced by the value from the other container. | |
/// | |
/// - Parameter other: Another `EnumKeyedContainer` to merge from. | |
/// | |
/// Example: | |
/// | |
/// let additionalInfo = EnumKeyedContainer<UserProperty>() | |
/// additionalInfo.email = "[email protected]" | |
/// user.merge(additionalInfo) | |
func merge(_ other: EnumKeyedContainer<Key>) { | |
for key in other.keys { | |
storage[key] = other.storage[key] | |
} | |
} | |
/// Transforms the value associated with a given key using the provided closure. | |
/// | |
/// - Parameters: | |
/// - key: The enum key of the value to transform. | |
/// - transform: A closure that takes a value of type `T` and returns a value of type `U`. | |
/// | |
/// Example: | |
/// | |
/// user.transform(.age) { (age: Int) -> String in | |
/// return "\(age) years old" | |
/// } | |
func transform<T, U>(_ key: Key, transform: (T) -> U) { | |
if let value: T = self[key] { | |
set(transform(value), forKey: key) | |
} | |
} | |
/// Performs an action with the value for a given key if it exists and is of the expected type. | |
/// | |
/// - Parameters: | |
/// - key: The enum key of the value to check. | |
/// - action: A closure that takes a value of type `T` and performs some action. | |
/// | |
/// Example: | |
/// | |
/// user.ifExists(.email) { (email: String) in | |
/// print("User email: \(email)") | |
/// } | |
func ifExists<T>(_ key: Key, action: (T) -> Void) { | |
if let value: T = self[key] { | |
action(value) | |
} | |
} | |
/// Converts the container's contents to a dictionary with string keys. | |
/// | |
/// - Returns: A dictionary representation of the container's contents. | |
/// | |
/// Example: | |
/// | |
/// let dict = user.toDictionary() | |
/// print(dict["name"] as? String) // Prints the user's name | |
func toDictionary() -> [String: Any] { | |
return Dictionary(uniqueKeysWithValues: storage.map { ($0.key.rawValue, $0.value) }) | |
} | |
/// Creates a new container from a dictionary. | |
/// | |
/// - Parameter dictionary: A dictionary with string keys to initialize the container. | |
/// | |
/// Example: | |
/// | |
/// let dict = ["name": "Bob", "age": 25] | |
/// let newUser = EnumKeyedContainer<UserProperty>(dictionary: dict) | |
convenience init(dictionary: [String: Any]) { | |
self.init() | |
for (key, value) in dictionary { | |
if let enumKey = Key(rawValue: key) { | |
storage[enumKey] = value | |
} | |
} | |
} | |
} | |
extension EnumKeyedContainer { | |
/// Reduces the container to a result based on the given keys and combining function. | |
/// | |
/// - Parameters: | |
/// - keys: The keys to include in the reduction. | |
/// - initialResult: The initial value to use for the result. | |
/// - nextPartialResult: A closure that combines the current result with a new key-value pair. | |
/// - Returns: The final result after reducing all specified keys. | |
/// | |
/// Example: | |
/// | |
/// let user = EnumKeyedContainer<UserProperty>() | |
/// user[.name] = "Alice" | |
/// user[.age] = 30 | |
/// user[.email] = nil | |
/// | |
/// let userDict = user.reduce([.name, .age, .email], initialResult: [:] as [UserProperty: String]) { (dict, key, value: Any?) in | |
/// var newDict = dict | |
/// if let stringValue = value as? String ?? value.map({ "\($0)" }) { | |
/// newDict[key] = stringValue | |
/// } | |
/// return newDict | |
/// } | |
/// print(userDict) | |
/// // Prints: [.name: "Alice", .age: "30"] | |
func reduce<Result>(_ keys: [Key], | |
initialResult: Result, | |
_ nextPartialResult: (Result, Key, Any?) -> Result) -> Result { | |
return keys.reduce(initialResult) { partialResult, key in | |
let value: Any? = self[key] | |
return nextPartialResult(partialResult, key, value) | |
} | |
} | |
/// Reduces the container to a dictionary containing only the non-null values of a specific type. | |
/// | |
/// - Parameter keys: The keys to include in the resulting dictionary. | |
/// - Returns: A dictionary where the keys are of type `Key` and the values are of type `T`, | |
/// containing only the entries where the value is not null and can be cast to type `T`. | |
/// | |
/// Example: | |
/// | |
/// let user = EnumKeyedContainer<UserProperty>() | |
/// user[.name] = "Alice" | |
/// user[.age] = 30 | |
/// user[.email] = nil | |
/// | |
/// let stringDict: [UserProperty: String] = user.reduce([.name, .age, .email]) | |
/// print(stringDict) | |
/// // Prints: [.name: "Alice"] | |
func reduce<T>(_ keys: [Key]) -> [Key: T] { | |
return reduce(keys, initialResult: [Key: T]()) { dict, key, value in | |
var newDict = dict | |
if let typedValue = value as? T { | |
newDict[key] = typedValue | |
} | |
return newDict | |
} | |
} | |
} | |
extension EnumKeyedContainer where Key: CaseIterable { | |
/// Reduces the container to a dictionary containing only the non-null values of a specific type for all cases of the Key. | |
/// | |
/// This method is available when the `Key` type conforms to `CaseIterable`. | |
/// | |
/// - Returns: A dictionary where the keys are of type `Key` and the values are of type `T`, | |
/// containing only the entries where the value is not null and can be cast to type `T`. | |
/// | |
/// Example: | |
/// | |
/// let user = EnumKeyedContainer<UserProperty>() | |
/// user[.name] = "Alice" | |
/// user[.age] = 30 | |
/// user[.email] = nil | |
/// | |
/// let stringDict: [UserProperty: String] = user.reduce() | |
/// print(stringDict) | |
/// // Prints: [.name: "Alice"] | |
func reduce<T>() -> [Key: T] { | |
return reduce(Array(Key.allCases)) | |
} | |
} |
Author
fenixkim
commented
Sep 4, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment