Skip to content

Commit

Permalink
Add an injected assemblyValidation closure for ModuleAssembler
Browse files Browse the repository at this point in the history
This will allow ScopedModuleAssembler to inject its validation rather than performing it at the very end.
  • Loading branch information
bradfol committed Oct 25, 2023
1 parent 0ab0687 commit b0649f4
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 3 deletions.
18 changes: 18 additions & 0 deletions Sources/Knit/Module/DependencyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import Foundation
/// Class for building a list of module dependencies based on the dependency tree
final class DependencyBuilder {

private let assemblyValidation: ((any ModuleAssembly.Type) throws -> Void)?
private var inputModules: [any ModuleAssembly] = []
var assemblies: [any ModuleAssembly] = []
let isRegisteredInParent: (any ModuleAssembly.Type) -> Bool
private let defaultOverrides: DefaultOverrideState
private var moduleSources: [String: any ModuleAssembly.Type] = [:]

init(modules: [any ModuleAssembly],
assemblyValidation: ((any ModuleAssembly.Type) throws -> Void)? = nil,
defaultOverrides: DefaultOverrideState = .whenTesting,
isRegisteredInParent: ((any ModuleAssembly.Type) -> Bool)? = nil
) throws {
self.assemblyValidation = assemblyValidation
self.defaultOverrides = defaultOverrides

inputModules = modules
Expand Down Expand Up @@ -77,6 +80,18 @@ final class DependencyBuilder {
result: inout [any ModuleAssembly.Type],
source: (any ModuleAssembly.Type)?
) throws {

// Assembly validation should be performed "up front"
// For example if we are validating the assemblies' `TargetResolver`, we should not walk the tree
// if the root assembly is targeting an incorrect resolver.
if let assemblyValidation {
do {
try assemblyValidation(from)
} catch {
throw DependencyBuilder.Error.assemblyValidationFailure(from, reason: error)
}
}

moduleSources[String(describing: from)] = source
let resolved = try resolvedType(from)
guard !result.contains(where: {$0 == resolved}) else {
Expand Down Expand Up @@ -149,6 +164,7 @@ extension DependencyBuilder {
enum Error: LocalizedError {
case moduleNotProvided(_ moduleType: any ModuleAssembly.Type, _ sourcePath: String)
case invalidDefault(_ overrideType: any ModuleAssembly.Type, _ moduleType: any ModuleAssembly.Type)
case assemblyValidationFailure(_ moduleType: any ModuleAssembly.Type, reason: Swift.Error)

var errorDescription: String? {
switch self {
Expand All @@ -170,6 +186,8 @@ extension DependencyBuilder {
}
"""
return "\(overrideType) used as default override does not implement \(moduleType)\n\(suggestion)"
case let .assemblyValidationFailure(moduleType, reason):
return "\(moduleType) did not pass assembly validation check: \(reason.localizedDescription)"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Knit/Module/ModuleAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public final class ModuleAssembler {
parent: ModuleAssembler? = nil,
_ modules: [any Assembly],
defaultOverrides: DefaultOverrideState = .whenTesting,
assemblyValidation: ((any ModuleAssembly.Type) throws -> Void)? = nil,
postAssemble: ((Container) -> Void)? = nil,
file: StaticString = #file,
line: UInt = #line
Expand All @@ -44,6 +45,7 @@ public final class ModuleAssembler {
do {
self.builder = try DependencyBuilder(
modules: moduleAssemblies,
assemblyValidation: assemblyValidation,
defaultOverrides: defaultOverrides
) { type in
return parent?.isRegistered(type) ?? false
Expand Down
60 changes: 57 additions & 3 deletions Tests/KnitTests/DependencyBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ final class DependencyBuilderTests: XCTestCase {
}

func test_parentRegistered() throws {
let builder = try DependencyBuilder(modules: [Assembly1()]) { type in
return type == Assembly2.self
}
let builder = try DependencyBuilder(
modules: [Assembly1()],
isRegisteredInParent: { type in
return type == Assembly2.self
}
)
// Assembly2 is not registered because the builder was told that it had been done by the parent
XCTAssertEqual(builder.assemblies.count, 1)
XCTAssertTrue(builder.assemblies[0] is Assembly1)
Expand Down Expand Up @@ -79,6 +82,47 @@ final class DependencyBuilderTests: XCTestCase {
}
}

func test_validation() throws {
XCTAssertThrowsError(
try DependencyBuilder(
modules: [Assembly3()],
assemblyValidation: { assembly in
guard assembly != Assembly1.self else {
throw DependencyBuilderTestError.failedValidation
}
}
),
"Should throw validation error",
{ error in
XCTAssertEqual(
error.localizedDescription,
"Assembly1 did not pass assembly validation check: Test case validation failure."
)

if case let DependencyBuilder.Error.assemblyValidationFailure(moduleType, reason: reasonError) = error {
XCTAssert(moduleType == Assembly1.self)
if case DependencyBuilderTestError.failedValidation = reasonError {
// Correct `reasonError`
} else {
XCTFail("The `reasonError` in the `assemblyValidationFailure` was incorrect")
}
} else {
XCTFail("The error thrown by `DependencyBuilder.init` was incorrect")
}
}
)

// Should pass validation
XCTAssertNotNil(try DependencyBuilder(
modules: [Assembly1()],
assemblyValidation: { assembly in
guard assembly != Assembly3.self else {
throw DependencyBuilderTestError.failedValidation
}
}
))
}

}

// Assembly1 depends on Assembly2
Expand Down Expand Up @@ -137,3 +181,13 @@ private struct Assembly7: ModuleAssembly {
func assemble(container: Container) {}
static var dependencies: [any ModuleAssembly.Type] { [Assembly5.self] }
}

private enum DependencyBuilderTestError: LocalizedError {
case failedValidation

var errorDescription: String? {
switch self {
case .failedValidation: return "Test case validation failure."
}
}
}

0 comments on commit b0649f4

Please sign in to comment.