Skip to content

Instantly share code, notes, and snippets.

@fenixkim
Last active September 4, 2024 18:44
Show Gist options
  • Save fenixkim/fa79329b4d10fc5c1e9a4adb9ee60a9a to your computer and use it in GitHub Desktop.
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.
//
// 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))
}
}
@fenixkim
Copy link
Author

fenixkim commented Sep 4, 2024

enum UserProperty: String {
    case name, age, email, isActive
}

let user = EnumKeyedContainer<UserProperty>()

// Uso básico
user.name = "Alice"
user.age = 30

// Get con fallback
let status = user.get(.isActive, defaultValue: false)

// Get con inferencia de tipo
if let age: Int = user.get(.age) {
    print("User age: \(age)")
}

// Transformación
user.transform(.age) { (age: Int) -> String in
    return "\(age) years old"
}

// Acción si existe
user.ifExists(.email) { (email: String) in
    print("User email: \(email)")
}

// Serialización
let dict = user.toDictionary()

// Deserialización
let newUser = EnumKeyedContainer<UserProperty>(dictionary: dict)

// Configuración opcional
user[.email] = nil // Elimina la clave si existe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment