Sourcery — How to create templates for iOS projects
Let’s get deep and look at the code that can generate others files
Continuing the first part of this article (link here) where I talk about the usage of Sourcery in iOS projects, let's get deep and look at the code that can generate others files, like: mocks, factories, helpers, etc.
Table of Contents
- Create a Static Library just for Sourcery files
- Understand .swifttemplate
- Coding on .meta.swift template file
- Create Core components to reuse code
- Create a .mock() method with default values
1. Create a Static Library just for Sourcery files
First thing first, create a new Static Library just for the Sourcery project. With this you can add the SourceryRuntime
and SourcerySwift
without any impacts in your Main project.
In my example I create a Static Library called SourceryCode
and use SPM to add the Packages.
2. Understand the .swifttemplate
When we create a .swifttemplate file we have a simple syntax to follow (doc)
For our example we will use the folling syntax:
- To import swift files <%- includeFile("relative_path_to_file.swift") %>
- To generate output <%= outputMethod() %>
And two Source Annotations:
- To define the file name that will be generated //sourcery:file:fileName.swift
- To mark the end //sourcery:end
Normaly I create a .swifttemplate and a .meta.swift files with the same name, example ModelsMock.swifttemplate
:
<%- includeFile("ModelsMock.meta.swift") %>
// sourcery:file:ExampleModels+mock.swift
<%= generateModelsMockOutput() %>
// sourcery:end
Here we have:
- Import related swift file ModelsMock.meta.swift
- Export file name ExampleModels.swift
- Method that will write the contents of the ExampleModels.swift
- End //sourcery:end
3. Coding on .meta.swift template file
The .meta.swift
files has the TemplateContext injected, with this we can access some vars to start our job.
3.1. var argument
The first that we can use is argument: [String: NSObject]
with this dictionary we can access the args passed on Sourcery.yml
I discribe this configuration file on first article (here)
Lets see an example of this:
-Sourcery.yml
I pass two args, one from local var and another one hardcoded
-ModelsMock.meta.swift
I just create a method that will return a string to be written on generated file and print the var argument
.
With this, the generated file get a header from sourcery, the print of argument
var and the comment that I leave.
You can think "how this code works even with the error on line 12?"
It's simples, the Xcode showing a error because they don't know about the TemplateContext that I wrote earlier, but we have this at run time.
"Has any way to bypass this?" Of course!
We can create a file just to Xcode understant what we have at run time without beaking the Sourcery build.
// RuntimeHelper.swift
import Foundation
import SourceryRuntime
let argument = [String: NSObject]()
Now it’s easier to code without errors and with Xcode autocomplete 🥳
3.2. var types
The var types: Types
we have access to all the swift files that Sourcery found at sources
path.
And here it's not diferent, we need to add to RuntimeHelper.swift
to remove the erros and turn on the autocomplete.
// RuntimeHelper.swift
import Foundation
import SourceryRuntime
let argument = [String: NSObject]()
let types = Types(types: [])
The var types
provides to us a lot of vars filtering the content that Soucery read from source, example:
- struct types.structs: [Struct]
- class types.classes: [Class]
- protocol types.protocols: [Protocol]
- enum types.enum: [Enum]
and the others types can be found at documentation.
For out example let's get all the structs that have the Codable
protocol
With dump we can see that Sourcery found two structs with Codable
protocol, knowing this, we can go through each struct and create something with its code.
Code to print the name of the struct and each var with your type
import Foundation
import SourceryRuntime
let autoMockableProtocolName = "Codable"
private let models: [Struct] = {
types.structs.filter {
$0.inheritedTypes.contains(autoMockableProtocolName)
}
}()
func generateModelsMockOutput() -> String {
models.forEach { model in
print(model.name)
model.rawVariables.forEach { variable in
print(variable.name, " -- ", variable.typeName.name)
}
print()
}
return ""
}
In our example we can see the both structs that we have with the vars and types:
4. Create Core components to reuse code
Using the option to include files that we see on 2. Understand the .swifttemplate we can create core components to help us.
Let's create a Core.meta.swift
to provide to us a var with the project name, and use this to generated a dynamic file name
// Core.meta.swift
import Foundation
import SourceryRuntime
var projectName: String {
(argument["projectName"] as? String) ?? ""
}
With this we can add the projectName
var to ModelsMock.swifttemplate
and ModelsMock.meta.swift
to generate a SourceryExampleModels+mock.swift
for our example.
Note that I import the core file on .swifttemplate
<%- includeFile("../Core/Core.meta.swift") %>
5. Create a .mock() method with default values
Finally let’s create a .mock() method for our example
// Core.meta.swift
import Foundation
import SourceryRuntime
var projectName: String {
(argument["projectName"] as? String) ?? ""
}
func mockParam(variable: Variable) -> String {
switch variable.typeName.name {
case "String":
return "\"\""
case "Int":
return "0"
case "Bool":
return "false"
default:
return ""
}
}
// ModelsMock.meta.swift
let autoMockableProtocolName = "Codable"
private let models: [Struct] = {
types.structs.filter {
$0.inheritedTypes.contains(autoMockableProtocolName)
}
}()
private func generateMock() -> String {
let stringMocks = models.map {
var varNames = [String]()
let paramsLines = $0.rawVariables.map {
varNames.append($0.name)
return "\($0.name): \($0.typeName.name) = \(mockParam(variable: $0))"
}
return """
// MARK: - Generated \($0.name)
extension \($0.name) {
static func mock(
\(paramsLines.joined(separator: ",\n "))
) -> \($0.name) {
.init(
\(varNames.map { "\($0): \($0)" }.joined(separator: ",\n "))
)
}
}
"""
}
return stringMocks.joined(separator: "\n\n")
}
// MARK: - Output
func generateModelsMockOutput() -> String {
"""
//
// \(projectName)Models+mock.swift
// \(projectName)Tests
//
// Created by Sourcery on xx/xx/xx.
//
import Foundation
@testable import \(projectName)
\(generateMock())
"""
}
With the details that you see here, you can create a file for everything in your project!
Just keep in mind that the generated string will be the content of your generated file.
The contents and examples of this article can be found of my GitHub page on branch part2
: https://github.com/jnevesjunior/sourceryExample/tree/part2