Skip to content

Commit

Permalink
Merge pull request #199 from cashapp/bradfol/parse-dup-validation
Browse files Browse the repository at this point in the history
Detect and validate duplicate registrations within an Assembly
  • Loading branch information
bradfol authored Oct 29, 2024
2 parents 1c0d324 + e3c6df4 commit d069370
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 0 deletions.
69 changes: 69 additions & 0 deletions Sources/KnitCodeGen/ConfigurationSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,72 @@ public extension ConfigurationSet {
"""

}

// MARK: - Validation

/// Validate that there are not duplicate registrations _within_ this ConfigurationSet
/// which represents a single module.
/// Note that this will not find duplicate registrations across modules.
extension ConfigurationSet {

private struct Key: Hashable {
let service: String
let name: String?
let arguments: [String]
}

public func validateNoDuplicateRegistrations() throws {
var registrationSetPerTargetResolver = [String: Set<Key>]()

try assemblies
// Get all registrations across all assemblies
.forEach { assembly in
let targetResolver = assembly.targetResolver

// First make sure there is a Set assigned for this assembly's TargetResolver
if registrationSetPerTargetResolver[targetResolver] == nil {
registrationSetPerTargetResolver[targetResolver] = Set()
}

try assembly.registrations.forEach { registration in
let key = Key(
service: registration.service,
name: registration.name,
arguments: registration.arguments.map { $0.type }
)

guard let registrationSet = registrationSetPerTargetResolver[targetResolver],
registrationSet.contains(key) == false else {
throw ConfigurationSetParsingError.detectedDuplicateRegistration(
service: key.service,
name: key.name,
arguments: key.arguments
)
}

var set = registrationSetPerTargetResolver[targetResolver]!
set.insert(key)
registrationSetPerTargetResolver[targetResolver] = set
}
}
}

}

enum ConfigurationSetParsingError: LocalizedError {

case detectedDuplicateRegistration(service: String, name: String?, arguments: [String])

var errorDescription: String? {
switch self {
case .detectedDuplicateRegistration(let service, let name, let arguments):
return """
Detected a duplicated registration:
Service type: \(service)
Name (optional): \(name ?? "`nil`")
Arguments: \(arguments)
"""
}
}

}
3 changes: 3 additions & 0 deletions Sources/KnitCommand/GenCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ struct GenCommand: ParsableCommand {
externalTestingAssemblies: expandedTestingPaths,
moduleDependencies: dependencyModuleNames
)

try parsedConfig.validateNoDuplicateRegistrations()

if let jsonDataOutputPath {
let data = try JSONEncoder().encode(parsedConfig.allAssemblies)
try data.write(to: URL(fileURLWithPath: jsonDataOutputPath))
Expand Down
177 changes: 177 additions & 0 deletions Tests/KnitCodeGenTests/ConfigurationSetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,94 @@ final class ConfigurationSetTests: XCTestCase {

}

func testValidateDuplicates() {

var configSet = Factory.makeConfigSet(
duplicateService: "DuplicateService",
serviceName: nil,
serviceArguments: []
)

XCTAssertThrowsError(
try configSet.validateNoDuplicateRegistrations(),
"Should throw error for duplicated registration",
{ error in
if case let ConfigurationSetParsingError.detectedDuplicateRegistration(service: service, name: name, arguments: arguments) = error {
XCTAssertEqual(service, "DuplicateService")
XCTAssertNil(name)
XCTAssertEqual(arguments, [])
} else {
XCTFail("Incorrect error")
}
}
)

// Test with a service name
configSet = Factory.makeConfigSet(
duplicateService: "DuplicateNamedService",
serviceName: "aName",
serviceArguments: []
)

XCTAssertThrowsError(
try configSet.validateNoDuplicateRegistrations(),
"Should throw error for duplicated registration",
{ error in
if case let ConfigurationSetParsingError.detectedDuplicateRegistration(service: service, name: name, arguments: arguments) = error {
XCTAssertEqual(service, "DuplicateNamedService")
XCTAssertEqual(name, "aName")
XCTAssertEqual(arguments, [])
} else {
XCTFail("Incorrect error")
}
}
)

// Test with service argument
configSet = Factory.makeConfigSet(
duplicateService: "DuplicateServiceArguments",
serviceName: nil,
serviceArguments: ["Argument"]
)

XCTAssertThrowsError(
try configSet.validateNoDuplicateRegistrations(),
"Should throw error for duplicated registration",
{ error in
if case let ConfigurationSetParsingError.detectedDuplicateRegistration(service: service, name: name, arguments: arguments) = error {
XCTAssertEqual(service, "DuplicateServiceArguments")
XCTAssertNil(name)
XCTAssertEqual(arguments, ["Argument"])
} else {
XCTFail("Incorrect error")
}
}
)

// No duplicates
configSet = ConfigurationSet(
assemblies: [
.init(assemblyName: "TestAssembly", moduleName: "TestModule", registrations: [Registration(service: "Service")], targetResolver: "TestResolver")
],
externalTestingAssemblies: [],
moduleDependencies: []
)
XCTAssertNoThrow(try configSet.validateNoDuplicateRegistrations())
}

func testValidateDuplicates_multipleTargetResolvers() {
// Registrations should only be compared to other registrations for the same TargetResolver
// It is allowed to make the same registration on two different TargetResolvers

let configSet = Factory.makeConfigSetAcrossTwoTargetResolvers(
duplicateService: "DuplicateService",
serviceName: nil,
serviceArguments: []
)

XCTAssertNoThrow(try configSet.validateNoDuplicateRegistrations())
}

}

private enum Factory {
Expand Down Expand Up @@ -297,4 +385,93 @@ private enum Factory {
],
targetResolver: "Resolver"
)

static func makeConfigSet(
duplicateService: String,
serviceName: String?,
serviceArguments: [String]
) -> ConfigurationSet {
let config1 = Configuration(
assemblyName: "Assembly1",
moduleName: "Module1",
registrations: [
Factory.makeRegistration(
duplicateService: duplicateService,
duplicateServiceName: serviceName,
duplicateArguments: serviceArguments
)
],
targetResolver: "TestResolver"
)
let config2 = Configuration(
assemblyName: "Assembly2",
moduleName: "Module2",
registrations: [
Factory.makeRegistration(
duplicateService: duplicateService,
duplicateServiceName: serviceName,
duplicateArguments: serviceArguments
)
],
targetResolver: "TestResolver"
)
return ConfigurationSet(
assemblies: [
config1,
config2,
], externalTestingAssemblies: [],
moduleDependencies: []
)
}

static func makeConfigSetAcrossTwoTargetResolvers(
duplicateService: String,
serviceName: String?,
serviceArguments: [String]
) -> ConfigurationSet {
let config1 = Configuration(
assemblyName: "Assembly1",
moduleName: "Module1",
registrations: [
Factory.makeRegistration(
duplicateService: duplicateService,
duplicateServiceName: serviceName,
duplicateArguments: serviceArguments
)
],
targetResolver: "TestResolver"
)
let config2 = Configuration(
assemblyName: "Assembly2",
moduleName: "Module2",
registrations: [
Factory.makeRegistration(
duplicateService: duplicateService,
duplicateServiceName: serviceName,
duplicateArguments: serviceArguments
)
],
targetResolver: "OtherTestResolver"
)
return ConfigurationSet(
assemblies: [
config1,
config2,
], externalTestingAssemblies: [],
moduleDependencies: []
)
}

static func makeRegistration(
duplicateService: String,
duplicateServiceName: String?,
duplicateArguments: [String]
) -> Registration {
Registration(
service: duplicateService,
name: duplicateServiceName,
arguments: duplicateArguments.map { .init(type: $0) }
)
}

}

0 comments on commit d069370

Please sign in to comment.