Skip to content

A tool that brings meta-programming to Swift, allowing you to code generate Swift code.

License

Notifications You must be signed in to change notification settings

area51/Sourcery

 
 

Repository files navigation

CircleCI codecov Version License Platform

What is Sourcery?

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.

Using it offers many benefits:

  • Write less repetitive code and make it easy to adhere to DRY principle.
  • It allows you to create better code, one that would be hard to maintain without it, e.g. performing automatic property level difference in tests
  • Limits the risk of introducing human error when refactoring.
  • Sourcery doesn't use runtime tricks, in fact, it allows you to leverage compiler, even more, creating more safety.
  • Immediate feedback: Sourcery features built-in daemon support, enabling you to write your templates in real-time side-by-side with generated code.

Daemon demo

Sourcery is so meta that it is used to code-generate its boilerplate code

Table of Contents generated with DocToc

Why?

Swift features very limited runtime and no meta-programming features. Which leads our projects to contain boilerplate code.

Sourcery exists to allow Swift developers to stop doing the same thing over and over again while still maintaining strong typing, preventing bugs and leveraging compiler.

Have you ever?

  • Had to write equatable/hashable?
  • Had to write NSCoding support?
  • Had to implement JSON serialization?
  • Wanted to use Lenses?

If you did then you probably found yourself writing repetitive code to deal with those scenarios, does this feel right?

Even worse, if you ever add a new property to a type all of those implementations have to be updated, or you will end up with bugs. In those scenarios usually compiler will not generate the error for you, which leads to error prone code.

Examples

Use case: I want to know how many elements are in each enum

Template:

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

Result:

extension AdType {
  static var count: Int { return 2 }
}

...

Use case: I want to generate Equality for types implementing AutoEquatable.

Template:

{% for type in types.implementing.AutoEquatable %}
extension {{ type.name }}: Equatable {}

func == (lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool {
    {% for variable in type.storedVariables %} if lhs.{{ variable.name }} != rhs.{{ variable.name }} { return false }
    {% endfor %}
    return true
}
{% endfor %}

Result:

extension AccountSectionConfiguration: Equatable {}

func == (lhs: AccountSectionConfiguration, rhs: AccountSectionConfiguration) -> Bool {
     if lhs.status != rhs.status { return false }
     if lhs.user != rhs.user { return false }
     if lhs.entitlements != rhs.entitlements { return false }

    return true
}
...
Use case: I want to create lenses for all structs.

Full implementation

Template:

{% for type in types.structs %}
extension {{ type.name }} {
{% for variable in type.variables %}
  static let {{ variable.name }}Lens = Lens<{{type.name}}, {{variable.type}}>(
    get: { $0.{{variable.name}} },
    set: { {{variable.name}}, {{type.name | lowercase}} in
       {{type.name}}({% for argument in type.variables %}{{argument.name}}: {% if variable.name == argument.name %}{{variable.name}}{% else %}{{type.name || lowercase}}.{{argument.name}}{% endif %}{% if not forloop.last%}, {% endif %}{% endfor %})
    }
  ){% endfor %}
}
{% endfor %}

Result:

extension House {

  static let addressLens = Lens<House, String>(
    get: { $0.address },
    set: { address, house in
       House(rooms: house.rooms, address: address, size: house.size)
    }
  )

  ...
}

Writing templates

Sourcery templates are powered by Stencil

Make sure you leverage Sourcery built-in daemon to make writing templates a pleasure: you can open template side-by-side with generated code and see it change live.

There are multiple ways to access your types:

  • type.TypeName => access specific type by name
  • types.all => all types, excluding protocols
  • types.classes
  • types.structs
  • types.enums
  • types.protocols => lists all protocols (that were defined in the project)
  • types.inheriting.BaseClass => lists all types inherting from known BaseClass (only those that were defined in source code that Sourcery scanned)
  • types.implementing.Protocol => lists all types conforming to given Protocol (only those that were defined in source code that Sourcery scanned)
  • types.based.BaseClassOrProtocol => lists all types implementing or inheriting from BaseClassOrProtocol (all type names encountered, even those that Sourcery didn't scan)

For each type you can access following properties:

  • name <- name
  • kind <- convience accessor that will contain one of enum, class, struct, protocol, it will also provide extension for types that are unknown to us(e.g. 3rd party or objc), but had extension in the project
  • isGeneric <- info whether the type is generic
  • localName <- name within parent scope
  • variables <- list of all variables defined in this type, excluding variables from protocols or inheritance
    • if you want to access all available variables, including those from inherited / protocol, then use allVariables
    • if you want to accces computed, stored, instance, or static variables, you can do so using our custom filters on both variables and allVariables
  • methods <- list of all methods defined in this type, excluding those from protocols or inheritance
  • allMethods <- same principles as in allVariables
  • initializers <- list of all initializers
  • inherits.BaseClass => info whether type inherits from known base class
  • implements.Protocol => info whether type implements known protocol
  • based.BaseClassOrProtocol => info whether type implements or inherits from BaseClassOrProtocol (all type names encountered, even those that Sourcery didn't scan)
  • containedTypes <- list of types contained within this type
  • parentName <- list of parent type (for contained ones)
  • annotations <- dictionary with configured annotations

Enum types builts on top of regular types and adds:

  • rawType <- enum raw type
  • cases <- list of Enum.Case
  • hasAssociatedValues <- true if any of cases has associated values

Enum.Case provides:

  • name <- name
  • rawValue <- raw value
  • associatedValues <- list of AssociatedValue
  • annotations <- dictionary with configured annotations

Enum.Case.AssociatedValue provides:

  • name <- name
  • typeName <- name of type of associated value
  • unwrappedTypeName <- returns name of the type, unwrapping the optional e.g. for variable with type Int? this would return Int
  • isOptional <- whether is optional

Variable provides:

  • name <- Name
  • type <- type of the variable, if known
  • typeName <- returns name of the type, including things like optional markup
  • unwrappedTypeName <- returns name of the type, unwrapping the optional e.g. for variable with type Int? this would return Int
  • isOptional <- whether is optional
  • isComputed <- whether is computed
  • isStatic <- whether is static variable
  • readAccess <- what is the protection access for reading?
  • writeAccess <- what is the protection access for writing?
  • annotations <- dictionary with configured annotations

Method provides:

  • selectorName <- full name of the method, i.e for func foo(bar: Bar) -> Bar foo(bar:)
  • shortName <- short method name, i.e. for func foo(bar: Bar) -> Bar foo
  • parameters <- list of all method parameters
  • returnType <- return type, if known, for initializers - containing type
  • returnTypeName <- return type name, including things like optional markup. Will be Void for methods without return value or empty string for initializers.
  • unwrappedReturnTypeName <- name of return type, unwrapping the optional e.g. for return type Int? this would return Int
  • isOptionalReturnType <- whether return type is optional, true for failable initializers
  • accessLevel <- method access level
  • isStatic <- whether method is static
  • isClass <- whether method is class (can be overriden by subclasses)
  • isInitializer <- whether method is an initializer
  • isFailableInitializer <- whether method is failable initializer
  • annotations <- dictionary with configured annotations

Method.Parameter provides:

  • name <- parameter name
  • argumentLabel <- argument label (external name), if not set will be eqal to name
  • type <- type of parameter, if known
  • typeName <- parameter type name, including things like optional markup
  • unwrappedTypeName <- name of the type, unwrapping the optional e.g. for parameter with type Int? this would return Int
  • isOptional <- whether is optional

Custom Stencil tags and filter

  • {{ name|upperFirst }} - makes first letter in name uppercase
  • {% if name|contains: "Foo" %} - check if name contains arbitrary substring
  • {% if name|hasPrefix: "Foo" %}- check if name starts with arbitrary substring
  • {% if name|hasSuffix: "Foo" %}- check if name ends with arbitrary substring
  • static, instance, computed, stored - can be used on Variable[s] as filter e.g. {% for var in variables|instance %}
  • static, instance, class, initializer - can be used on Method[s] as filter e.g. {% for method in allMethods|instance %}
  • enum, class, struct, protocol - can be used for Type[s] as filter
  • count - can be used to get count of filtered array

Using Source Annotations

Sourcery supports annotating your classes and variables with special annotations, similar how attributes work in Rust / Java

/// sourcery: skipPersistence
/// Some documentation comment
/// sourcery: anotherAnnotation = 232, yetAnotherAnnotation = "value"
/// Documentation
var precomputedHash: Int

If you want to attribute multiple items with same attributes, you can use section annotations:

/// sourcery:begin: skipEquality, skipPersistence
  var firstVariable: Int
  var secondVariable: Int
/// sourcery:end

Rules:

  • Multiple annotations can occur on the same line
  • You can add multiline annotations
  • You can interleave annotations with documentation
  • Sourcery scans all sourcery: annotations in the given comment block above the source until first non-comment/doc line

Format:

  • simple entry, e.g. sourcery: skipPersistence
  • key = number, e.g. sourcery: another = 123
  • key = string, e.g. sourcery: jsonKey = "json_key"

Accessing in templates:

{% ifnot variable.annotations.skipPersistence %}
  var local{{ variable.name|capitalize }} = json["{{ variable.annotations.jsonKey }}"] as? {{ variable.typeName }}
{% endif %}

Installing

Binary form The easiest way to download the tool right now is to just grab a newest `.zip` distribution from [releases tab](https://github.com/krzysztofzablocki/Sourcery/releases).
Via CocoaPods If you're using CocoaPods, you can simply add pod 'Sourcery' to your Podfile.

This will download the Sourcery binaries and dependencies in Pods/. You just need to add $PODS_ROOT/Sourcery/bin/sourcery {source} {templates} {output} in your Script Build Phases.

Via Swift Package Manager If you're using SwiftPM, you can simply add 'Sourcery' to your manifest.

Sourcery is placed in Packages. After your first swift build, you can run .build/debug/sourcery {source} {templates} {output}.

From Source You can clone it from the repo and just run `Sourcery.xcworkspace`.

Usage

Sourcery is a command line tool sourcery:

$ ./sourcery <source> <templates> <output> [--args arg1=value,arg2]

Arguments:

  • source - Path to a source swift files.
  • templates - Path to templates. File or Directory.
  • output - Path to output. File or Directory.
  • args - Additional arguments to pass to templates. Each argument can have explicit value or will have implicit true value. Arguments should be separated with , without spaces. Arguments are accessible in templates via argument.name

Options:

  • --watch [default: false] - Watch both code and template folders for changes and regenerate automatically.
  • --verbose [default: false] - Turn on verbose logging for ignored entities

Contributing

Contributions to Sourcery are welcomed and encouraged!

It is easy to get involved. Please see the Contributing guide for more details.

A list of contributors is available through GitHub.

To give clarity of what is expected of our community, Sourcery has adopted the code of conduct defined by the Contributor Covenant. This document is used across many open source communities, and I think it articulates my values well. For more, see the Code of Conduct.

License

Sourcery is available under the MIT license. See LICENSE for more information.

Attributions

This tool is powered by

Thank you! for:

  • Mariusz Ostrowski for creating the logo.
  • Artsy Eidolon team, because we use their codebase as a stub data for performance testing the parser.
  • Olivier Halligon for showing me his setup scripts for CLI tools which are powering our rakefile.

Other Libraries / Tools

If you want to generate code for asset related data like .xib, .storyboards etc. use SwiftGen. SwiftGen and Sourcery are complementary tools.

Make sure to check my other libraries and tools, especially:

  • KZPlayground - Powerful playgrounds for Swift and Objective-C
  • KZFileWatchers - Daemon for observing local and remote file changes, used for building other developer tools (Sourcery uses it)

You can follow me on twitter for news/updates about other projects I am creating.

About

A tool that brings meta-programming to Swift, allowing you to code generate Swift code.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Languages

  • Swift 97.4%
  • Ruby 2.1%
  • Other 0.5%