|
import Foundation |
|
|
|
// MARK: - Type Definitions |
|
|
|
/// Represents an object able to be configured with a `Configuration<T>` |
|
public protocol Configurable { } |
|
|
|
/// Shortcut protocol to enable applying and creating configurations using `.with` |
|
/// methods on instance and class types of implementers. |
|
public protocol With: Configurable { } |
|
|
|
public struct Configuration<T>: CustomDebugStringConvertible { |
|
private var configurations: [AnyEntry] = [] |
|
|
|
public var debugDescription: String { |
|
var descriptions: [String] = [ |
|
"\(type(of: self)) - keypath count: \(configurations.count)", |
|
"----" |
|
] |
|
|
|
for config in configurations { |
|
descriptions.append(config.debugDescription) |
|
} |
|
|
|
return descriptions.joined(separator: "\n") |
|
} |
|
|
|
public init() { |
|
|
|
} |
|
|
|
public func applied(on target: T) -> T { |
|
var target = target |
|
for config in configurations { |
|
target = config.closure(target) |
|
} |
|
return target |
|
} |
|
|
|
public func apply(on target: inout T) { |
|
target = applied(on: target) |
|
} |
|
|
|
/// Fetches a nested Configuration for a given keypath |
|
public func configuration<U>(forKeypath keyPath: KeyPath<T, U>) -> Configuration<U>? { |
|
return |
|
configurations.lazy.compactMap { entry in |
|
entry.typedEntry as? TypedEntry<U> |
|
}.compactMap { config -> Configuration<U>? in |
|
switch config.configuration { |
|
case let .keyPathConfiguration(kp, value) where kp == keyPath: |
|
return value |
|
default: |
|
return nil |
|
} |
|
}.first |
|
} |
|
|
|
/// Fetches a nested Configuration for a given keypath |
|
public func configuration<U>(forKeypath keyPath: KeyPath<T, U?>) -> Configuration<U>? { |
|
return |
|
configurations.lazy.compactMap { entry in |
|
entry.typedEntry as? TypedEntry<U> |
|
}.compactMap { config -> Configuration<U>? in |
|
switch config.configuration { |
|
case let .keyPathConfigurationOnOptional(kp, value) where kp == keyPath: |
|
return value |
|
default: |
|
return nil |
|
} |
|
}.first |
|
} |
|
|
|
/// Fetches the value for the given keypath within this Configuration object |
|
public func value<U>(forKeypath keyPath: KeyPath<T, U>) -> U? { |
|
return |
|
configurations.lazy.compactMap { entry in |
|
entry.typedEntry as? TypedEntry<U> |
|
}.compactMap { entry -> U? in |
|
switch entry.configuration { |
|
case let .keyPath(kp, value) where kp == keyPath: |
|
return value |
|
default: |
|
return nil |
|
} |
|
}.first |
|
} |
|
|
|
public func joined(with other: Configuration) -> Configuration { |
|
var new = self |
|
new.configurations.append(contentsOf: other.configurations) |
|
return new |
|
} |
|
|
|
mutating private func appendConfiguration<U>(_ config: TypedEntry<U>.Kind, |
|
file: String, |
|
line: Int) { |
|
|
|
let entry = TypedEntry(configuration: config, file: file, line: line) |
|
let anyEntry = AnyEntry(entry: entry) |
|
|
|
configurations.append(anyEntry) |
|
} |
|
|
|
fileprivate struct TypedEntry<U>: CustomDebugStringConvertible { |
|
var configuration: Kind |
|
var file: String |
|
var line: Int |
|
|
|
func applied(to target: T) -> T { |
|
var target = target |
|
switch configuration { |
|
case let .keyPath(kp, value): |
|
target[keyPath: kp] = value |
|
|
|
case let .keyPathConfiguration(kp, config): |
|
config.apply(on: &target[keyPath: kp]) |
|
|
|
case let .keyPathConfigurationOnOptional(kp, config): |
|
if let value = target[keyPath: kp] { |
|
target[keyPath: kp] = config.applied(on: value) |
|
} |
|
|
|
case .closure(let closure): |
|
target = closure(target) |
|
} |
|
|
|
return target |
|
} |
|
|
|
var debugDescription: String { |
|
func descForKeyPath(_ kp: PartialKeyPath<T>, _ config: Configuration<U>) -> String { |
|
let configDescription = config.debugDescription |
|
|
|
let finalLine = |
|
configDescription |
|
// Indent nested configurations |
|
.components(separatedBy: "\n") |
|
.map { line in |
|
" " + line |
|
}.joined(separator: "\n") |
|
|
|
return "\(type(of: kp).valueType) =\n\(finalLine)" |
|
} |
|
|
|
var description = "" |
|
|
|
description += "\((file as NSString).lastPathComponent) line #\(line): " |
|
|
|
switch configuration { |
|
case let .keyPath(kp, value): |
|
description += "\(type(of: kp).valueType) = \(value)" |
|
|
|
case let .keyPathConfiguration(kp, config): |
|
description += descForKeyPath(kp, config) |
|
|
|
case let .keyPathConfigurationOnOptional(kp, config): |
|
description += descForKeyPath(kp, config) |
|
|
|
case .closure: |
|
description += "{ closure }" |
|
} |
|
|
|
return description |
|
} |
|
|
|
enum Kind { |
|
case keyPath(WritableKeyPath<T, U>, value: U) |
|
case keyPathConfiguration(WritableKeyPath<T, U>, config: Configuration<U>) |
|
case keyPathConfigurationOnOptional(WritableKeyPath<T, U?>, config: Configuration<U>) |
|
case closure((T) -> T) |
|
} |
|
} |
|
|
|
fileprivate struct AnyEntry: CustomDebugStringConvertible { |
|
var typedEntry: Any |
|
|
|
var isKeypath: Bool |
|
var value: Any? |
|
var debugDescription: String |
|
var file: String |
|
var line: Int |
|
var closure: (T) -> T |
|
|
|
init<U>(entry: TypedEntry<U>) { |
|
self.typedEntry = entry |
|
|
|
switch entry.configuration { |
|
case let .keyPath(_, value): |
|
isKeypath = true |
|
self.value = value |
|
case .keyPathConfiguration: |
|
isKeypath = true |
|
value = nil |
|
default: |
|
isKeypath = false |
|
value = nil |
|
} |
|
|
|
debugDescription = entry.debugDescription |
|
file = entry.file |
|
line = entry.line |
|
closure = entry.applied |
|
} |
|
} |
|
} |
|
|
|
extension Configuration { |
|
public func with<U>(_ keyPath: WritableKeyPath<T, U>, |
|
setTo value: U, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration { |
|
|
|
var new = self |
|
new.appendConfiguration(.keyPath(keyPath, value: value), file: file, line: line) |
|
|
|
return new |
|
} |
|
|
|
public func with<U>(_ keyPath: WritableKeyPath<T, U?>, |
|
setTo value: U, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration { |
|
|
|
var new = self |
|
new.appendConfiguration(.keyPath(keyPath, value: value), file: file, line: line) |
|
|
|
return new |
|
} |
|
|
|
public func with<U>(_ keyPath: WritableKeyPath<T, U>, |
|
configuredWith config: Configuration<U>, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration { |
|
|
|
var new = self |
|
new.appendConfiguration(.keyPathConfiguration(keyPath, config: config), file: file, line: line) |
|
|
|
return new |
|
} |
|
|
|
public func with<U>(_ keyPath: WritableKeyPath<T, U?>, |
|
configuredWith config: Configuration<U>, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration { |
|
|
|
var new = self |
|
new.appendConfiguration(.keyPathConfigurationOnOptional(keyPath, config: config), file: file, line: line) |
|
|
|
return new |
|
} |
|
|
|
public func with(closure: @escaping (T) -> T, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration { |
|
|
|
var new = self |
|
new.appendConfiguration(TypedEntry<T>.Kind.closure(closure), file: file, line: line) |
|
|
|
return new |
|
} |
|
} |
|
|
|
// MARK: - Static Configuration Constructors |
|
|
|
extension With { |
|
public static func with<T>(_ property: WritableKeyPath<Self, T>, |
|
setTo value: T, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration<Self> { |
|
|
|
return Configuration().with(property, setTo: value, file: file, line: line) |
|
} |
|
|
|
public static func with<T>(_ property: WritableKeyPath<Self, T?>, |
|
setTo value: T, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration<Self> { |
|
|
|
return Configuration().with(property, setTo: value, file: file, line: line) |
|
} |
|
|
|
public static func with<T>(_ property: WritableKeyPath<Self, T>, |
|
configuredWith config: Configuration<T>, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration<Self> { |
|
|
|
return Configuration().with(property, configuredWith: config, file: file, line: line) |
|
} |
|
|
|
public static func with<T>(_ property: WritableKeyPath<Self, T?>, |
|
configuredWith config: Configuration<T>, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration<Self> { |
|
|
|
return Configuration().with(property, configuredWith: config, file: file, line: line) |
|
} |
|
|
|
public static func with(closure: @escaping (Self) -> Self, |
|
file: String = #file, |
|
line: Int = #line) -> Configuration<Self> { |
|
|
|
return Configuration().with(closure: closure, file: file, line: line) |
|
} |
|
} |
|
|
|
// MARK: - Reference (class) Type Support |
|
|
|
// MARK: Default 'withable's |
|
extension NSObject: With {} |
|
|
|
public extension Configurable where Self: AnyObject { |
|
public func configure(with config: Configuration<Self>) { |
|
config.apply(on: self) |
|
} |
|
} |
|
|
|
public extension With where Self: AnyObject { |
|
@discardableResult |
|
public func with<T>(_ property: WritableKeyPath<Self, T>, |
|
setTo value: T) -> Self { |
|
|
|
return Configuration().with(property, setTo: value).applied(on: self) |
|
} |
|
|
|
@discardableResult |
|
public func with<T>(_ property: WritableKeyPath<Self, T?>, |
|
setTo value: T) -> Self { |
|
|
|
return Configuration().with(property, setTo: value).applied(on: self) |
|
} |
|
|
|
@discardableResult |
|
public func with<T>(_ property: WritableKeyPath<Self, T>, |
|
configuredWith config: Configuration<T>) -> Self { |
|
|
|
Configuration() |
|
.with(property, configuredWith: config) |
|
.apply(on: self) |
|
|
|
return self |
|
} |
|
|
|
@discardableResult |
|
public func with<T>(_ property: WritableKeyPath<Self, T?>, |
|
configuredWith config: Configuration<T>) -> Self { |
|
|
|
Configuration() |
|
.with(property, configuredWith: config) |
|
.apply(on: self) |
|
|
|
return self |
|
} |
|
|
|
@discardableResult |
|
public func with(config: Configuration<Self>) -> Self { |
|
return config.applied(on: self) |
|
} |
|
|
|
@discardableResult |
|
public func with(closure: @escaping (Self) -> Void) -> Self { |
|
Configuration<Self>().with(closure: closure).apply(on: self) |
|
return self |
|
} |
|
|
|
@discardableResult |
|
public static func with(closure: @escaping (Self) -> Void) -> Configuration<Self> { |
|
return Configuration().with(closure: closure) |
|
} |
|
} |
|
|
|
public extension Configuration where T: AnyObject { |
|
public func apply(on target: T) { |
|
_=applied(on: target) |
|
} |
|
|
|
public func with(closure: @escaping (T) -> Void) -> Configuration { |
|
return with(closure: { target -> T in closure(target); return target; }) |
|
} |
|
} |
|
|
|
// MARK: - Value Type Support |
|
|
|
public extension Configurable { |
|
mutating public func configure(with config: Configuration<Self>) { |
|
config.apply(on: &self) |
|
} |
|
} |
|
|
|
public extension With { |
|
public func with<T>(_ property: WritableKeyPath<Self, T>, setTo value: T) -> Self { |
|
return Configuration().with(property, setTo: value).applied(on: self) |
|
} |
|
|
|
public func with<T>(_ property: WritableKeyPath<Self, T?>, setTo value: T) -> Self { |
|
return Configuration().with(property, setTo: value).applied(on: self) |
|
} |
|
|
|
public func with<T>(_ property: WritableKeyPath<Self, T>, configuredWith config: Configuration<T>) -> Self { |
|
return Configuration().with(property, configuredWith: config).applied(on: self) |
|
} |
|
|
|
public func with<T>(_ property: WritableKeyPath<Self, T?>, configuredWith config: Configuration<T>) -> Self { |
|
return Configuration().with(property, configuredWith: config).applied(on: self) |
|
} |
|
|
|
public func with(closure: @escaping (Self) -> Self) -> Self { |
|
return Configuration().with(closure: closure).applied(on: self) |
|
} |
|
|
|
public func with(config: Configuration<Self>) -> Self { |
|
return config.applied(on: self) |
|
} |
|
} |
|
|
|
// MARK: - Configuration Extraction |
|
|
|
public extension With { |
|
public func extractConfig(with block: (ConfigurationExtractor<Self>) -> (ConfigurationExtractor<Self>)) -> Configuration<Self> { |
|
var extractor = ConfigurationExtractor<Self>() |
|
extractor = block(extractor) |
|
|
|
return extractor.configuration(with: self) |
|
} |
|
} |
|
|
|
public struct ConfigurationExtractor<T> { |
|
private var keyPathExtractors: [(Configuration<T>, T) -> Configuration<T>] = [] |
|
|
|
init() { |
|
|
|
} |
|
|
|
public func value<U>(for keyPath: WritableKeyPath<T, U>, |
|
file: String = #file, |
|
line: Int = #line) -> ConfigurationExtractor { |
|
|
|
var copy = self |
|
|
|
copy.keyPathExtractors.append({ config, target in |
|
config.with(keyPath, setTo: target[keyPath: keyPath], file: file, line: line) |
|
}) |
|
|
|
return copy |
|
} |
|
|
|
public func configuration(with target: T) -> Configuration<T> { |
|
var config = Configuration<T>() |
|
for extractor in keyPathExtractors { |
|
config = extractor(config, target) |
|
} |
|
return config |
|
} |
|
} |