Skip to content

AliSoftware/SourceryTemplates

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SourceryTemplates

This repository contains some templates (in the Templates/ directory) to use for Code Generation in Swift with Sourcery.

You can see them in action by opening the TemplatesDemo.xcodeproj Xcode project: its UnitTests target contains some use case examples for each template, and some code using the generated code in each associated XCTestCase.


Type Erasure

This template provides type erasure to any protocol you annotated accordingly.

Usage

  • Annotate the protocol to type-erase using // sourcery: TypeErase = X where X is the associatedtype to erase
  • Use the template in Templates/TypeErase.stencil with Sourcery to generate Type-Erased code for this annotated protocol(s)
  • Add the generated TypeErase.generated.swift file to your Xcode project
  • Profit!

Examples

You can find some examples in the UnitTests/TypeErasure directory in this repo, like those:

// sourcery: TypeErase = PokemonType
protocol Pokemon {
  associatedtype PokemonType
  func attack(move: PokemonType)
}
// sourcery: TypeErase = Model
protocol Row {
  associatedtype Model

  var sizeLabelText: String { get set }

  func configure(model: Model)
}

You can look at the file generated by Sourcery from those examples using the provided Templates/TypeErase.stencil template. Magic! 🎩✨

More Info

This is a first draft of the template, so please don't hesitate to improve it.

I haven't tested a lot of real-world use cases yet so some corner cases are probably missing but feel free to submit a PR to fix them!

The template generates the same code structure as the one explained in this BigNerdRanch article that inspired me to come up with a working template.

You can also find more information about Type-Erasure by watching Gwendolyn's talk at Try! Swift — from which I borrowed the Pokemon examples to test that template.

AutoInterface

This template allows to auto-generate a protocol matching the API of a class/struct.

Usage

  • Create an empty phantom protocol protocol AutoInterface {} somewhere in your code.
  • Make your classes or structs conform to AutoInterface to opt-in
  • Optionally, you can customize the name of the generated protocol for each type using annotations:
    • By default, the generated protocol as the same name as the type it was generated from, but prefixed with an I (e.g. protocol IWebService for class WebService: AutoInterface
    • You can use // sourcery: AutoInterfacePrefix = … to change that prefix
    • You can use // sourcery: AutoInterfaceSuffix = … to add a suffix
    • You can use // sourcery: AutoInterface = … to force an exact name (in that case the AutoInterfacePrefix & AutoInterfaceSuffix are ignored if present)
  • Use the template in Templates/AutoInterface.stencil with Sourcery to generate a protocol for each opted-in type and automatically make your type conform to that new protocol
  • Add the generated AutoInterface.generated.swift file to your Xcode project
  • Profit!

Examples

You can find some examples in the UnitTests/AutoInterface directory in this repo, like this:

class UserWSClient: AutoInterface {
  struct User {
    let id: Int
    let name: String
  }

  let baseURL: URL = URL(string: "https://example.com/api")!
  
  func fetchUsers() -> [User] {
    return (0..<10).map { User(id: $0, name: "User\($0)") }
  }

  func fetchUser(id: Int) -> User? {
    return User(id: id, name: "User\(id)")
  }
}

Will generate a protocol IUserWSClient containing all the functions and properties of your type and make your type conform to that protocol:

protocol IUserWSClient {
	var baseURL: URL { get }
	func fetchUsers() -> [WSClient.UserWSClient.User]
	func fetchUser(id: Int) -> WSClient.UserWSClient.User?
}
extension UserWSClient: IUserWSClient {}

You can look at the file generated by Sourcery from this example using the provided Templates/AutoInterface.stencil template. Magic! 🎩✨

AutoPropertiesProtocol

This template allows you to generate one protocol for each property of your type, each protocol exposing that single property.

This allows you to later leverage protocol composition to express exactly which properties of that type you're gonna use and want to be able to access.

This pattern is typically useful to leverage protocol composition for Dependency Injection, which mixes the benefits of having a single struct containing all your dependencies (instead of passing them all one by one in your constructor) and the benefits of compile-type safety + explicit list of dependencies you want without exposing too much

Usage

  • Declare a phantom protocol protocol AutoPropertiesProtocol {} somewhere in your code
  • Make the class you want to opt-it for this feature to conform to this phantom protocol
  • Optionally, you can annotate your properties with the PropertiesProtocolPrefix and/or PropertiesProtocolSuffix annotations to provide a custom prefix/suffix for the name of the generated protocols
    • By default (if the annotation is not set / has no value), the prefix will be Has and there will be no suffix
    • Reminder: if you want all the properties of your type to have the same prefix/suffix, you can use // sourcery:begin: PropertiesProtocolPrefix = … at the beginning of your type + // sourcery:end at the end to apply the same annotation(s) to all the variables inside that scope.
  • Alternatively, you can also optionally annotate your properties with the PropertiesProtocol annotation to give an exact name for the protocol to generate for that property.
  • Finally, use the template in Templates/AutoPropertiesProtocol.stencil with Sourcery to generate all the necessary protocols

Examples

You can find some examples in the UnitTests/AutoPropertiesProtocol directory in this repo, like this one:

// sourcery:begin: PropertiesProtocolPrefix = I, PropertiesProtocolSuffix = Container
class Dependencies: AutoPropertiesProtocol {
  let webServiceClient: WebServiceClient
  let loginManager: LoginManager
  let cartManager: CartManager

  init(wsClient: WebServiceClient, loginManager: LoginManager, cartManager: CartManager) {
    self.webServiceClient = wsClient
    self.loginManager = loginManager
    self.cartManager = cartManager
  }
}
// sourcery:end

With this example above, you can then use Dependencies as a single struct containing all the dependencies you need to pass throughout your app workflow, but still keep type safety and explicitness by saying exactly which dependencies of that container you allow to use:

class LoginScreen {
  typealias Dependencies = IWebServiceClientContainer & ILoginManagerContainer
  private let deps: Dependencies
  init(deps: Dependencies) {
    self.deps = deps
  }

  func login() {
    print(self.deps.webServiceClient)
    print(self.deps.loginManager)
    // print(self.deps.cartManager) // Can't access this, which is a good thing :)
  }
}

You can look at the file generated by Sourcery from those examples using the provided Templates/AutoPropertiesProtocol.stencil template. Magic! 🎩✨

AutoCaseName

This template allows you to generate an extension to expose the root name of cases in enums with associated values.

This especially allows you to test if an enum is of a given case using a simple if or guard, without having to do a switch (which would otherwise be needed if you want to use a guard clause, because the enum has associated value, and sure you can use if case .loading = state, but you cannot negate the pattern matching condition)

Usage

  • Declare a phantom protocol protocol AutoCaseName {} somewhere in your code
  • Make the enums with associated values you want to opt-it for this feature to conform to this phantom protocol
  • Finally, use the template in Templates/AutoCaseName.stencil with Sourcery to generate all the necessary protocols

Examples

You can find some examples in the UnitTests/AutoCaseName directory in this repo, like this one:

enum State: AutoCaseName {
  case notLoaded
  case loading(message: String, percent: Double)
  case loaded(data: [String])
  case error(message: String, code: Int)
}

Without the Sourcery Template, if you wanted to then check if a State was not in state loaded without caring about the associated value of that state, you'd still have to use a switch:

let state = State.loading(message: "In progress…", percent: 0.42)
let isLoading: Bool = {
  switch state {
    case .loading: return true
    default: return false
  }
}()
guard !isLoading else { return }

But instead, using this Sourcery template, you can simply do:

let state = State.loading(message: "In progress…", percent: 0.42)
guard state.caseName != .loading else { return }

Also, as the caseName property returns a YourType.CaseName (generated by Sourcery)… which is Comparable, you can also compare if two instances of your enum are the same case regardless of their payload too:

let state1 = State.loaded(data: ["Foo", Bar"])
let state2 = State.loaded(data: ["Baz"])
let state3 = State.error(message)
guard state1.caseName == satte2.caseName, state1.caseName != state3.caseName else { return }

As a last bonus, the generated YourType.CaseName is also a enum CaseName: String (so it's RawRepresentable using a String) which allows you to get the name of a case as a String using state.caseName.rawValue if needed.

You can look at the file generated by Sourcery from those examples using the provided Templates/AutoCaseName.stencil template. Magic! 🎩✨

Other templates