The builder pattern is one of the best examples for getting started with metaprogramming. Sourcery can help us generate the code, and we'll quickly see how it is possible to add a simple builder stencil.
Sourcery is a command-line tool, quite popular in the Swift community, that can help you generate code. I recommend you take some time to read the documentation as well as digging into the tutorials.
First, you will need to install Sourcery on your machine. There are multiple methods, but the best way is to follow the instructions at https://github.com/krzysztofzablocki/Sourcery#installation.
My preferred method would be to install it with Homebrew.
Once you have Sourcery, you can familiarize yourself with it. I'll focus on the particular stencil we'll use to generate our builders. First, we need to add a conformance to our article class. Sourcery works by looking up the types exposed, and it's easier to find the type when you mark it with a particular protocol, in our case Buildable, as follows:
protocol Buildable {}
struct Article: Buildable {
/* ... */
}
Once this is done, you can create your .stencil file, which will hold the template for our builder classes.
Sourcery looks for files with a .stencil extension, and, by default, will create a Swift file with the same name, but a .generated.swift extension. For example, if your file is named Buildable.stencil, the generated source code will be Buildable.generated.swift.
Create your Buildable.stencil file as follows:
- First, loop all the Buildable types. In our case, this only applies to Article now:
{% for type in types.implementing.Buildable %}
- Declare an extension for the current Buildable type, and a nested Builder class:
extension {{ type.name }} {
class Builder {
- Declare all the variables as optionals, based on the parent's variable names and types:
// MARK: private members
{% for member in type.variables %}
private var {{ member.name }}: {{member.typeName}}?
{% endfor %}
- Declare all the setters:
// MARK: - Setters
{% for member in type.variables %}
func set({{ member.name }}: {{member.typeName}}) -> Builder {
self.{{ member.name }} = {{ member.name }}
return self
}
{% endfor %}
- Add the build method that will return a new instance:
// MARK: Builder method
func build() -> {{type.name}} {
return {{type.name}}(
{% for member in type.variables %}
{{ member.name }}: {{ member.name }}!{% if not forloop.last %},{% else %}){% endif %}
{% endfor %}
}
}
}
{% endfor %}
Et voilà—running Sourcery against our article type will generate a valid Builder.generated.swift that we can use in our code.
The generated code doesn't have the default values, but is generic enough to help you implement the builder pattern in a much quicker way than by hand. Once you have the stencil for builders, you can copy/paste the generated code in your programs. This is still much faster than writing it out by hand!