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.
Sourcery is so meta that it is used to code-generate its boilerplate code
Table of Contents generated with DocToc
- What is Sourcery?
- Why?
- Examples
- Writing templates
- Installing
- Usage
- Contributing
- License
- Attributions
- Other Libraries / Tools
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.
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 }
}
...
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
}
...
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)
}
)
...
}
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 nametypes.all
=> all types, excluding protocolstypes.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 fromBaseClassOrProtocol
(all type names encountered, even those that Sourcery didn't scan)
All of these properties return Type
objects.
Available types:
**Type**. Properties:
name
<- namekind
<- convience accessor that will contain one ofenum
,class
,struct
,protocol
, it will also provideextension
for types that are unknown to us(e.g. 3rd party or objc), but had extension in the projectisGeneric
<- info whether the type is genericlocalName
<- name within parent scopevariables
<- 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
andallVariables
- if you want to access all available variables, including those from inherited / protocol, then use
methods
<- list of all methods defined in this type, excluding those from protocols or inheritanceallMethods
<- same principles as inallVariables
initializers
<- list of all initializersinherits.BaseClass
=> info whether type inherits from known base classimplements.Protocol
=> info whether type implements known protocolbased.BaseClassOrProtocol
=> info whether type implements or inherits fromBaseClassOrProtocol
(all type names encountered, even those that Sourcery didn't scan)containedTypes
<- list of types contained within this typeparentName
<- list of parent type (for contained ones)attributes
<- type attributes, i.e.type.attributes.objc
annotations
<- dictionary with configured annotations
**Enum**. Built on top of `Type` and provides some additional properties:
rawType
<- enum raw typecases
<- list ofEnum.Case
hasAssociatedValues
<- true if any of cases has associated values
**Enum.Case**. Properties:
name
<- namerawValue
<- raw valueassociatedValues
<- list ofAssociatedValue
annotations
<- dictionary with configured annotations
**Enum.Case.AssociatedValue**. Properties:
localName
<- name to use to construct value, i.e.value
inFoo.foo(value: ...)
externalName
<- name to use when binding value, i.e.value
orother
inenum Foo { case foo(value: ..., other: ... )}
. Will use index as a fallbacktypeName
<- name of type of associated value (TypeName)actualTypeName
<- returnstypeName.actualTypeName
or if it'snil
returnstypeName
unwrappedTypeName
<- shorthand fortypeName.unwrappedTypeName
isOptional
<- shorthand fortypeName.isOptional
isImplicitlyUnwrappedOptional
<- shorthand fortypeName. isImplicitlyUnwrappedOptional
isTuple
<- shorthand fortypeName.isTuple
**Variable**. Properties:
name
<- Nametype
<- type of the variable, if knowntypeName
<- returns name of the type (TypeName)actualTypeName
<- returnstypeName.actualTypeName
or if it'snil
returnstypeName
unwrappedTypeName
<- shorthand fortypeName.unwrappedTypeName
isOptional
<- shorthand fortypeName.isOptional
isImplicitlyUnwrappedOptional
<- shorthand fortypeName. isImplicitlyUnwrappedOptional
isComputed
<- whether is computedisStatic
<- whether is static variableisTuple
<- shorthand fortypeName.isTuple
typeName.tuple
<- returns information about tuple typereadAccess
<- what is the protection access for reading?writeAccess
<- what is the protection access for writing?attributes
<- variable attributes, i.e.var.attributes.NSManaged
annotations
<- dictionary with configured annotations
**Method**. Properties:
selectorName
<- full name of the method, i.e forfunc foo(bar: Bar) -> Bar
foo(bar:)
shortName
<- short method name, i.e. forfunc foo(bar: Bar) -> Bar
foo
parameters
<- list of all method parametersreturnType
<- return type, if known, for initializers - containing typereturnTypeName
<- return type name (TypeName). Will beVoid
for methods without return value or empty string for initializers.unwrappedReturnTypeName
<- shorthand forreturnTypeName.unwrappedTypeName
isOptionalReturnType
<- shorthand forreturnTypeName.isOptional
isImplicitlyUnwrappedOptionalReturnType
<- shorthand forreturnTypeName. isImplicitlyUnwrappedOptional
accessLevel
<- method access levelisStatic
<- whether method is staticisClass
<- whether method is class (can be overriden by subclasses)isInitializer
<- whether method is an initializerisFailableInitializer
<- whether method is failable initializerattributes
<- method attributes, i.e.method.attributes.discardableResult
annotations
<- dictionary with configured annotations
**Method.Parameter**. Properties:
name
<- parameter nameargumentLabel
<- argument label (external name), if not set will be eqal toname
type
<- type of parameter, if knowntypeName
<- parameter type name (TypeName)actualTypeName
<- returnstypeName.actualTypeName
or if it'snil
returnstypeName
unwrappedTypeName
<- shorthand fortypeName.unwrappedTypeName
isOptional
<- shorthand fortypeName.isOptional
isImplicitlyUnwrappedOptional
<- shorthand fortypeName. isImplicitlyUnwrappedOptional
isTuple
<- shorthand fortypeName.isTuple
typeAttributes
<- parameter's type attributes, shorthand fortypeName.attributes
, i.e.param.typeAttributes.escaping
**TypeName**. Properties:
name
<- type nameactualTypeName
<- if given type is a typealias will contain actual type nameunwrappedTypeName
<- returns name of the type, unwrapping the optional e.g. for variable with typeInt?
this would returnInt
isOptional
<- whether is optionalisImplicitlyUnwrappedOptional
<- whether is implicitly unwrapped optionalisVoid
<- whether type is Void (Void
or()
)isTuple
<- whether given type is a tupletuple.elements
<- if given type is a tuple returns its elements information (TupleType.Element)attributes
<- type attributes, i.e.typeName.attributes.escaping
**TupleType.Element**. Properties:
name
<- element nametype
<- type of element, if knowntypeName
<- element type name (TypeName)unwrappedTypeName
<- shorthand fortypeName.unwrappedTypeName
isOptional
<- shorthand fortypeName.isOptional
isTuple
<- shorthand fortypeName.isTuple
{{ name|upperFirst }}
- makes first letter inname
uppercase{% if name|contains: "Foo" %}
- check ifname
contains arbitrary substring, can be negated with!
prefix.{% if name|hasPrefix: "Foo" %}
- check ifname
starts with arbitrary substring, can be negated with!
prefix.{% if name|hasSuffix: "Foo" %}
- check ifname
ends with arbitrary substring, can be negated with!
prefix.static
,instance
,computed
,stored
,tuple
- can be used on Variable[s] as filter e.g.{% for var in variables|instance %}
, can be negated with!
prefix.static
,instance
,class
,initializer
- can be used on Method[s] as filter e.g.{% for method in allMethods|instance %}
, can be negated with!
prefix.enum
,class
,struct
,protocol
- can be used for Type[s] as filter, can be negated with!
prefix.based
,implements
,inherits
- can be used for Type[s], Variable[s], Associated value[s], can be negated with!
prefix.count
- can be used to get count of filtered arrayannotated
- can be used on Type[s], Variable[s], Method[s] and Enum Case[s] to filter by annotation, e.g.{% for var in variable|annotated: \"skipDescription\"%}
, can be negated with!
prefix.
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
- 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
- simple entry, e.g.
sourcery: skipPersistence
- key = number, e.g.
sourcery: another = 123
- key = string, e.g.
sourcery: jsonKey = "json_key"
{% ifnot variable.annotations.skipPersistence %}
var local{{ variable.name|capitalize }} = json["{{ variable.annotations.jsonKey }}"] as? {{ variable.typeName }}
{% endif %}
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`.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 viaargument.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
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.
Sourcery is available under the MIT license. See LICENSE for more information.
This tool is powered by
- SourceKitten by JP Simard
- Stencil and few other libs by Kyle Fuller
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.
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.