Last active
August 3, 2023 15:47
-
-
Save KaQuMiQ/2dfe2b2390f145632feb8a525f5b91e0 to your computer and use it in GitHub Desktop.
MQLite
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
import OSLog | |
import class Foundation.Bundle | |
import class Foundation.ProcessInfo | |
import struct Foundation.TimeZone | |
/// Diagnostics allows debugging and gathering usage data. | |
public enum Diagnostics { | |
/// Info including application name and version. | |
public static let info: String = { | |
let infoDictionary: Dictionary<String, Any> = Bundle.main.infoDictionary ?? .init() | |
return "\(infoDictionary["CFBundleName"] as? String ?? "Application") \(infoDictionary["CFBundleShortVersionString"] as? String ?? "?.?.?")" | |
}() | |
private static let deviceInfo: String = { | |
ProcessInfo.processInfo.operatingSystemVersionString | |
}() | |
private static let appBundle: String = { | |
Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String | |
?? "com.miquido.diagnostics" | |
}() | |
/// Optionally disable diagnostics (i.e. in unit tests). | |
/// Only the first value set will be used. Assignment after | |
/// the first or after using diagnostics will be ignored. | |
public static var enabled: Bool = true { | |
didSet { | |
// If this log is visible then indeed diagnostics is enabled. | |
Diagnostics.logger.log(level: .info, "Diagnostics is enabled!") | |
} | |
} | |
/// Access current diagnostics logger. | |
public static let logger: Logger = { | |
let log: OSLog | |
if Diagnostics.enabled { | |
log = .init( | |
subsystem: Diagnostics.appBundle, | |
category: "diagnostics" | |
) | |
} | |
else { | |
log = .disabled | |
} | |
return Logger(log) | |
}() | |
/// Load diagnostics details incluidng os info, application info and logs. | |
@Sendable public static func details() async throws -> Array<String> { | |
try await Task<Array<String>, Error>.detached { () async throws -> Array<String> in | |
let environmentInfo: String = "\(Diagnostics.deviceInfo) \(Diagnostics.info)" | |
let logStore: OSLogStore = try .init(scope: .currentProcessIdentifier) | |
let dateFormatter: DateFormatter = .init() | |
dateFormatter.timeZone = .init(secondsFromGMT: TimeZone.current.secondsFromGMT()) | |
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" | |
let position = logStore // one hour back | |
.position(date: Date(timeIntervalSinceNow: -60 * 60)) | |
let predicate = NSPredicate( | |
format: "subsystem == %@", | |
argumentArray: [Diagnostics.appBundle] | |
) | |
let logs: Array<String> = try logStore | |
.getEntries( | |
at: position, | |
matching: predicate | |
) | |
.map { (logEntry: OSLogEntry) -> String in | |
if let entryWithPayload: OSLogEntryWithPayload = logEntry as? OSLogEntryWithPayload { | |
return "[\(dateFormatter.string(from: logEntry.date))] [\(entryWithPayload.category)] \(logEntry.composedMessage)" | |
} | |
else { | |
return "[\(dateFormatter.string(from: logEntry.date))] \(logEntry.composedMessage)" | |
} | |
} | |
return [environmentInfo] + logs | |
}.value | |
} | |
} | |
/// Source code location for diagnostics purposes. | |
public struct DiagnosticsLocation { | |
@usableFromInline internal let file: StaticString | |
@usableFromInline internal let line: UInt | |
@usableFromInline internal let column: UInt | |
@_transparent public static func location( | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> Self { | |
.init( | |
file: file, | |
line: line, | |
column: column | |
) | |
} | |
@usableFromInline internal init( | |
file: StaticString, | |
line: UInt, | |
column: UInt | |
) { | |
self.file = file | |
self.line = line | |
self.column = column | |
} | |
} | |
extension DiagnosticsLocation: Sendable {} | |
extension DiagnosticsLocation: Hashable { | |
public static func == ( | |
_ lhs: DiagnosticsLocation, | |
_ rhs: DiagnosticsLocation | |
) -> Bool { | |
lhs.line == rhs.line | |
&& lhs.column == rhs.column | |
&& lhs.file.description == rhs.file.description | |
} | |
public func hash( | |
into hasher: inout Hasher | |
) { | |
hasher.combine(self.file.description) | |
hasher.combine(self.line) | |
hasher.combine(self.column) | |
} | |
} | |
extension DiagnosticsLocation: CustomStringConvertible { | |
public var description: String { | |
@_transparent get { | |
"\(self.file):\(self.line):\(self.column)" | |
} | |
} | |
} | |
extension DiagnosticsLocation: CustomDebugStringConvertible { | |
public var debugDescription: String { | |
@_transparent get { | |
"\(self.file):\(self.line):\(self.column)" | |
} | |
} | |
} |
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
// MARK: - ViewState | |
import SwiftUI | |
import Combine | |
@dynamicMemberLookup @propertyWrapper | |
public final class ViewState<State> | |
where State: Equatable { | |
@MainActor public var wrappedValue: State { | |
willSet { self.state.send(newValue) } | |
} | |
fileprivate let state: CurrentValueSubject<State, Never> | |
public init( | |
_ wrappedValue: State | |
) { | |
self.wrappedValue = wrappedValue | |
self.state = .init(wrappedValue) | |
} | |
@MainActor public subscript<Value>( | |
dynamicMember keyPath: WritableKeyPath<State, Value> | |
) -> Value { | |
_read { | |
yield self.wrappedValue[keyPath: keyPath] | |
} | |
_modify { | |
yield &self.wrappedValue[keyPath: keyPath] | |
} | |
} | |
@MainActor public func mutate( | |
_ mutation: @MainActor (inout State) -> Void | |
) { | |
mutation(&self.wrappedValue) | |
} | |
@MainActor public var projectedValue: Binding<State> { | |
.init( | |
get: { self.wrappedValue }, | |
set: { (newState: State) in | |
self.wrappedValue = newState | |
} | |
) | |
} | |
} | |
extension ViewState { | |
internal func trim<Value>( | |
to keyPath: KeyPath<State, Value> | |
) -> TrimmedViewState<State, Value> { | |
TrimmedViewState( | |
from: self, | |
at: keyPath | |
) | |
} | |
} | |
// MARK: - TrimmedViewState | |
internal final class TrimmedViewState<SourceState, State>: ObservableObject | |
where SourceState: Equatable, State: Equatable { | |
internal let objectWillChange: Publishers.RemoveDuplicates<Publishers.MapKeyPath<CurrentValueSubject<SourceState, Never>, State>> | |
internal var current: State { | |
@MainActor get { self.read() } | |
} | |
private let read: @MainActor () -> State | |
fileprivate init( | |
from source: ViewState<SourceState>, | |
at keyPath: KeyPath<SourceState, State> | |
) { | |
self.objectWillChange = source | |
.state | |
.map(keyPath) | |
.removeDuplicates() | |
self.read = { source.wrappedValue[keyPath: keyPath] } | |
} | |
} | |
// MARK: - ViewController | |
@_exported import SwiftUI | |
public protocol ViewController: AnyObject { | |
associatedtype Module: FeaturesModule | |
associatedtype State: Equatable = Stateless | |
associatedtype Parameter = Void | |
var viewState: ViewState<State> { get } | |
var features: Features<Module> { get } | |
init( | |
in features: Features<Module>, | |
parameter: Parameter | |
) | |
} | |
extension ViewController | |
where Parameter == Void { | |
@_transparent public init( | |
in features: Features<Module> | |
) { | |
self.init( | |
in: features, | |
parameter: Void() | |
) | |
} | |
} | |
extension ViewController | |
where State == Stateless { | |
public var viewState: ViewState<State> { Stateless.state } | |
} | |
public struct Stateless: Equatable { | |
fileprivate static let state: ViewState<Self> = .init(.init()) | |
public init() {} | |
} | |
extension ViewController { | |
public typealias Context<ContextModule> = ControllerContext<Self.Module, ContextModule> | |
where ContextModule: FeaturesModule | |
public typealias Dependency<Feature> = ControllerDependency<Self.Module, Feature> | |
where Feature: FeatureInterface | |
} | |
// MARK: - ControlledView | |
import SwiftUI | |
public protocol ControlledView: View { | |
associatedtype Controller: ViewController | |
var controller: Controller { get } | |
init(controller: Controller) | |
} | |
extension ControlledView { | |
@_transparent public init( | |
in features: Features<Controller.Module>, | |
parameter: Controller.Parameter | |
) { | |
self.init( | |
controller: .init( | |
in: features, | |
parameter: parameter | |
) | |
) | |
} | |
} | |
extension ControlledView | |
where Controller.Parameter == Void { | |
@_transparent public init( | |
in features: Features<Controller.Module> | |
) { | |
self.init( | |
controller: .init( | |
in: features, | |
parameter: Void() | |
) | |
) | |
} | |
} | |
extension ControlledView { | |
@MainActor public func with<Content, Value>( | |
_ keyPath: KeyPath<Controller.State, Value>, | |
@ViewBuilder _ content: @escaping @MainActor (Value) -> Content | |
) -> some View | |
where Content: View, Value: Equatable { | |
With( | |
state: self.controller.state(at: keyPath), | |
content: content | |
) | |
} | |
@MainActor public func withEach<Content, Value>( | |
_ keyPath: KeyPath<Controller.State, Value>, | |
@ViewBuilder _ content: @escaping @MainActor (Value.Element) -> Content | |
) -> some View | |
where Content: View, Value: Equatable, Value: RandomAccessCollection, Value.Element: Identifiable { | |
WithEach( | |
state: self.controller.state(at: keyPath), | |
content: content | |
) | |
} | |
} | |
extension ViewController { | |
fileprivate func state<Value>( | |
at keyPath: KeyPath<State, Value> | |
) -> TrimmedViewState<State, Value> | |
where Value: Equatable { | |
self.viewState.trim(to: keyPath) | |
} | |
} | |
// MARK: - With | |
private struct With<SourceState, State, Content>: View | |
where SourceState: Equatable, State: Equatable, Content: View { | |
@StateObject private var state: TrimmedViewState<SourceState, State> | |
private let content: @MainActor (State) -> Content | |
fileprivate init( | |
state: TrimmedViewState<SourceState, State>, | |
content: @escaping @MainActor (State) -> Content | |
) { | |
self._state = .init(wrappedValue: state) | |
self.content = content | |
} | |
fileprivate var body: some View { | |
self.content(self.state.current) | |
} | |
} | |
// MARK: - WithEach | |
private struct WithEach<SourceState, State, Content>: View | |
where SourceState: Equatable, State: Equatable, State: RandomAccessCollection, State.Element: Identifiable, Content: View { | |
@StateObject private var state: TrimmedViewState<SourceState, State> | |
private let content: @MainActor (State.Element) -> Content | |
fileprivate init( | |
state: TrimmedViewState<SourceState, State>, | |
content: @escaping @MainActor (State.Element) -> Content | |
) { | |
self._state = .init(wrappedValue: state) | |
self.content = content | |
} | |
fileprivate var body: some View { | |
ForEach(self.state.current) { (element: State.Element) in | |
self.content(element) | |
} | |
} | |
} | |
// MARK: - ControllerContext | |
/// Access module context value in the features tree. | |
/// Please use `Context` inside feature implementation instead. | |
@propertyWrapper | |
public struct ControllerContext<Module, ContextModule> | |
where Module: FeaturesModule, ContextModule: FeaturesModule { | |
public var wrappedValue: ContextModule.Context { | |
@available(*, unavailable, message: "Context can be used only inside ViewController.") | |
get { fatalError("Unavailable") } | |
} | |
@usableFromInline internal let cache: ContextModule.Context? | |
@usableFromInline internal init( | |
cache: ContextModule.Context? | |
) { | |
self.cache = cache | |
} | |
@inlinable public static subscript<Controller>( | |
_enclosingInstance instance: Controller, | |
wrapped wrappedKeyPath: KeyPath<Controller, ContextModule.Context>, | |
storage storageKeyPath: ReferenceWritableKeyPath<Controller, Self> | |
) -> ContextModule.Context | |
where Controller: ViewController, Controller.Module == Module { | |
if let context: ContextModule.Context = instance[keyPath: storageKeyPath].cache { | |
return context | |
} | |
else { | |
let node: FeaturesTreeNode = instance[keyPath: \.features].node.node(for: ContextModule.self) | |
let context: ContextModule.Context = Features<ContextModule>(from: node).context | |
instance[keyPath: storageKeyPath] = .init(cache: context) | |
return context | |
} | |
} | |
} | |
// MARK: - ControllerDependency | |
/// Access a feature instance in the features tree. | |
/// Please use `Dependency` inside feature implementation instead. | |
@propertyWrapper | |
public struct ControllerDependency<Module, Interface> | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
public var wrappedValue: Interface { | |
@available(*, unavailable, message: "Dependency can be used only inside ViewController.") | |
get { fatalError("Unavailable") } | |
} | |
// TODO: verify thread safety of cache access - it might not be safe at all now | |
private let cache: Interface? | |
private let path: KeyPath<Features<Module>, Interface> | |
/// Define path for accessing the feature. | |
public init( | |
_ path: KeyPath<Features<Module>, Interface> | |
) { | |
self.cache = .none | |
self.path = path | |
} | |
private init( | |
cache: Interface?, | |
path: KeyPath<Features<Module>, Interface> | |
) { | |
self.cache = cache | |
self.path = path | |
} | |
public static subscript<Controller>( | |
_enclosingInstance instance: Controller, | |
wrapped wrappedKeyPath: KeyPath<Controller, Interface>, | |
storage storageKeyPath: ReferenceWritableKeyPath<Controller, Self> | |
) -> Interface | |
where Controller: ViewController, Controller.Module == Module { | |
let dependency: ControllerDependency<Module, Interface> = instance[keyPath: storageKeyPath] | |
if let cached: Interface = dependency.cache { | |
return cached | |
} | |
else { | |
let feature: Interface = instance[keyPath: \.features][keyPath: dependency.path] | |
instance[keyPath: storageKeyPath] = .init( | |
cache: feature, | |
path: dependency.path | |
) | |
return feature | |
} | |
} | |
} | |
// MARK: - Type safety extensions for ancestors - add more if needed | |
// MARK: - ControllerContext ancestor extensions | |
extension ControllerContext { | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
} |
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
// MARK: - FeaturesModule | |
/// Module defines availability and lifetime of features. | |
/// Modules should group and encapsulate reusable parts | |
/// of the application allowing granulated access to logically | |
/// connected features parametrized with common context value. | |
/// Do not create instances of modules directly outside of | |
/// modules registry during initialization of features tree. | |
public protocol FeaturesModule: Sendable { | |
/// Module directly required by this module. | |
associatedtype Ancestor: FeaturesModule | |
/// Optional value to parametrize associated features. | |
associatedtype Context: Sendable = Void | |
/// Placeholder for unavailable/undefined modules. | |
/// Should also provide placeholder implementations of features. | |
nonisolated static var placeholder: Self { get } | |
} | |
extension FeaturesModule { | |
/// Property wrapper for declaring features inside a module. | |
/// `@Declare(FeatureType.self) var featureNameInModule` | |
public typealias Declare<Interface> = Feature<Self, Interface> | |
where Interface: FeatureInterface | |
} | |
extension FeaturesModule { | |
/// Internal identifier of module. | |
internal nonisolated static var identifier: ObjectIdentifier { | |
@_transparent get { .init(Self.self) } } | |
} | |
/// Module dedicated for providing features tree root. | |
/// Can't have any associated features or context. | |
/// Root module has to have this module as its Ancestor. | |
public enum RootFeaturesModule: FeaturesModule { | |
// Using itself as an ancestor - it does not provide | |
// any features nor context so can't be accessed anyway. | |
public typealias Ancestor = RootFeaturesModule | |
public typealias Context = Never | |
case placeholder | |
} | |
// MARK: - FeatureInterface | |
/// Base protocol for all features. | |
public protocol FeatureInterface { | |
/// Placeholder for unavailable/undefined features. | |
/// Instance used as placeholder should not crash nor | |
/// indicate its usage when initializd. However any usage | |
/// of its methods should either crash or indicate invalid usage. | |
/// You can use `placeholderValue` or `placeholderError` | |
/// to provide function placeholders. | |
nonisolated static var placeholder: Self { get } | |
} | |
// MARK: - FeatureImplementation | |
/// Protocol used to define implementation of features. | |
/// Despite being optional it is strongly recommended to | |
/// provide implementations of features with this protocol. | |
public protocol FeatureImplementation<Module, Feature>: AnyObject { | |
/// Module where implementation is valid. | |
associatedtype Module: FeaturesModule | |
/// Implemented feature type/interface. | |
associatedtype Feature: FeatureInterface | |
/// Define if instance will be cached in module (true) | |
/// or created each time when requested (false). Default is true. | |
static var cacheable: Bool { get } | |
/// Instance of feature prepared by this implementation. | |
var instance: Feature { get } | |
/// Features subtree used to initialize and access other features. | |
var features: Features<Module> { get } | |
/// Required initializer to prepare instance in features tree. | |
init(in features: Features<Module>) | |
} | |
extension FeatureImplementation { | |
/// Access to context of specified module. Module has to | |
/// be either the same as required by implementation | |
/// or one of its ancestors. | |
/// `@Context(of: Module.self) var context` | |
public typealias Context<ContextModule> = FeatureContext<Self.Module, ContextModule> | |
where ContextModule: FeaturesModule | |
/// Access other features from features tree. | |
/// `@Dependency(\.featureNameInTree) var localFeatureName` | |
public typealias Dependency<Feature> = FeatureDependency<Self.Module, Feature> | |
where Feature: FeatureInterface | |
// Default implementation of cacheable. | |
public static var cacheable: Bool { true } | |
} | |
// MARK: - Feature | |
/// Declaration of feature. Used in module definition to associate | |
/// feature interfaces with the module. It has a role of proxy | |
/// between accessing feature and providing its implementation (loader). | |
/// - Warning: Do not use it directly, please use `Declare` inside | |
/// module context instead. | |
@propertyWrapper | |
public struct Feature<Module, Interface>: Sendable | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
/// Loader is a function called to create instances of the feature. | |
public typealias Loader = @Sendable (Features<Module>) -> Interface | |
/// Define implementation by using provided instance. | |
@_transparent public static func use( | |
_ instance: Interface | |
) -> Self { | |
Self( | |
// access without cache is faster | |
// here instance is already made and reused | |
cacheable: false, | |
{ (_: Features<Module>) in instance } | |
) | |
} | |
/// Define implementation by using provided custom function. | |
@_transparent public static func use( | |
cacheable: Bool = true, | |
_ loader: @escaping Loader | |
) -> Self { | |
Self( | |
cacheable: cacheable, | |
loader | |
) | |
} | |
/// Define implementation by using provided implementation type. | |
@_transparent public static func use<Implementation>( | |
_ implementation: Implementation.Type | |
) -> Self | |
where Implementation: FeatureImplementation<Module, Interface> { | |
Self( | |
cacheable: Implementation.cacheable | |
) { (features: Features<Module>) -> Interface in | |
Implementation(in: features).instance | |
} | |
} | |
/// Access to the declaration (self). Should not be used directly. | |
public var wrappedValue: Self { | |
@_transparent get { self } | |
@_transparent set { self = newValue } | |
} | |
private let loader: Loader | |
internal let instanceCache: FeatureCache<Interface>? | |
/// Default, placeholder definition of feature. Should be used | |
/// to define module content and overriden when defining module instance. | |
public init( | |
_: Interface.Type = Interface.self, | |
in module: Module.Type = Module.self | |
) { | |
self.init( | |
// placeholders use cache in order to allow | |
// patching instances for mocking later | |
cacheable: true | |
) { (features: Features<Module>) -> Interface in | |
assertionFailure("Attempting to use unimplemented feature (\(Interface.self) in \(Module.self)! You have to provide implementations of all features when registering module during features tree initialization.") | |
return .placeholder | |
} | |
} | |
@usableFromInline internal init( | |
cacheable: Bool, | |
_ loader: @escaping Loader | |
) { | |
self.loader = loader | |
self.instanceCache = cacheable ? .init() : .none | |
} | |
@Sendable internal func instance( | |
in features: Features<Module> | |
) -> Interface { | |
if let instanceCache: FeatureCache<Interface> = self.instanceCache { | |
return instanceCache | |
.use( | |
in: features, | |
with: self.loader | |
) | |
} | |
else { | |
return self.loader(features) | |
} | |
} | |
@Sendable internal func patch<Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
with patched: Patched | |
) { | |
if let instanceCache: FeatureCache<Interface> = self.instanceCache { | |
instanceCache | |
.patch( | |
keyPath, | |
with: patched | |
) | |
} | |
else { | |
assertionFailure("Can't patch a feature which does not use a cache! Please use default implementation (placeholder) for mocking.") | |
} | |
} | |
} | |
// MARK: - FeaturePath | |
/// Path defining access to a feature in a module. It does not represent | |
/// access to feature instance directly but allows (and defines) | |
/// access through a Features tree instance. | |
public typealias FeaturePath<Module, Interface> = KeyPath<Module, Feature<Module, Interface>> | |
where Module: FeaturesModule, Interface: FeatureInterface | |
// MARK: - FeatureCache | |
import struct os.os_unfair_lock | |
import func os.os_unfair_lock_lock | |
import func os.os_unfair_lock_unlock | |
internal final class FeatureCache<Interface>: @unchecked Sendable | |
where Interface: FeatureInterface { | |
private var lock: os_unfair_lock | |
private var instance: Interface? | |
internal init( | |
instance: Interface? = .none | |
) { | |
self.lock = .init() | |
self.instance = instance | |
} | |
@Sendable internal func use<Module>( | |
in features: Features<Module>, | |
with loader: @Sendable (Features<Module>) -> Interface | |
) -> Interface { | |
// if this crashes because of recursive lock access | |
// it means that you have made a circular dependency | |
// between features, please delay initialization of one of them | |
os_unfair_lock_lock(&self.lock) | |
if let instance: Interface = self.instance { | |
os_unfair_lock_unlock(&self.lock) | |
return instance | |
} | |
else { | |
let instance: Interface = loader(features) | |
self.instance = instance | |
os_unfair_lock_unlock(&self.lock) | |
return instance | |
} | |
} | |
@Sendable internal func patch<Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
with patched: Patched | |
) { | |
os_unfair_lock_lock(&self.lock) | |
if var instance: Interface = self.instance { | |
instance[keyPath: keyPath] = patched | |
self.instance = instance | |
os_unfair_lock_unlock(&self.lock) | |
} | |
else { | |
var instance: Interface = .placeholder | |
instance[keyPath: keyPath] = patched | |
self.instance = instance | |
os_unfair_lock_unlock(&self.lock) | |
} | |
} | |
} | |
// MARK: - ModulesRegistry | |
/// Registry of modules available in Features tree. | |
/// Defines feature implementations used in the tree. | |
public struct ModulesRegistry { | |
private var modules: Dictionary<ObjectIdentifier, @Sendable () -> any FeaturesModule> | |
internal init() { | |
self.modules = [ | |
// RootFeaturesModule have to be defined in the registry. | |
RootFeaturesModule.identifier: | |
{ RootFeaturesModule.placeholder } | |
] | |
} | |
/// Define module with associated feature implementations. | |
/// Note that instances of module will be created on demand | |
/// based on provided instance. | |
public mutating func use<Module>( | |
_ module: @escaping @autoclosure @Sendable () -> Module | |
) where Module: FeaturesModule { | |
assert( | |
!self.modules.keys.contains(Module.identifier), | |
"Please register module (\(Module.self)) only once!" | |
) | |
assert( | |
self.modules.keys.contains(Module.Ancestor.identifier), | |
"Please register ancestor module (\(Module.Ancestor.self)) first!" | |
) | |
self.modules[Module.identifier] = module | |
} | |
internal func instance<Module>( | |
of _: Module.Type | |
) -> Module | |
where Module: FeaturesModule { | |
guard let prepareInstance: @Sendable () -> any FeaturesModule = self.modules[Module.identifier] | |
else { | |
assertionFailure("Attempting to access unknown module (\(Module.self))! Please register all of your modules when creating features tree.") | |
return .placeholder | |
} | |
if let instance: Module = prepareInstance() as? Module { | |
return instance | |
} | |
else { | |
fatalError("Internal inconsistency - please report a bug!") | |
} | |
} | |
} | |
// MARK: - Features | |
/// Features tree defining access and lifetime to features. | |
/// Instances of Features provide direct access to a specific node | |
/// in features tree defined by the Module parameter. It allows | |
/// accessing features from that module and all of its ancestor modules. | |
@dynamicMemberLookup | |
public struct Features<Module> | |
where Module: FeaturesModule { | |
@usableFromInline internal var node: FeaturesTreeNode { | |
@_transparent get { | |
if let node: FeaturesTreeNode = self._node { | |
return node | |
} | |
else { | |
fatalError("Attempting to use deallocated features branch! This is most likely a memory leak.") | |
} | |
} | |
} | |
@usableFromInline internal private(set) weak var _node: FeaturesTreeNode! | |
private let retainedNode: FeaturesTreeNode? | |
@usableFromInline internal init( | |
from node: FeaturesTreeNode | |
) { | |
self._node = node | |
self.retainedNode = .none | |
} | |
@usableFromInline internal init( | |
retained node: FeaturesTreeNode | |
) { | |
self._node = node | |
self.retainedNode = node | |
} | |
/// Retain reference to the tree node to prevent its deallocation. | |
/// Features instances (except just branched which is always retained) | |
/// will be deallocated unless at least one becomes retained and kept. | |
/// Lifetime of all retained instances of Features define lifetime of | |
/// associated subtree. | |
@_transparent public func retained() -> Self { | |
Features(retained: self.node) | |
} | |
/// Access context value for associated module. | |
public var context: Module.Context { | |
self.node.context(of: Module.self) | |
} | |
/// Access feature instance within this module. | |
/// Used by dynamic member lookup automatically. | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { self.node.feature(at: path) } | |
} | |
/// Create a features subtree for given module. | |
public func load<ForkedModule>( | |
_: ForkedModule.Type | |
) -> Features<ForkedModule> | |
where ForkedModule: FeaturesModule, ForkedModule.Context == Void, ForkedModule.Ancestor == Module { | |
Features<ForkedModule>( | |
retained: self.node | |
.createNode(for: ForkedModule.self) | |
) | |
} | |
/// Create a features subtree for given module and context. | |
public func load<ForkedModule>( | |
_: ForkedModule.Type, | |
with context: ForkedModule.Context | |
) -> Features<ForkedModule> | |
where ForkedModule: FeaturesModule, ForkedModule.Ancestor == Module { | |
Features<ForkedModule>( | |
retained: self.node | |
.createNode( | |
for: ForkedModule.self, | |
with: context | |
) | |
) | |
} | |
/// Access a features ancestor subtree with given module. | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
/// Patch an instance of a feature. | |
/// This method should not be used in production builds. | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
} | |
extension Features { | |
/// Create features tree root with specified module. | |
public static func tree<Root>( | |
_ registerModules: (inout ModulesRegistry) -> Void | |
) -> Features<Root> | |
where Root: FeaturesModule, Root.Context == Void, Root.Ancestor == RootFeaturesModule { | |
var registry: ModulesRegistry = .init() | |
registerModules(®istry) | |
return Features<RootFeaturesModule>( | |
retained: .init(registry: registry) | |
) | |
.load(Root.self) | |
} | |
/// Create features tree root with specified module and context. | |
public static func tree<Root>( | |
context: Root.Context, | |
_ registerModules: (inout ModulesRegistry) -> Void | |
) -> Features<Root> | |
where Root: FeaturesModule, Root.Ancestor == RootFeaturesModule { | |
var registry: ModulesRegistry = .init() | |
registerModules(®istry) | |
return Features<RootFeaturesModule>( | |
retained: .init(registry: registry) | |
) | |
.load(Root.self, with: context) | |
} | |
/// Create features tree root with specified module | |
/// prepared for testing and mocking. | |
public static func testTree<Root, Implementation>( | |
for: Implementation.Type, | |
at path: WritableKeyPath<Implementation.Module, Feature<Implementation.Module, Implementation.Feature>> | |
) -> Features<Root> | |
where Root: FeaturesModule, Root.Context == Void, Root.Ancestor == RootFeaturesModule, Implementation: FeatureImplementation { | |
var registry: ModulesRegistry = .init() | |
registry.use( | |
{ | |
var testedModule: Implementation.Module = .placeholder | |
testedModule[keyPath: path].wrappedValue = .use(Implementation.self) | |
return testedModule | |
}() | |
) | |
return Features<RootFeaturesModule>( | |
retained: .init(registry: registry) | |
) | |
.load(Root.self) | |
} | |
/// Create features tree root with specified module and context | |
/// prepared for testing and mocking. | |
public static func testTree<Root, Implementation>( | |
for: Implementation.Type, | |
at path: WritableKeyPath<Implementation.Module, Feature<Implementation.Module, Implementation.Feature>>, | |
rootContext: Root.Context | |
) -> Features<Root> | |
where Root: FeaturesModule, Root.Ancestor == RootFeaturesModule, Implementation: FeatureImplementation { | |
var registry: ModulesRegistry = .init() | |
registry.use( | |
{ | |
var testedModule: Implementation.Module = .placeholder | |
testedModule[keyPath: path].wrappedValue = .use(Implementation.self) | |
return testedModule | |
}() | |
) | |
return Features<RootFeaturesModule>( | |
retained: .init(registry: registry) | |
) | |
.load(Root.self, with: rootContext) | |
} | |
} | |
// MARK: - FeaturesTreeNode | |
@usableFromInline internal final class FeaturesTreeNode { | |
internal let module: any FeaturesModule | |
internal let context: Any | |
internal let registry: ModulesRegistry | |
@usableFromInline internal let parent: FeaturesTreeNode? | |
internal init( | |
registry: ModulesRegistry | |
) { | |
self.module = RootFeaturesModule.placeholder | |
self.context = RootFeaturesModule.self | |
self.registry = registry | |
self.parent = .none | |
} | |
internal init<Module>( | |
module: Module, | |
context: Module.Context, | |
parent: FeaturesTreeNode | |
) where Module: FeaturesModule { | |
self.module = module | |
self.context = context | |
self.registry = parent.registry | |
self.parent = parent | |
} | |
@usableFromInline @Sendable internal func feature<Module, Interface>( | |
at path: FeaturePath<Module, Interface> | |
) -> Interface | |
where Module: FeaturesModule { | |
if let module: Module = self.module as? Module { | |
return module[keyPath: path] | |
.instance(in: Features<Module>(from: self)) | |
} | |
else if let parent: FeaturesTreeNode = self.parent { | |
return parent.feature(at: path) | |
} | |
else { | |
assertionFailure("Attempting to access a feature (\(Interface.self)) outside of its module (\(Module.self))!") | |
return .placeholder | |
} | |
} | |
@Sendable internal func context<Module>( | |
of _: Module.Type | |
) -> Module.Context | |
where Module: FeaturesModule { | |
if self.module is Module, let context: Module.Context = self.context as? Module.Context { | |
return context | |
} | |
else if let parent: FeaturesTreeNode = self.parent { | |
return parent.context(of: Module.self) | |
} | |
else { | |
fatalError("Attempting to access a context of unavailable module (\(Module.self))!") | |
} | |
} | |
@usableFromInline @Sendable internal func node<Module>( | |
for _: Module.Type | |
) -> FeaturesTreeNode | |
where Module: FeaturesModule { | |
if self.module is Module { | |
return self | |
} | |
else if let parent: FeaturesTreeNode = self.parent { | |
return parent.node(for: Module.self) | |
} | |
else { | |
fatalError("Attempting to access inaccessible module \(Module.self)!") | |
} | |
} | |
@Sendable internal func createNode<Module>( | |
for _: Module.Type | |
) -> FeaturesTreeNode | |
where Module: FeaturesModule, Module.Context == Void { | |
FeaturesTreeNode( | |
module: self.registry.instance(of: Module.self), | |
context: Void(), | |
parent: self.node(for: Module.Ancestor.self) | |
) | |
} | |
@Sendable internal func createNode<Module>( | |
for _: Module.Type, | |
with context: Module.Context | |
) -> FeaturesTreeNode | |
where Module: FeaturesModule { | |
FeaturesTreeNode( | |
module: self.registry.instance(of: Module.self), | |
context: context, | |
parent: self.node(for: Module.Ancestor.self) | |
) | |
} | |
@Sendable internal func patch<Module, Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module, Interface>, | |
with patched: Patched | |
) where Module: FeaturesModule, Interface: FeatureInterface { | |
guard let module: Module = self.node(for: Module.self).module as? Module | |
else { | |
return assertionFailure("Attempting to patch a feature in undefined module, patch will be ignored!") | |
} | |
module[keyPath: featurePath] | |
.instanceCache? | |
.patch(keyPath, with: patched) | |
} | |
} | |
// MARK: - FeatureContext | |
/// Access module context value in the features tree. | |
/// Please use `Context` inside feature implementation instead. | |
@propertyWrapper | |
public struct FeatureContext<Module, ContextModule> | |
where Module: FeaturesModule, ContextModule: FeaturesModule { | |
public var wrappedValue: ContextModule.Context { | |
@available(*, unavailable, message: "Context can be used only inside FeatureImplementation.") | |
get { fatalError("Unavailable") } | |
} | |
@usableFromInline internal let cache: ContextModule.Context? | |
@usableFromInline internal init( | |
cache: ContextModule.Context? | |
) { | |
self.cache = cache | |
} | |
@inlinable public static subscript<Implementation>( | |
_enclosingInstance instance: Implementation, | |
wrapped wrappedKeyPath: KeyPath<Implementation, ContextModule.Context>, | |
storage storageKeyPath: ReferenceWritableKeyPath<Implementation, Self> | |
) -> ContextModule.Context | |
where Implementation: FeatureImplementation, Implementation.Module == Module { | |
if let context: ContextModule.Context = instance[keyPath: storageKeyPath].cache { | |
return context | |
} | |
else { | |
let node: FeaturesTreeNode = instance[keyPath: \.features].node.node(for: ContextModule.self) | |
let context: ContextModule.Context = Features<ContextModule>(from: node).context | |
instance[keyPath: storageKeyPath] = .init(cache: context) | |
return context | |
} | |
} | |
} | |
// MARK: - FeatureDependency | |
/// Access a feature instance in the features tree. | |
/// Please use `Dependency` inside feature implementation instead. | |
@propertyWrapper | |
public struct FeatureDependency<Module, Interface> | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
public var wrappedValue: Interface { | |
@available(*, unavailable, message: "Dependency can be used only inside FeatureImplementation.") | |
get { fatalError("Unavailable") } | |
} | |
// TODO: verify thread safety of cache access - it might not be safe at all now | |
private let cache: Interface? | |
private let path: KeyPath<Features<Module>, Interface> | |
/// Define path for accessing the feature. | |
public init( | |
_ path: KeyPath<Features<Module>, Interface> | |
) { | |
self.cache = .none | |
self.path = path | |
} | |
private init( | |
cache: Interface?, | |
path: KeyPath<Features<Module>, Interface> | |
) { | |
self.cache = cache | |
self.path = path | |
} | |
public static subscript<Implementation>( | |
_enclosingInstance instance: Implementation, | |
wrapped wrappedKeyPath: KeyPath<Implementation, Interface>, | |
storage storageKeyPath: ReferenceWritableKeyPath<Implementation, Self> | |
) -> Interface | |
where Implementation: FeatureImplementation, Implementation.Module == Module { | |
let dependency: FeatureDependency<Module, Interface> = instance[keyPath: storageKeyPath] | |
if let cached: Interface = dependency.cache { | |
return cached | |
} | |
else { | |
let feature: Interface = instance[keyPath: \.features][keyPath: dependency.path] | |
instance[keyPath: storageKeyPath] = .init( | |
cache: feature, | |
path: dependency.path | |
) | |
return feature | |
} | |
} | |
} | |
// MARK: - Type safety extensions for ancestors - add more if needed | |
// MARK: - Features ancestor extensions | |
extension Features { | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
public subscript<Interface>( | |
dynamicMember path: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface> | |
) -> Interface | |
where Interface: FeatureInterface { | |
@_transparent get { | |
self.node.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.parent?.feature(at: path) ?? .placeholder | |
} | |
} | |
} | |
extension Features { | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
public func subtree<AncestorModule>( | |
for: AncestorModule.Type | |
) -> Features<AncestorModule> | |
where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == AncestorModule { | |
Features<AncestorModule>( | |
from: self.node | |
.node(for: AncestorModule.self) | |
) | |
} | |
} | |
extension Features { | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
@discardableResult | |
@Sendable public func patch<Interface, Patched>( | |
_ keyPath: WritableKeyPath<Interface, Patched>, | |
in featurePath: FeaturePath<Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor, Interface>, | |
with patched: Patched | |
) -> Self | |
where Module: FeaturesModule, Interface: FeatureInterface { | |
self.node.patch( | |
keyPath, | |
in: featurePath, | |
with: patched | |
) | |
return self | |
} | |
} | |
// MARK: - FeatureContext ancestor extensions | |
extension FeatureContext { | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
@_transparent public init( | |
of _: ContextModule.Type | |
) where Module.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor.Ancestor == ContextModule { | |
self.init(cache: .none) | |
} | |
} |
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
/// Placehodler to fill some gaps. | |
public struct Placeholder: TheError { | |
public static func error( | |
file: StaticString, | |
line: UInt, | |
column: UInt | |
) -> Self { | |
.init( | |
diagnosticsLocation: .location( | |
file: file, | |
line: line, | |
column: column | |
) | |
) | |
} | |
public let diagnosticsMessage: StaticString = "Placeholder" | |
public let diagnosticsLocation: DiagnosticsLocation | |
public var displayableMessage: String { | |
self.diagnosticsLocation.description | |
} | |
} | |
// TODO: add variadic parameters if able | |
extension Placeholder { | |
@_transparent @Sendable public static func returning<Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable () -> Value { | |
{ @Sendable () -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1) -> Value { | |
{ @Sendable (_: Arg1) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Arg4, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Arg4, Arg5, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6, _: Arg7) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func returning<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Value>( | |
_ value: @autoclosure @escaping () -> Value, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8) -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6, _: Arg7, _: Arg8) -> Value in | |
Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
return value() | |
} | |
} | |
@_transparent @Sendable public static func throwing<Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable () throws -> Value { | |
{ @Sendable () throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1) throws -> Value { | |
{ @Sendable (_: Arg1) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Arg3, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func retuthrowingrning<Arg1, Arg2, Arg3, Arg4, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Arg3, Arg4, Arg5, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6, _: Arg7) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
@_transparent @Sendable public static func throwing<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Value>( | |
_ error: @autoclosure @escaping () -> Error? = .none, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> @Sendable (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8) throws -> Value { | |
{ @Sendable (_: Arg1, _: Arg2, _: Arg3, _: Arg4, _: Arg5, _: Arg6, _: Arg7, _: Arg8) throws -> Value in | |
if let error: Error = error() { | |
throw error | |
.asTheError( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
else { | |
throw Placeholder | |
.error( | |
file: file, | |
line: line, | |
column: column | |
) | |
.asAssertionFailure( | |
file: file, | |
line: line | |
) | |
} | |
} | |
} | |
} |
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
/// "One Error to rule them all, One Error to handle them, One Error to bring them all, and on the screen bind them." | |
public protocol TheError: Error, CustomStringConvertible, CustomDebugStringConvertible { | |
/// Message representing this error in logs. | |
var diagnosticsMessage: StaticString { get } | |
/// Source code location of error occurence to improve diagnostics. | |
var diagnosticsLocation: DiagnosticsLocation { get } | |
/// Message that can be displayed to the end user. | |
/// Should be localized if able. | |
var displayableMessage: String { get } | |
/// Log this error using ``Diagnostics``. | |
/// In order to customize a log content you | |
/// override this method. | |
@discardableResult func log() -> Self | |
} | |
extension TheError /* CustomStringConvertible */ { | |
public var description: String { | |
@_transparent get { | |
"\(Self.self)@\(self.diagnosticsLocation) - \(self.displayableMessage)" | |
} | |
} | |
} | |
extension TheError /* CustomDebugStringConvertible */ { | |
public var debugDescription: String { | |
let children: Mirror.Children = Mirror(reflecting: self) | |
.children // ignoring "displayStyle" | |
let propertiesDescription: String | |
if children.isEmpty { | |
propertiesDescription = "N/A" | |
} | |
else { | |
propertiesDescription = children | |
.reduce(into: String()) { result, child in | |
let formattedLabel: String | |
if let label: String = child.label { | |
// `context` is part of debug description anyway | |
// `displayableString` has own section in description | |
// `group` is part of debug description anyway | |
guard label != "diagnosticsMessage", label != "diagnosticsLocation", label != "displayableMessage" | |
else { return } // skip property | |
formattedLabel = "\n⎜ 🧩 \(label): " | |
} | |
else { | |
formattedLabel = "\n⎜ 🧩 " | |
} | |
let formattedValue: String = | |
.init( | |
reflecting: child.value | |
) | |
.replacingOccurrences( // keep indentation | |
of: "\n", | |
with: "\n⎜ ⮑ " | |
) | |
result | |
.append("\(formattedLabel)\(formattedValue)") | |
} | |
} | |
return "⎡ ⚠️ \(Self.self)\n⎜ 📍 Location: \(self.diagnosticsLocation)\n⎜ ✉️ Message: \(self.diagnosticsMessage)\n⎜ 📺 Displayable: \(self.displayableMessage)\n⎜ 📦 Properties: \(propertiesDescription)" | |
} | |
} | |
extension TheError { | |
/// Exit (crash) with this error. | |
@_transparent public func asFatalError( | |
file: StaticString = #fileID, | |
line: UInt = #line | |
) -> Never { | |
fatalError( | |
"\(Self.self)", | |
file: file, | |
line: line | |
) | |
} | |
/// Assertion failure with this error. | |
@_transparent @discardableResult public func asAssertionFailure( | |
file: StaticString = #fileID, | |
line: UInt = #line | |
) -> Self { | |
#if DEBUG | |
assertionFailure( | |
"\(Self.self)", | |
file: file, | |
line: line | |
) | |
#endif | |
return self | |
} | |
@discardableResult public func log() -> Self { | |
#if DEBUG | |
Diagnostics.logger.log(level: .error, "\n\(self.debugDescription, privacy: .auto)") | |
#else | |
Diagnostics.logger.log(level: .error, "\("\(Self.self)", privacy: .public)@\(self.diagnosticsLocation, privacy: .public)\(self.diagnosticsMessage, privacy: .public)") | |
#endif | |
return self | |
} | |
} | |
extension Error { | |
/// Convert any error to TheError by wrapping in ``Undefined`` if needed. | |
@_transparent public func asTheError( | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> TheError { | |
if let theError: TheError = self as? TheError { | |
return theError | |
} | |
else { | |
return Undefined | |
.error( | |
self, | |
file: file, | |
line: line, | |
column: column | |
) | |
} | |
} | |
/// Log this error after conversion to TheError. | |
@_transparent public func log( | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> TheError { | |
if let theError: TheError = self as? TheError { | |
return theError.log() | |
} | |
else { | |
return Undefined | |
.error( | |
self, | |
file: file, | |
line: line, | |
column: column | |
) | |
.log() | |
} | |
} | |
} | |
// MARK: - Undefined | |
/// Undefined wraps any error inside TheError. | |
public struct Undefined: TheError { | |
public static func error( | |
_ error: Error, | |
file: StaticString = #fileID, | |
line: UInt = #line, | |
column: UInt = #column | |
) -> Self { | |
.init( | |
error: error, | |
diagnosticsLocation: .location( | |
file: file, | |
line: line, | |
column: column | |
) | |
) | |
} | |
public let error: Error | |
public let diagnosticsMessage: StaticString = "Undefined" | |
public let diagnosticsLocation: DiagnosticsLocation | |
public var displayableMessage: String { | |
self.error.localizedDescription | |
} | |
@discardableResult public func log() -> Self { | |
#if DEBUG | |
Diagnostics.logger.log(level: .error, "\n\(self.debugDescription, privacy: .auto)") | |
#else | |
Diagnostics.logger.log(level: .error, "\("\(Self.self)", privacy: .public)@\(self.diagnosticsLocation, privacy: .public)\(self.error, privacy: .public)") | |
#endif | |
return self | |
} | |
} | |
// MARK: - Cancelled | |
public typealias Cancelled = CancellationError | |
extension CancellationError: TheError { | |
public static func error() -> Self { | |
.init() | |
} | |
public var diagnosticsMessage: StaticString { | |
@_transparent get { "Cancelled" } | |
} | |
public var diagnosticsLocation: DiagnosticsLocation { | |
@_transparent get { // CancellationError can't store location | |
.location(file: "Cancelled", line: 0, column: 0) | |
} | |
} | |
public var displayableMessage: String { | |
self.localizedDescription | |
} | |
@discardableResult public func log() -> Self { | |
#if DEBUG | |
Diagnostics.logger.log(level: .error, "\n⎡ ⚠️ Cancelled\n⎜ 📍 Location: N/A\n⎜ ✉️ Message: Cancelled\n⎜ 📺 Displayable: \(self.displayableMessage, privacy: .public)\n⎜ 📦 Properties: N/A") | |
#else | |
Diagnostics.logger.log(level: .error, "Operation has been cancelled") | |
#endif | |
return self | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment