Storyteller: the code

(en)

ruby

This is the second part of Storyteller. You can read the first post here.

Storyteller

I cheated, storyteller sounds more like User Stories, and we've been talking about Use Cases all along, but the name sounded cooler, and maybe we can some day write our user stories within Storyteller.

So, the base of Storyteller is a Story which should be named like an action, i.e. BuyTickets, SendCargoManifest and they will inherit from Story. Each story will have its own parameters, for example concert, location and quantity. The first big change, is that the autoinitializer is gone, and for now SmartInit will take the helm, in the future we expect to remove the dependency if this becomes the only dependency.

class BuyTickets < Storyteller::Story
initialize_with :concert, :location, :quantity
end

So now, just like before, you will have access to all initializable variables, now just calling concert, location, or quantity. Most of the Use Cases ran this way, but some had some extra assigns, we used to called them after super like so

class BuyTickets < UseCases::Base
def initialize(concert:, location:, quantity:)
super
@limit = 10
end
end

But now that part is gone, so the first hook was written

class BuyTickets < Storyteller::Story
initialize_with :concert, :location, :quantity

after_init :set_limits

def set_limits
@limit = 10
end
end

I know that can be done with SmartInit, but give me a break, you can come with a better example for sure.

You can call as much methods as you want in the after_init, just do it one by one (multimethod calls are still not implemented).

Then you can define a prepares_with a validates_with method, which allows you to call them once (you can call them more times, but just the last one will be considered).

And then the most important, you can call step to implement each step of the Story, this will follow the order you wrote them in the file, thats the thing to be careful!

On each of them you can pass a symbol or a Proc as arguments, and steps can be named.

class BuyTickets < Storyteller::Story
initialize_with :concert, :location, :quantity

after_init :set_limits

prepares_with :load_prices

validates_with do
error(:quantity, :exceeds_limit) if quantity > limit
end

step 'Find best location', :find_best_location
step :lock_tickets
step :generate_voucher
step 'Notify client', do
# Code to notify the client of their tickets
end
end

BuyTickets.new(concert:, location:, quantity:).execute.result

See? Looks for DSL-y

The innards don't look too good for now, but the outer shell looks way sexier. Here is what I did:

Every inherited class has an array for holding the after_init, prepare, validate, and step methods, they can be symbols or Procs.

Each hook (are they called hooks?) appends one of those into the arrays.

Whenever a stage comes, something like this happens:

@@step_methods.each do |step|
meth = step[:arg]
if meth.is_a?(Proc)
@result = self.instance_eval &meth
else
@result = send(meth)
end
end

def self.step(name = '', meth = nil, &block)
@@step_methods << { name:, arg: (meth.nil? ? block : meth) }
end

I'm pretty sure this is the ugliest way to do it, but works for now and I hope to improve it, its probable that some of Rails' libraries has a way to implement it nicer, but we are trying to not depend on any Rails code (I really miss ActiveSupport)

Excuses aside, what I try to do here is to first, detect what is coming into the hook, so arg becomes just the thing we need to run, to later evaluate it again to run it. Just realized that its way better to just pass the arguments and evaluate for nil later on. Well, thats for version 0.1.1!

The rest of the code pretty much is the same but the difference is huge, maybe the engine will change, but the interface looks strong enough to be used in our products, so now we have to reach the next big thing: shipping into a gem.

Anyway, you can reach the gem's webpage here: https://blog.avispa.tech/storyteller/