Skip to content

Instantly share code, notes, and snippets.

@KaQuMiQ
Last active August 3, 2023 15:47
Show Gist options
  • Save KaQuMiQ/2dfe2b2390f145632feb8a525f5b91e0 to your computer and use it in GitHub Desktop.
Save KaQuMiQ/2dfe2b2390f145632feb8a525f5b91e0 to your computer and use it in GitHub Desktop.
MQLite
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)"
}
}
}
// 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)
}
}
// 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(&registry)
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(&registry)
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)
}
}
/// 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
)
}
}
}
}
/// "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