Topics

#294: Annotations with Sourcery 🔮

Topics

We first covered Sourcery back in Bite #292. It's a command-line tool that helps us generate Swift code from .stencil template files.

Today we'll check a specific feature of Sourcery, annotations. Let's dive in.

Nefore we begin, let's do a quick "refresher". The TLDR for Sourcery is Render template files into real Swift code. Full access to the type system using SourceKitten. Useful for simple things (automate tasks like adding counts to enums), or crazy complex things (automatically generate Swift test code). Essentially, metaprogramming. Whew. Ok, moving on.

Annotations are simply small bits of metadata that annotate our properties functions, enums, enum cases and other types.

Then, later we can access this metadata inside our .stencil template files.

Let's try it out.

Before we begin, let's add a new Swift Enum to our project. Nothing special here so far, just regular Swift.

enum SpaceshipKind {
    case cruiser
    case frieghter
    case destroyer
}

Next, we'll add a new SpaceshipKind+Count.stencil file for Sourcery to render. We'll use the "Empty" option in Xcode when creating a new file, then open the Inspector (right side) and change the file's type to Swift Source so it will read (slightly) nicer in Xcode.

Finally, let's run sourcery:

sourcery source/ templates/ /source/_generated/ --watch --verbose

Passing in the --watch tells Sourcery to run continuously and re-render our templates anytime our code or templates change. Neat.

The --verbose flag tells Sourcery to print a bunch of useful debug info to the console while it's working. Helpful at first, but once we're more comfortable with Sourcery, we can probably leave this off.

Whew! Now we can finally start using Annotations.

Let's add an annotation to our enum:

enum SpaceshipKind {
    case cruiser
    case frieghter
    case destroyer
}

Then in our template, let's generate a count property for all our enums:

{% for enum in types.enums %}
extension {{ enum.name }} {
  static var count: Int { return {{ enum.cases.count }} }
}
{% endfor %}

This works great:

extension SpaceshipKind {
  static var count: Int { return 3 }
}

But what if we were farther along in our project? What if we had many more Enums in our code base, and wanted to exempt a few of them from generating a count property.

We'll add another new enum:

enum EngineKind {
    case hyperdrive
    case sunsail
}

Since we added some code, Sourcery's --watch kicks in and re-generates our template:

extension SpaceshipKind {
  static var count: Int { return 3 }
}
extension EngineKind {
  static var count: Int { return 2 }
}

Let's pretend we don't need that second enum to get a count property.

There's a few ways we could approach this in Sourcery, but this gives us a nice way to learn about and understand how annotations work.

We'll add one above EngineKind in our code:

/// sourcery: skipCount
enum EngineKind {
    case hyperdrive
    case sunsail
}

Annotations are simply special comments that Sourcery parses, and makes available in our .stencil templates.

The cool part is it's location aware, so we can put annotations above types, and they'll be available on those types in our .stencil templates! Same goes for annotations on properties, enum cases and more!

Now, we can access this annotation in our template:

{% for enum in types.enums %}
extension {{ enum.name }} {
  {% ifnot enum.annotations.skipCount %}
  static var count: Int { return {{ enum.cases.count }} }
  {% endif %}
}
{% endfor %}

Finally, our generated code is back to just the one enum:

extension SpaceshipKind {
  static var count: Int { return 3 }
}

Success!

We can also apply annotations to multiple things at once using :begin and :end tags:

/// sourcery:begin: skipCustomSetter
var captainID: Int
var maxSpeedInParsecs: Int
/// sourcery:end

This is just the beginning. Come back next time to learn about key/value pair annotations.

Learn more about Sourcery at git.io/sourcery (also in Bite #292)