Origin

Origin provides ease-to-use macros for delegating methods from a class to another object. It behaves like delegate Crystal's stdlib macro, but with type declaration support and simpler declaration.

This kind of macro is really interesting when, for example, using the Decorator Pattern.

It supports:

Installation

dependencies:
  origin:
    github: pyrsmk/origin
    version: ~> 0.1.0

Usage

Basics

Origin has two macros: wire and autowire. They work in a similar way by relying on the definition of an @origin instance variable with the object you want to wire on.

See the following very simple example using wire:

require "origin"

class Animal
  def head
    1
  end

  def ears
    2
  end

  def legs
    4
  end
end

class Human
  wire head, to: head
  wire ears, to: ears

  def initialize(@origin : Animal); end

  def legs
    2
  end

  def arms
    2
  end
end

human = Human.new(Animal.new)
# Prints `2`
puts human.legs
# Prints `2`
puts human.ears
# Prints `1`
puts human.head

That is, we don't need to re-implement head and ears since we don't want to change their behavior.

Internally, the code wire head, to: head compiles to:

def head(*args, **options)
  @origin.head(*args, **options)
end

def head(*args, **options)
  @origin.head(*args, **options) do |*yield_args|
    yield *yield_args
  end
end

That means wire can handle arguments and blocks as well.

Wire a method to another name

wire is also useful when you want to plug to a method with another name. Let's use the same example with an Animal and a Human:

class Animal
  def tail
    "I'm a tail!"
  end
end

class Human
  wire tailbone, to: tail

  def initialize(@origin : Animal); end
end

# Prints `"I'm a tail!"`
puts Human.new(Animal.new).tailbone

Return types

At this point, you may think: But what about return type definition?. Here's how we can declare types:

class Animal
  def head : Int32; end

  def ears : Int32; end

  def eyes : Int32; end

  def nose : Bool; end

  def tailbone : String; end
end

class Human
  wire head : Int32, to: head
  wire ears : Int32, to: ears
  wire eyes : Int32, to: eyes
  wire nose : Bool, to: nose
  wire tailbone : String, to: tail

  def initialize(@origin : Animal); end
end

Setters and special method names

There's a special syntax we had not talk about yet. If you want to wire setters or some weird method names like << or []? you would see that the compiler's complaining. Hence, for these cases, you need to use a symbol.

Just note that if you want to have a return type for special method names, you'll need to explicitly define it with the return_type option (but you won't be able to use this trick with autowire).

Here's a more concrete example:

class StringCollection
  @items = [] of String

  def <<(item : String)
    @items << item
  end

  def []=(index : Int32, item : String)
    @items[index] = item
  end

  def [](index : Int32) : String
    @items[index]
  end

  def []?(index : Int32) : String?
    @items[index]?
  end
end

class SymbolToStringCollection
  wire :[], return_type: String, to: :[]
  wire :[]?, return_type: String?, to: :[]?

  def initialize(@origin : StringCollection); end

  def <<(item : Symbol)
    @origin << item.to_s
  end

  def []=(index : Int32, item : Symbol)
    @origin[index] = item.to_s
  end
end

collection = SymbolToStringCollection.new(StringCollection.new)
collection << :foo
collection[0] = :bar
# Prints `"bar"`
puts collection[0]?
# Prints `nil`
puts collection[1]?

Auto-wiring

After we approached how wire works, we can talk about autowire. It wraps wire behavior and simplify the declaration for methods you don't need to rename:

class Animal
  def head : Int32; end

  def ears : Int32; end

  def eyes : Int32; end

  def nose : Bool; end
end

class Human
  autowire head : Int32,
           ears : Int32,
           eyes : Int32,
           node : Bool

  def initialize(@origin : Animal); end
end

Contributing

Before contributing don't hesitate to ask if your idea of a new feature could be accepted before developing it.

  1. Fork the project (https://github.com/pyrsmk/origin/fork)
  2. Initialize the repository (make init)
  3. Create your feature branch (git checkout -b feature/my-new-feature)
  4. Write your code and exhaustive tests (make test)
  5. Push and create a new Pull Request
  6. Wait for the maintainers to approve or request changes
  7. Merge your work 🎉️