Reducing boilerplate code with Sourcery

Profile photo of Mihails Tumkins
Mihails Tumkins
Oct 09, 2017
3 min
Categories: Development
Screen with color code
Screen with color code

We all write boilerplate code. Be it Equatable, Hashable or some data mapping, we all need to write boring repetitive code sometimes.

Recently I’ve encountered a tool which is great at doing this task in our place. The name is Sourcery a library created by Krzysztof Zabłocki.

Sourcery scans your source code, applies your personal templates and generates Swift code for you, allowing you to use meta-programming techniques to save time and decrease potential mistakes.

So what can it do for us?

swift-gif.gif

I’ll show you how we can reduce our code when creating data objects.

Let’s imagine that in your application you need to get some information about some post from the server via JSON format.

So let’s say that server response looks something like this:

{ "id": 1, "title": "Reducing boilerplate code with Sourcery.", "content": "We all write boilerplate code. Be it Equatible, Hashable or some data mapping, we need to write..." }

For tasks like that we usually need to create data object, something like this:

struct PostDO { var id: Int? var title: String? var content: String? }

Now on if you are using Alamofire, it’s quite easy to make a request to a server and if you don’t want to do all the json parsing by hand you’ll probably end up using something like ObjectMapper.

Now to confirm to Mappable protocol provided by ObjectMapper you need to write extension for PostDO object which looks something like:

import ObjectMapper extension PostDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { id <- map["id"] title <- map["title"] content <- map["content"] } }

Now that doesn’t look to bad, but as your app grows you find yourself in process of continuously implementing mapping function for different data objects and that’s totally not fun.

Let’s automate this task using Sourcery.

Sourcery generates code by using your templates written in Stencil. So for our task, the template would look something like:

import ObjectMapper {% for type in types.all|!protocol|annotated:"mappable" %} // MARK: - Mappable ({{ type.name }}) extension {{ type.localName }}: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { {% for var in type.storedVariables|!annotated:"ignore" %} self.{{ var.name }} <- map["{{ var.name }}"] {% endfor %} } } {% endfor %}

Now if we’ll modify our PostDO code to look like this:

//sourcery: mappable struct PostDO { var id: Int? var title: String? var content: String? }

and run Sourcery, the code in Mappable.generated.swift file would look like:

import ObjectMapper // MARK: - Mappable (PostDO) extension PostDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { self.id <- map["id"] self.title <- map["title"] self.content <- map["content"] } }

Yay! Sourcery generated for us implementation of Mappable protocol.

Now let’s say we need some user information from server

{ "id": 1, "first_name": "John", "last_name": "Smith", "email": "john@gmail.com", }

So we’ll create UserDO struct and mark it as mappable:

//sourcery: mappable struct UserDO { var id: Int? var firstName: String? var lastName: String? var email: String? }

If we will run Sourcery now, we will and check in generated file we will see something like:

import ObjectMapper // MARK: - Mappable (PostDO) extension PostDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { self.id <- map["id"] self.title <- map["title"] self.content <- map["content"] } } // MARK: - Mappable (UserDO) extension UserDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { self.id <- map["id"] self.firstName <- map["firstName"] self.lastName <- map["lastName"] self.email <- map["email"] } }

Almost what we want, but we need to get firstName and lastName right and Sourcery provides tool for that — it’s called key annotations. Let’s modify our template to look like this:

import ObjectMapper {% for type in types.all|!protocol|annotated:"mappable" %} // MARK: - Mappable ({{ type.name }}) extension {{ type.localName }}: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { {% for var in type.storedVariables|!annotated:"ignore" %} {% if var.annotations.jsonKey %} self.{{ var.name }} <- map["{{ var.annotations.jsonKey }}"] {% else %} self.{{ var.name }} <- map["{{ var.name }}"] {% endif %} {% endfor %} } } {% endfor %}

We are checking here if property is marked with jsonKey, and using its value instead of property name. So we will fix our UserDO to look like:

//sourcery: mappable struct UserDO { //sourcery: jsonKey = "first_name" var firstName: String? //sourcery: jsonKey = "last_name" var lastName: String? var email: String? }

run Sourcery again and generated file will look like:

import ObjectMapper // MARK: - Mappable (PostDO) extension PostDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { self.id <- map["id"] self.title <- map["title"] self.content <- map["content"] } } // MARK: - Mappable (UserDO) extension UserDO: Mappable { init?(map: Map) {} mutating func mapping(map: Map) { self.id <- map["id"] self.firstName <- map["first_name"] self.lastName <- map["last_name"] self.email <- map["email"] } }

Yuppi! Now everything seems to be in order.

As you can see Sourcery considerably reduced amount of boilerplate needed for ours data objects creation, as well it has some powerful features.

I would strongly encourage you to look at Sourcery and it’s documentation as can do much more than described in this article.

Also if you do Vapor development i would recommend to look at sourcery templates for it.

Oh, an by the way Krzysztof Zabłocki will be speaking about new stuff in Sourcery at DevFest Baltics 2017 in Riga, next month, so if you have any questions for him, you could meet him there.

See you guys!

Share with friends

Let’s build products together!

Contact us