Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add result builders to MultiLogger and MultiTrackers 👷 #265

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Alicerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -615,7 +615,7 @@
1B4D4CB61F05016B00FA4260 /* URLRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequest.swift; sourceTree = "<group>"; };
1B57E97C1EB150C80027AB30 /* Analytics+MultiTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+MultiTracker.swift"; sourceTree = "<group>"; };
1B57E97E1EB1510D0027AB30 /* AnalyticsTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTracker.swift; sourceTree = "<group>"; };
1B57E9871EB15F510027AB30 /* MultiTrackerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiTrackerTestCase.swift; sourceTree = "<group>"; };
1B57E9871EB15F510027AB30 /* Analytics+MultiTrackerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+MultiTrackerTestCase.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
1B667A0B20127C7000A8CD5A /* StackOrchestratorPerformanceMetricsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackOrchestratorPerformanceMetricsTracker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1435,7 +1435,7 @@
1B57E9861EB15F3C0027AB30 /* Analytics */ = {
isa = PBXGroup;
children = (
1B57E9871EB15F510027AB30 /* MultiTrackerTestCase.swift */,
1B57E9871EB15F510027AB30 /* Analytics+MultiTrackerTestCase.swift */,
0A708F6C20E99D9A001784DA /* MockAnalyticsTracker.swift */,
);
path = Analytics;
Expand Down Expand Up @@ -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 */,
Expand Down
49 changes: 49 additions & 0 deletions Sources/Analytics/Trackers/Analytics+MultiTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<State, Action, ParameterKey>]) {

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.
Expand All @@ -33,3 +42,43 @@ public extension Analytics {
}
}
}

extension Analytics.MultiTracker {

@resultBuilder
public struct TrackerBuilder {

public typealias AnyAnalyticsTracker = Analytics.AnyAnalyticsTracker<State, Action, ParameterKey>

public static func buildExpression<Tracker: AnalyticsTracker>(_ 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 }
}
}
}
79 changes: 79 additions & 0 deletions Sources/Logging/Loggers/Log+MultiLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataKey>]
) {

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:
Expand Down Expand Up @@ -209,3 +244,47 @@ extension Log {
}
}
}

extension Log.MultiLogger {

@resultBuilder
public struct DestinationBuilder {

public typealias AnyLogDestination = AnyMetadataLogDestination<MetadataKey>

public static func buildExpression<Destination: MetadataLogDestination>(
_ 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 }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tag> : [Token<Tag>]]>([:])

/// 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:
Expand Down Expand Up @@ -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 }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import XCTest
@testable import Alicerce

final class MultiTrackerTestCase: XCTestCase {
final class Analytics_MultiTrackerTestCase: XCTestCase {

enum MockState {
case screen(name: String)
Expand All @@ -22,6 +22,72 @@ final class MultiTrackerTestCase: XCTestCase {
typealias MultiTracker = Analytics.MultiTracker<MockState, MockAction, MockParameterKey>
typealias MockSubTracker = MockAnalyticsTracker<MockState, MockAction, MockParameterKey>

// 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() {
Expand Down
4 changes: 3 additions & 1 deletion Tests/AlicerceTests/Analytics/MockAnalyticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ final class MockAnalyticsTracker<S, A, PK: AnalyticsParameterKey>: AnalyticsTrac

var trackInvokedClosure: ((Event) -> Void)?

init() {}
let id: UUID

init(id: UUID = .init()) { self.id = id }

func track(_ event: Event) { trackInvokedClosure?(event) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ class MockMetadataLogDestination<Module: LogModule, MetadataKey: Hashable>: 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
}

Expand Down
Loading
Loading