Skip to content

Commit

Permalink
Added mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
Szaq committed Nov 29, 2023
1 parent 07401c7 commit 5663f3f
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 32 deletions.
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ If you're willing to help then by all means chime in! We are open for PRs.
This

```swift
class Context {
let networkProvider: NetworkProvider
let authProvider: AuthProvider
let localStorage: LocalStorage
}
let context = Context(...)

let VC = MessagesViewController(networkProvider: context.networkProvider, authProvider: context.authProvider, localStorage: context.localStorage)
//----------------------------------------------------------------
class MessagesViewController: UIViewController {
private let networkProvider: NetworkProvider
private let authProvider: AuthProvider
Expand All @@ -44,36 +53,49 @@ class MessagesViewModel {
self.networkProvider = networkProvider
self.authProvider = authProvider
self.localStorage = localStorage
self.authProvider.checkifLoggedIn()
}

func checkIfLoggedIn() -> Bool {
self.authProvider.checkifLoggedIn()
}
}
```

becomes

```swift
protocol MessagesViewControllerInjector: Injector {

class Context: RootInjector {
let networkProvider: NetworkProvider
let authProvider: AuthProvider
let localStorage: LocalStorage
}
let context = Context(...)

let VC: MessagesViewController = context.inject()
//------------------------------------------------------
protocol MessagesViewControllerInjector {
}

class MessagesViewController: UIViewController, Injectable, InjectsMessagesViewModelInjector {
let injector: MessagesViewControllerInjectorImpl
@Needs<MessagesViewControllerInjector>
@Injects<MessagesViewModelInjector>
class MessagesViewController: UIViewController {
init(injector: MessagesViewControllerInjectorImpl) {
self.injector = injector
self.viewModel = MessagesViewModel(inject())
self.viewModel = inject()
}
}
// ------------------------------------------------------------------
protocol MessagesViewModelInjector: Injector {
// ------------------------------------------------------
protocol MessagesViewModelInjector {
var networkProvider: NetworkProvider {get}
var authProvider: AuthProvider {get}
var localStorage: LocalStorage {get}
}

class MessagesViewModel: Injectable {
let injector: MessagesViewModelInjectorImpl
init(injector: MessagesViewModelInjectorImpl) {
self.injector = injector
self.authProvider.checkifLoggedIn()
@Needs<MessagesViewModelInjector>
class MessagesViewModel {
func checkIfLoggedIn() -> Bool {
self.authProvider.checkifLoggedIn()
}
}
```
Expand Down
69 changes: 57 additions & 12 deletions Templates/Inject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public struct InjectData {
let injectorsToInjectables: [String: [String]]
let injectablesToInjectors: [String: String]
let injectablesToInjects: [String: [String]]
let injectablesToProtocols: [String: [String]]
// Each initializer is list of pairs representing each argument (label, type)
let injectablesToInitializers: [String: [[(String?, String)]]]
let protocolsToInjectables: [String: [String]]
let injectorProperties: [String: [Property]]
}

Expand Down Expand Up @@ -231,22 +235,35 @@ func extractNeededName(_ type: Type) -> String? {
return injector
}

func extractInjects(_ type: Type, _ injectablesToInjectors: [String: String]) -> [String] {
func extractInjects(_ type: Type, _ injectablesToInjectors: [String: String], _ protocolsToInjectables: [String: [String]]) -> [String] {
guard
let attribute = type.attributes["Injects"]?.first
else { return [] }

let name = String(describing: attribute)

return String(name[name.index(after: name.firstIndex(of: "<")!)...name.index(name.endIndex, offsetBy: -2)]).components(separatedBy:",").compactMap{
let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines)
if let injectedInjector = injectablesToInjectors[trimmed] {
return "Injects\(injectedInjector)"
} else {
Log.error("Failed to find Injector for Injected class \($0) while checking \(type.name).\n")
return nil
}
}
return String(name[name.index(after: name.firstIndex(of: "<")!)...name.index(name.endIndex, offsetBy: -2)])
.components(separatedBy:",")
.flatMap{
let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines)
if let injectedInjector = injectablesToInjectors[trimmed] {
return ["Injects\(injectedInjector)"]
} else if let injectedInjectors = protocolsToInjectables[trimmed]?.compactMap({ injectablesToInjectors[$0] })
.map({"Injects\($0)"}) {
return injectedInjectors
} else {
Log.error("Failed to find Injector for Injected class \($0) while checking \(type.name).\n")
return []
}
}
}

public func protocolName(_ injectable: String, data: InjectData) -> String {
var protocolName = injectable.replacingOccurrences(of: "Impl", with: "")
if injectData.protocolsToInjectables[protocolName] == nil {
protocolName = injectable
}
return protocolName
}

public func calculateInjectData() -> InjectData {
Expand All @@ -259,7 +276,10 @@ public func calculateInjectData() -> InjectData {
var injectorsToInjectables: [String: [String]] = [:]
var injectablesToInjectors: [String: String] = [:]
var injectablesToInjects: [String: [String]] = [:]
var injectablesToInitializers: [String: [[(String?, String)]]] = [:]
var injectorProperties: [String: [Property]] = [:]
var injectablesToProtocols: [String: [String]] = [:]
var protocolsToInjectables: [String: [String]] = [:]

//Find root injector properties
guard let rootInjector = types.all.first(where: {$0.inheritedTypes.contains("RootInjector")}) else { fatalError("RootInjector not found.") }
Expand All @@ -277,21 +297,43 @@ public func calculateInjectData() -> InjectData {
// Build injection relation by
// finding which Injectors are used to init which Injectables and which Injectables inject any other Injectors
injectables.forEach({injectable in

var extraInitializers: [[(String?, String)]] = []

if let injectorForInjectable = injectable.storedVariables.first(where: {$0.name == "injector"})?.typeName {
let injectableName = String("\(injectorForInjectable)".dropLast(4))
injectablesToInjectors[injectable.name] = injectableName
injectorsToInjectables[injectableName] = (injectorsToInjectables[injectableName] ?? []) + [injectable.name]
//Macro generates initializer if needed
//This will be removed when we implement injection for all initializers
extraInitializers.append([("injector", injectorForInjectable.name)])
} else if let injectorForInjectable = extractNeededName(injectable) {
injectablesToInjectors[injectable.name] = injectorForInjectable
injectorsToInjectables[injectorForInjectable] = (injectorsToInjectables[injectorForInjectable] ?? []) + [injectable.name]
//Macro generates initializer if needed
extraInitializers.append([("injector", injectorForInjectable)])
}

injectablesToInjects[injectable.name] = injectable.inheritedTypes.filter({$0.hasPrefix("Injects")})

let validInitializers = injectable.initializers
.filter({ initializer in initializer.parameters.first(where: { $0.name == "injector"}) != nil })
.map { initializer in initializer.parameters.map { ($0.argumentLabel, $0.typeName.name) }}

injectablesToInitializers[injectable.name] = /* validInitializers +*/ extraInitializers
})

//Add dependencies from @Injects macro

injectables.forEach({injectable in
injectablesToInjects[injectable.name] = (injectablesToInjects[injectable.name] ?? []) + extractInjects(injectable, injectablesToInjectors)
//Add dependencies from @Injects macro
injectablesToInjects[injectable.name] = (injectablesToInjects[injectable.name] ?? []) + extractInjects(injectable, injectablesToInjectors, protocolsToInjectables)

//Add mapping between injectables and protocols
injectablesToProtocols[injectable.name] = injectable.inheritedTypes
injectable.inheritedTypes.forEach {protocolName in
protocolsToInjectables[protocolName] = (protocolsToInjectables[protocolName] ?? []) + [injectable.name]
}

})

//Find properties required by all injectors (including ones required by their children and found in root injector)
Expand All @@ -306,6 +348,9 @@ public func calculateInjectData() -> InjectData {
injectorsToInjectables: injectorsToInjectables,
injectablesToInjectors: injectablesToInjectors,
injectablesToInjects: injectablesToInjects,
injectablesToProtocols: injectablesToProtocols,
injectablesToInitializers: injectablesToInitializers,
protocolsToInjectables: protocolsToInjectables,
injectorProperties: injectorProperties)
}

87 changes: 79 additions & 8 deletions Templates/Inject.swifttemplate
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,26 @@
imports = []
}

func outputInjectFunction(injectorType :String, parentType: String, injectorProperties: [String: [Property]], useInjectorString: Bool, injectDirectly: Bool, injectableName: String) {

let testImports: [String]!
if let _imports = (argument["testImports"] as? [String]) {
testImports = _imports
} else if let _import = argument["testImports"] as? String {
testImports = [_import]
} else {
testImports = []
}

func outputInjectFunction(injectorType :String,
parentType: String,
injectorProperties: [String: [Property]],
useInjectorString: Bool,
initializerParams: [(String?, String)],
injectDirectly: Bool,
injectableName: String,
protocolName: String,
addMock: Bool,
isHelper: Bool = false) {
let targetProperties = sorted(injectorProperties[injectorType] ?? [])
let sourceProperties = injectorProperties[parentType] ?? []
var injectorMappings: [String: String] = [:]
Expand All @@ -36,15 +55,16 @@ func outputInjectFunction(injectorType :String, parentType: String, injectorProp
//Inject dependencies directly
if injectDirectly {
%>
func inject(<%= arguments.map({"\($0.name): \($0.type)"}).joined(separator: ", ") %>) -> <%= injectableName %> {
return <%= injectableName %>(injector: <%= injectorType %>Impl(
func inject<% if isHelper { %>Impl<% } %>(<%= arguments.map({"\($0.name): \($0.type)"}).joined(separator: ", ") %>) -> <% if isHelper { %><%= injectableName %><% } else { %><%= protocolName %><% } %> {

return <% if addMock && !isHelper {%><% if useInjectorString {%> injector.<% }%>mock?.mock<%= protocolName %> ??<% } %> <%= injectableName %>(injector: <%= injectorType %>Impl(<% if addMock {%>mock: <% if useInjectorString {%> injector.<% }%>mock<% if targetProperties.count > 0 { %>,<%}%><% } %>
<%= targetProperties.map({target in let mapping = injectorMappings[target.name].map({source in useInjectorString ? "injector.\(source)" : source}); return "\(target.name): \(mapping ?? target.name)"}).joined(separator: ",\n ") %>
))
}
<%
} else {
%>
func inject(<%= arguments.map({"\($0.name): \($0.type)"}).joined(separator: ", ") %>) -> <%= injectorType %>Impl {
func inject(<%= arguments.map({"\($0.name): \($0.type)"}).joined(separator: ", ") %>) -> <%= injectorType %> {
return <%= injectorType %>Impl(
<%= targetProperties.map({target in let mapping = injectorMappings[target.name].map({source in useInjectorString ? "injector.\(source)" : source}); return "\(target.name): \(mapping ?? target.name)"}).joined(separator: ",\n ") %>
)
Expand All @@ -58,8 +78,24 @@ func outputInjectFunction(injectorType :String, parentType: String, injectorProp
/////////////////////////////////////////////////////////MAIN ROUTINE/////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let injectDirectly = argument["legacyInjection"] == nil
let addMocks = argument["noMocks"] == nil
let injectData = calculateInjectData()
-%>
<% if addMocks { %>
// sourcery:file:InjectGrail/Mock.swift
import Foundation
<%_ for `import` in imports { -%>
import <%= `import` %>
<%_ } -%>

class InjectGrailMock {
<% for type in injectData.injectables { let type = protocolName(type.name, data: injectData) -%>
var mock<%= type %>: <%= type %>? = nil
<% } %>
init() {}
}
// sourcery:end
<% } %>
// sourcery:file:InjectGrail/RootInjector.swift
import Foundation
<%_ for `import` in imports { -%>
Expand All @@ -71,14 +107,46 @@ extension <%= injectData.rootInjector.name %> {
<%_ for type in injectData.injectors{
if injectDirectly {
injectData.injectorsToInjectables[type.name]?.forEach {injectableName in
outputInjectFunction(injectorType: type.name, parentType: "RootInjector", injectorProperties: injectData.injectorProperties, useInjectorString: false, injectDirectly: true, injectableName: injectableName)
for initializerParams in injectData.injectablesToInitializers[injectableName] ?? [] {
outputInjectFunction(injectorType: type.name,
parentType: "RootInjector",
injectorProperties: injectData.injectorProperties,
useInjectorString: false,
initializerParams: initializerParams,
injectDirectly: true,
injectableName: injectableName,
protocolName: protocolName(injectableName, data: injectData),
addMock: addMocks)
}
}
} else {
outputInjectFunction(injectorType: type.name, parentType: "RootInjector", injectorProperties: injectData.injectorProperties, useInjectorString: false, injectDirectly: false, injectableName: type.name)
outputInjectFunction(injectorType: type.name, parentType: "RootInjector", injectorProperties: injectData.injectorProperties, useInjectorString: false, initializerParams: [], injectDirectly: false, injectableName: type.name, protocolName: "", addMock: addMocks)
}
} %>
}
// sourcery:end
<% if injectDirectly {%>
// sourcery:file:InjectGrail/TestHelpers.swift
import Foundation
<%_ for `import` in imports { -%>
import <%= `import` %>
<%_ } -%>
<%_ for `import` in testImports { -%>
<%= `import` %>
<%_ } -%>

//Extension of RootInject which contains helper injectors.
extension <%= injectData.rootInjector.name %> {
<%_ for type in injectData.injectors{
injectData.injectorsToInjectables[type.name]?.forEach {injectableName in
for initializerParams in injectData.injectablesToInitializers[injectableName] ?? [] {
outputInjectFunction(injectorType: type.name, parentType: "RootInjector", injectorProperties: injectData.injectorProperties, useInjectorString: false, initializerParams: initializerParams, injectDirectly: true, injectableName: injectableName, protocolName: protocolName(injectableName, data:injectData), addMock: addMocks, isHelper: true)
}
}
} %>
}
// sourcery:end
<% } %>
// sourcery:file:InjectGrail/Injectors.swift

<%_
Expand All @@ -89,6 +157,7 @@ for type in injectData.injectors{ -%>
//MARK: - <%= type.name %>
//Actual implementation of <%= type.name %> which includes all dependencies found in RootInjector and required by this injector's children.
struct <%= type.name %>Impl: <%= type.name %> {
<% if addMocks && injectData.injectorProperties[type.name]?.allSatisfy({$0.name != "mock"}) ?? false { %> let mock: InjectGrailMock? <%}%>
<%_ for property in sorted(injectData.injectorProperties[type.name] ?? []) { %>
let <%= property.name%>: <%= property.type -%>
<% } %>
Expand Down Expand Up @@ -123,10 +192,12 @@ extension <%= type.name %> {
guard let childInjector = injectData.injectsToInjectors[injects] else { fatalError("Child injector not found for \(injects)") }
if injectDirectly {
injectData.injectorsToInjectables[childInjector]?.forEach {injectableName in
outputInjectFunction(injectorType: childInjector, parentType: injector, injectorProperties: injectData.injectorProperties, useInjectorString: true, injectDirectly: true, injectableName: injectableName)
for initializerParams in injectData.injectablesToInitializers[injectableName] ?? [] {
outputInjectFunction(injectorType: childInjector, parentType: injector, injectorProperties: injectData.injectorProperties, useInjectorString: true, initializerParams: initializerParams, injectDirectly: true, injectableName: injectableName, protocolName: protocolName(injectableName, data: injectData), addMock: addMocks)
}
}
} else {
outputInjectFunction(injectorType: childInjector, parentType: injector, injectorProperties: injectData.injectorProperties, useInjectorString: true, injectDirectly: false, injectableName: childInjector)
outputInjectFunction(injectorType: childInjector, parentType: injector, injectorProperties: injectData.injectorProperties, useInjectorString: true, initializerParams: [], injectDirectly: false, injectableName: childInjector, protocolName: childInjector, addMock: addMocks)
}
%>

Expand Down

0 comments on commit 5663f3f

Please sign in to comment.