diff --git a/Alicerce.xcodeproj/project.pbxproj b/Alicerce.xcodeproj/project.pbxproj index 59dead7e..c8ff449a 100644 --- a/Alicerce.xcodeproj/project.pbxproj +++ b/Alicerce.xcodeproj/project.pbxproj @@ -42,7 +42,7 @@ 0A266F0F1ED33B65009CD0D7 /* CAGradientLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A266F0E1ED33B65009CD0D7 /* CAGradientLayer.swift */; }; 0A266F201ED374F5009CD0D7 /* AssertDumpsEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A266F1F1ED374F5009CD0D7 /* AssertDumpsEqual.swift */; }; 0A266F861ED59DC7009CD0D7 /* Alicerce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A3C2D711EA7E3E800EFB7D4 /* Alicerce.framework */; }; - 0A266F8C1ED59FB6009CD0D7 /* MultiTrackerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B57E9871EB15F510027AB30 /* MultiTrackerTestCase.swift */; }; + 0A266F8C1ED59FB6009CD0D7 /* Analytics+MultiTrackerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B57E9871EB15F510027AB30 /* Analytics+MultiTrackerTestCase.swift */; }; 0A266F901ED59FB6009CD0D7 /* Route+ComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C2D0A1EA7E1EE00EFB7D4 /* Route+ComponentTests.swift */; }; 0A266F911ED59FB6009CD0D7 /* Route+TrieNode_AddTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C2D0B1EA7E1EE00EFB7D4 /* Route+TrieNode_AddTests.swift */; }; 0A266F921ED59FB6009CD0D7 /* Route+TrieNode_InitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3C2D0C1EA7E1EE00EFB7D4 /* Route+TrieNode_InitTests.swift */; }; @@ -615,7 +615,7 @@ 1B4D4CB61F05016B00FA4260 /* URLRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequest.swift; sourceTree = ""; }; 1B57E97C1EB150C80027AB30 /* Analytics+MultiTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+MultiTracker.swift"; sourceTree = ""; }; 1B57E97E1EB1510D0027AB30 /* AnalyticsTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTracker.swift; sourceTree = ""; }; - 1B57E9871EB15F510027AB30 /* MultiTrackerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiTrackerTestCase.swift; sourceTree = ""; }; + 1B57E9871EB15F510027AB30 /* Analytics+MultiTrackerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+MultiTrackerTestCase.swift"; sourceTree = ""; }; 1B57E9891EB1606F0027AB30 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 1B667A0920127C1600A8CD5A /* StackOrchestrator+Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StackOrchestrator+Store.swift"; sourceTree = ""; }; 1B667A0B20127C7000A8CD5A /* StackOrchestratorPerformanceMetricsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackOrchestratorPerformanceMetricsTracker.swift; sourceTree = ""; }; @@ -1435,7 +1435,7 @@ 1B57E9861EB15F3C0027AB30 /* Analytics */ = { isa = PBXGroup; children = ( - 1B57E9871EB15F510027AB30 /* MultiTrackerTestCase.swift */, + 1B57E9871EB15F510027AB30 /* Analytics+MultiTrackerTestCase.swift */, 0A708F6C20E99D9A001784DA /* MockAnalyticsTracker.swift */, ); path = Analytics; @@ -1826,7 +1826,7 @@ 4838FE5723A951E6007311F0 /* TopConstrainableProxyTestCase.swift in Sources */, 0A708F6E20E99D9F001784DA /* MockAnalyticsTracker.swift in Sources */, 3E8D61952546F90400C08EA2 /* ConstraintGroupToggleTestCase.swift in Sources */, - 0A266F8C1ED59FB6009CD0D7 /* MultiTrackerTestCase.swift in Sources */, + 0A266F8C1ED59FB6009CD0D7 /* Analytics+MultiTrackerTestCase.swift in Sources */, 0A85F0E720B3177E0095AFFB /* PublicKeyAlgorithmTestCase.swift in Sources */, 0A266F901ED59FB6009CD0D7 /* Route+ComponentTests.swift in Sources */, 0A266F911ED59FB6009CD0D7 /* Route+TrieNode_AddTests.swift in Sources */, diff --git a/Sources/Analytics/Trackers/Analytics+MultiTracker.swift b/Sources/Analytics/Trackers/Analytics+MultiTracker.swift index 8bdfdd48..9ab5cd4c 100644 --- a/Sources/Analytics/Trackers/Analytics+MultiTracker.swift +++ b/Sources/Analytics/Trackers/Analytics+MultiTracker.swift @@ -22,6 +22,15 @@ public extension Analytics { self.trackers = trackers } + /// Creates an analytics multi tracker instance. + /// - Parameter trackers: The result builder that outputs the analytics trackers to register. + public init(@TrackerBuilder trackers: () -> [AnyAnalyticsTracker]) { + + self.trackers = trackers() + + assert(!self.trackers.isEmpty, "🙅‍♂️ Trackers shouldn't be empty, since it renders this tracker useless!") + } + // MARK: - Tracking /// Tracks an analytics event, by propagating it to all the registered sub trackers. @@ -33,3 +42,43 @@ public extension Analytics { } } } + +extension Analytics.MultiTracker { + + @resultBuilder + public struct TrackerBuilder { + + public typealias AnyAnalyticsTracker = Analytics.AnyAnalyticsTracker + + public static func buildExpression(_ tracker: Tracker) -> [AnyAnalyticsTracker] + where Tracker.State == State, Tracker.Action == Action, Tracker.ParameterKey == ParameterKey { + + [tracker.eraseToAnyAnalyticsTracker()] + } + + public static func buildExpression(_ tracker: AnyAnalyticsTracker) -> [AnyAnalyticsTracker] { [tracker] } + + public static func buildExpression(_ trackers: [AnyAnalyticsTracker]) -> [AnyAnalyticsTracker] { trackers } + + public static func buildBlock(_ trackers: [AnyAnalyticsTracker]...) -> [AnyAnalyticsTracker] { + + trackers.flatMap { $0 } + } + + public static func buildOptional(_ tracker: [AnyAnalyticsTracker]?) -> [AnyAnalyticsTracker] { tracker ?? [] } + + public static func buildEither(first tracker: [AnyAnalyticsTracker]) -> [AnyAnalyticsTracker] { tracker } + + public static func buildEither(second tracker: [AnyAnalyticsTracker]) -> [AnyAnalyticsTracker] { tracker } + + public static func buildLimitedAvailability(_ tracker: [AnyAnalyticsTracker]) -> [AnyAnalyticsTracker] { + + tracker + } + + public static func buildArray(_ trackers: [[AnyAnalyticsTracker]]) -> [AnyAnalyticsTracker] { + + trackers.flatMap { $0 } + } + } +} diff --git a/Sources/Logging/Loggers/Log+MultiLogger.swift b/Sources/Logging/Loggers/Log+MultiLogger.swift index d35ea00c..8bafde30 100644 --- a/Sources/Logging/Loggers/Log+MultiLogger.swift +++ b/Sources/Logging/Loggers/Log+MultiLogger.swift @@ -34,6 +34,41 @@ extension Log { /// The logger's log destination error callback closure. private let onError: LogDestinationErrorClosure? + /// Creates a new multi logger instance, with the specified log destinations and modules. + /// + /// - Note: + /// Module filtering works as follows: + /// + /// A log message having a module parameter will only be logged _if the module is registered_ in the logger, and + /// the log message's level is *above* the module's registered minimum log level. On the other hand, if the + /// message is logged without module (i.e. using the `Logger`'s `log` API, i.e. *without* `module` parameter), + /// no module filtering will be made. + /// + /// - Parameters: + /// - modules: The log modules and respective minimum log level to be registered. Used when the + /// `ModuleLogger` APIs are used (i.e. with `module` parameter). + /// - onError: The logger's log destination error callback closure. + /// - destinations: The result builder which outputs log destinations to forward logging events to. + public init( + modules: [Module: Log.Level] = [:], + onError: LogDestinationErrorClosure? = nil, + @DestinationBuilder destinations: () -> [AnyMetadataLogDestination] + ) { + + self.modules = modules + + self.onError = onError ?? { destination, error in + Log.internalLogger.error("💥 LogDestination '\(destination)' failed operation with error: \(error)") + } + + self.destinations = destinations() + + assert( + !self.destinations.isEmpty, + "🙅‍♂️ Destinations shouldn't be empty, since it renders this logger useless!" + ) + } + /// Creates a new multi logger instance, with the specified log destinations and modules. /// /// - Note: @@ -209,3 +244,47 @@ extension Log { } } } + +extension Log.MultiLogger { + + @resultBuilder + public struct DestinationBuilder { + + public typealias AnyLogDestination = AnyMetadataLogDestination + + public static func buildExpression( + _ destination: Destination + ) -> [AnyLogDestination] where Destination.MetadataKey == MetadataKey { + + [destination.eraseToAnyMetadataLogDestination()] + } + + public static func buildExpression(_ destinations: AnyLogDestination) -> [AnyLogDestination] { [destinations] } + + public static func buildExpression(_ destinations: [AnyLogDestination]) -> [AnyLogDestination] { destinations } + + public static func buildBlock(_ destinations: [AnyLogDestination]...) -> [AnyLogDestination] { + + destinations.flatMap { $0 } + } + + public static func buildOptional(_ destinations: [AnyLogDestination]?) -> [AnyLogDestination] { + + destinations ?? [] + } + + public static func buildEither(first destination: [AnyLogDestination]) -> [AnyLogDestination] { destination } + + public static func buildEither(second destination: [AnyLogDestination]) -> [AnyLogDestination] { destination } + + public static func buildLimitedAvailability(_ destination: [AnyLogDestination]) -> [AnyLogDestination] { + + destination + } + + public static func buildArray(_ destinations: [[AnyLogDestination]]) -> [AnyLogDestination] { + + destinations.flatMap { $0 } + } + } +} diff --git a/Sources/PerformanceMetrics/Trackers/PerformanceMetrics+MultiTracker.swift b/Sources/PerformanceMetrics/Trackers/PerformanceMetrics+MultiTracker.swift index b598a3ce..cfc62886 100644 --- a/Sources/PerformanceMetrics/Trackers/PerformanceMetrics+MultiTracker.swift +++ b/Sources/PerformanceMetrics/Trackers/PerformanceMetrics+MultiTracker.swift @@ -19,6 +19,20 @@ public extension PerformanceMetrics { /// The tracker's token dictionary, containing the mapping between internal and sub trackers' tokens. private let tokens = Atomic<[Token : [Token]]>([:]) + /// Creates a new performance metrics multi trcker instance, with the specified sub trackers. + /// + /// - Parameters: + /// -trackers: The result builder to output thje sub trackers to forward performance measuring events to. + public init(@TrackerBuilder trackers: () -> [PerformanceMetricsTracker]) { + + self.trackers = trackers() + + assert( + self.trackers.isEmpty == false, + "🙅‍♂️ Trackers shouldn't be empty, since it renders this tracker useless!" + ) + } + /// Creates a new performance metrics multi trcker instance, with the specified sub trackers. /// /// - Parameters: @@ -144,3 +158,52 @@ public extension PerformanceMetrics { } } + +extension PerformanceMetrics.MultiTracker { + + @resultBuilder + public struct TrackerBuilder { + + public static func buildExpression(_ tracker: PerformanceMetricsTracker) -> [PerformanceMetricsTracker] { + + [tracker] + } + + public static func buildExpression(_ trackers: [PerformanceMetricsTracker]) -> [PerformanceMetricsTracker] { + + trackers + } + + public static func buildBlock(_ trackers: [PerformanceMetricsTracker]...) -> [PerformanceMetricsTracker] { + + trackers.flatMap { $0 } + } + + public static func buildOptional(_ tracker: [PerformanceMetricsTracker]?) -> [PerformanceMetricsTracker] { + + tracker ?? [] + } + + public static func buildEither(first tracker: [PerformanceMetricsTracker]) -> [PerformanceMetricsTracker] { + + tracker + } + + public static func buildEither(second tracker: [PerformanceMetricsTracker]) -> [PerformanceMetricsTracker] { + + tracker + } + + public static func buildLimitedAvailability( + _ tracker: [PerformanceMetricsTracker] + ) -> [PerformanceMetricsTracker] { + + tracker + } + + public static func buildArray(_ trackers: [[PerformanceMetricsTracker]]) -> [PerformanceMetricsTracker] { + + trackers.flatMap { $0 } + } + } +} diff --git a/Tests/AlicerceTests/Analytics/MultiTrackerTestCase.swift b/Tests/AlicerceTests/Analytics/Analytics+MultiTrackerTestCase.swift similarity index 55% rename from Tests/AlicerceTests/Analytics/MultiTrackerTestCase.swift rename to Tests/AlicerceTests/Analytics/Analytics+MultiTrackerTestCase.swift index 77fc9954..14c5d6a6 100644 --- a/Tests/AlicerceTests/Analytics/MultiTrackerTestCase.swift +++ b/Tests/AlicerceTests/Analytics/Analytics+MultiTrackerTestCase.swift @@ -1,7 +1,7 @@ import XCTest @testable import Alicerce -final class MultiTrackerTestCase: XCTestCase { +final class Analytics_MultiTrackerTestCase: XCTestCase { enum MockState { case screen(name: String) @@ -22,6 +22,72 @@ final class MultiTrackerTestCase: XCTestCase { typealias MultiTracker = Analytics.MultiTracker typealias MockSubTracker = MockAnalyticsTracker + // init + + func testInit_WithResultBuilder_ShouldInstantiateCorrectTrackers() { + + let subTracker1 = MockSubTracker() + let subTracker2 = MockSubTracker() + let subTracker3 = MockSubTracker() + let subTracker4 = MockSubTracker() + let subTrackerOpt = MockSubTracker() + let subTrackerTrue = MockSubTracker() + let subTrackerFalse = MockSubTracker() + let subTrackerArray = (1...3).map { _ in MockSubTracker() } + let subTrackerAvailable = MockSubTracker() + + let optVar: Bool? = true + let optNil: Bool? = nil + let trueVar = true + let falseVar = false + + let tracker = MultiTracker { + subTracker1 + subTracker2 + + subTracker3.eraseToAnyAnalyticsTracker() + + [subTracker4].map { $0.eraseToAnyAnalyticsTracker() } + + if let _ = optVar { subTrackerOpt } + if let _ = optNil { subTrackerOpt } + + if trueVar { + subTrackerTrue + } else { + subTrackerFalse + } + + if falseVar { + subTrackerTrue + } else { + subTrackerFalse + } + + for tracker in subTrackerArray { tracker } + + if #available(iOS 1.337, *) { subTrackerAvailable } + } + + XCTAssertDumpsEqual( + tracker.trackers, + ( + [ + subTracker1, + subTracker2, + subTracker3, + subTracker4, + subTrackerOpt, + subTrackerTrue, + subTrackerFalse + ] + + subTrackerArray + + [subTrackerAvailable] + ) + .map { $0.eraseToAnyAnalyticsTracker() } + ) + } + // track func testTrack_WithActionEvent_ShouldCallTrackOnAlltrackers() { diff --git a/Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift b/Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift index 11c449eb..25a9a9e5 100644 --- a/Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift +++ b/Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift @@ -9,7 +9,9 @@ final class MockAnalyticsTracker: AnalyticsTrac var trackInvokedClosure: ((Event) -> Void)? - init() {} + let id: UUID + + init(id: UUID = .init()) { self.id = id } func track(_ event: Event) { trackInvokedClosure?(event) } } diff --git a/Tests/AlicerceTests/Logging/Destinations/MockMetadataLogDestination.swift b/Tests/AlicerceTests/Logging/Destinations/MockMetadataLogDestination.swift index b00fd9a9..8dad0488 100644 --- a/Tests/AlicerceTests/Logging/Destinations/MockMetadataLogDestination.swift +++ b/Tests/AlicerceTests/Logging/Destinations/MockMetadataLogDestination.swift @@ -20,10 +20,13 @@ class MockMetadataLogDestination: Meta var minLevel: Log.Level { mockMinLevel } + let id: UUID + // MARK: - Lifecycle - public init(mockMinLevel: Log.Level = .verbose) { + public init(id: UUID = .init(), mockMinLevel: Log.Level = .verbose) { + self.id = id self.mockMinLevel = mockMinLevel } diff --git a/Tests/AlicerceTests/Logging/Loggers/MultiLoggerTestCase.swift b/Tests/AlicerceTests/Logging/Loggers/MultiLoggerTestCase.swift index d453e443..35ba3a3e 100644 --- a/Tests/AlicerceTests/Logging/Loggers/MultiLoggerTestCase.swift +++ b/Tests/AlicerceTests/Logging/Loggers/MultiLoggerTestCase.swift @@ -10,6 +10,72 @@ class MultiLoggerTestCase: XCTestCase { typealias MockLogDestination = MockMetadataLogDestination typealias MultiLogger = Log.MultiLogger + // init + + func testInit_WithResultBuilder_ShouldInstantiateCorrectDestinations() { + + let destination1 = MockLogDestination() + let destination2 = MockLogDestination() + let destination3 = MockLogDestination() + let destination4 = MockLogDestination() + let destinationOpt = MockLogDestination() + let destinationTrue = MockLogDestination() + let destinationFalse = MockLogDestination() + let destinationArray = (1...3).map { _ in MockLogDestination() } + let destinationAvailable = MockLogDestination() + + let optVar: Bool? = true + let optNil: Bool? = nil + let trueVar = true + let falseVar = false + + let log = MultiLogger { + destination1 + destination2 + + destination3.eraseToAnyMetadataLogDestination() + + [destination4].map { $0.eraseToAnyMetadataLogDestination() } + + if let _ = optVar { destinationOpt } + if let _ = optNil { destinationOpt } + + if trueVar { + destinationTrue + } else { + destinationFalse + } + + if falseVar { + destinationTrue + } else { + destinationFalse + } + + for tracker in destinationArray { tracker } + + if #available(iOS 1.337, *) { destinationAvailable } + } + + XCTAssertDumpsEqual( + log.destinations, + ( + [ + destination1, + destination2, + destination3, + destination4, + destinationOpt, + destinationTrue, + destinationFalse + ] + + destinationArray + + [destinationAvailable] + ) + .map { $0.eraseToAnyMetadataLogDestination() } + ) + } + // log func testLog_WithRegisteredModuleAllowingLogLevel_ShouldCallWriteOnAllDestinationsAllowingLogLevel() { diff --git a/Tests/AlicerceTests/PerformanceMetrics/MockPerformanceMetricsTracker.swift b/Tests/AlicerceTests/PerformanceMetrics/MockPerformanceMetricsTracker.swift index ca7048b7..fbe13533 100644 --- a/Tests/AlicerceTests/PerformanceMetrics/MockPerformanceMetricsTracker.swift +++ b/Tests/AlicerceTests/PerformanceMetrics/MockPerformanceMetricsTracker.swift @@ -11,6 +11,10 @@ class MockPerformanceMetricsTracker: PerformanceMetricsTracker { let tokenizer = Tokenizer() + let id: UUID + + init(id: UUID = .init()) { self.id = id } + func start(with identifier: Identifier) -> Token { startInvoked?(identifier) return tokenizer.next diff --git a/Tests/AlicerceTests/PerformanceMetrics/PerformanceMetrics+MultiTrackerTestCase.swift b/Tests/AlicerceTests/PerformanceMetrics/PerformanceMetrics+MultiTrackerTestCase.swift index 3b9f13eb..f4f586fb 100644 --- a/Tests/AlicerceTests/PerformanceMetrics/PerformanceMetrics+MultiTrackerTestCase.swift +++ b/Tests/AlicerceTests/PerformanceMetrics/PerformanceMetrics+MultiTrackerTestCase.swift @@ -26,6 +26,65 @@ final class PerformanceMetrics_MultiTrackerTestCase: XCTestCase { super.tearDown() } + // init + + func testInit_WithResultBuilder_ShouldInstantiateCorrectTrackers() { + + let subTracker1 = SubTracker() + let subTracker2 = SubTracker() + let subTracker3 = SubTracker() + let subTrackerOpt = SubTracker() + let subTrackerTrue = SubTracker() + let subTrackerFalse = SubTracker() + let subTrackerArray = (1...3).map { _ in SubTracker() } + let subTrackerAvailable = SubTracker() + + let optVar: Bool? = true + let optNil: Bool? = nil + let trueVar = true + let falseVar = false + + let tracker = MultiTracker { + subTracker1 + subTracker2 + + [subTracker3] + + if let _ = optVar { subTrackerOpt } + if let _ = optNil { subTrackerOpt } + + if trueVar { + subTrackerTrue + } else { + subTrackerFalse + } + + if falseVar { + subTrackerTrue + } else { + subTrackerFalse + } + + for tracker in subTrackerArray { tracker } + + if #available(iOS 1.337, *) { subTrackerAvailable } + } + + XCTAssertDumpsEqual( + tracker.trackers, + [ + subTracker1, + subTracker2, + subTracker3, + subTrackerOpt, + subTrackerTrue, + subTrackerFalse + ] + + subTrackerArray + + [subTrackerAvailable] + ) + } + // start func testStart_ShouldInvokeStartOnAllSubTrackers() {