Skip to content

Instantly share code, notes, and snippets.

@alessandro-fazzi
Last active December 12, 2022 22:12
Show Gist options
  • Save alessandro-fazzi/61386d37441c41a9e0d54de0b45aa58e to your computer and use it in GitHub Desktop.
Save alessandro-fazzi/61386d37441c41a9e0d54de0b45aa58e to your computer and use it in GitHub Desktop.
[Study] An alternative approach managing LightService hooks

Sequence is a class representing a sequence, a procedure.

A Sequence object is initialized using [] method, passing in a list of runnable objects just as it would be an Array.

A runnable object may be:

  • LightService::Action
  • Sequence
  • Member

Sequence has the following behavioural rules:

  • it accepts any number of nested Sequence objects
  • it accepts manually created Member objects
  • if a LightService::Action is passed in, then it will be mapped to a Member

Member is a kind of proxy object for a LightService::Action; the advantage of directly using Member objects instead of a "raw" Action is that Member responds to modifier methods used to inject hooks before or after the Action.

Sequence and Member objects are designed - generally - following the "command query separation" concept.

Sequence's modifiers methods are:

  • before; sets one or more actions to be run before the sequence itself
  • after; sets one or more actions to be run after the sequence itself
  • before_each; sets one or more actions to be run before each member inside the sequence. Thus this method doesn't modify the sequence directly.
  • after_each; sets one or more actions to be run after each member inside the sequence. Thus this method doesn't modify the sequence directly.
  • with; sets the initial context of the sequence. Note that a nested Sequence will respond to with too, but setting a context on a "sub-sequence" would be a conceptual error: the context has to be set only on the outer Sequence. The outer one, will automatically pass the context down to the inner, passing the state through all the procedure.

Member's modifiers methods are:

  • before
  • after

Note that hooks will be attached to Member objects preserving the order: given a member with a "Foo" action in the "before" hook, if a before_each will be called on the containing sequence with "Bar" action as argument, then the member will have Foo, Bar before actions in that order.

as = Sequence[Member.new(Action1).before(Action2)].before_each(Action3)
pp as
#<Sequence:0x0000000107b6e538 @after_actions=[], @before_actions=[], @members=[#<Member:0x0000000107b6e6a0 @action=Action1, @after_actions=[], @before_actions=[Action2, Action3]>]>
source "https://rubygems.org"
gem "light-service", "~> 0.18.0"
gem "rubocop"
gem "debug", "~> 1.6"
GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
ast (2.4.2)
concurrent-ruby (1.1.10)
debug (1.7.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
io-console (0.5.11)
irb (1.5.1)
reline (>= 0.3.0)
json (2.6.3)
light-service (0.18.0)
activesupport (>= 4.0.0)
minitest (5.16.3)
parallel (1.22.1)
parser (3.1.3.0)
ast (~> 2.4.1)
rainbow (3.1.1)
regexp_parser (2.6.1)
reline (0.3.1)
io-console (~> 0.5)
rexml (3.2.5)
rubocop (1.40.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.23.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.24.0)
parser (>= 3.1.1.0)
ruby-progressbar (1.11.0)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
unicode-display_width (2.3.0)
PLATFORMS
arm64-darwin-21
DEPENDENCIES
debug (~> 1.6)
light-service (~> 0.18.0)
rubocop
BUNDLED WITH
2.3.7
# frozen_string_literal: true
require 'rubygems'
require 'bundler/setup'
# require your gems as usual
Bundler.require(:default)
(1..20).each do |n|
Object.const_set("Action#{n}", Class.new do
extend LightService::Action
expects :foo
executed do |context|
context.foo << name
p "Action -#{name}- called. Context: #{context}"
end
end)
end
module Runnable
def run(action, context)
case action
in LightService::Action
return action.execute(context)
in Member
return action.call(context)
in Sequence
return action.with(context).call
in Proc
action.call(context)
return context
end
raise ArgumentError
end
private :run
end
class Sequence
include Runnable
attr_reader :members, :context
attr_accessor :before_actions, :after_actions
class << self
def[](*actions)
new[*actions]
end
end
def [](*actions)
@before_actions = []
@after_actions = []
@members = actions.map do |action|
next action if action.is_a?(Sequence)
next action if action.is_a?(Member)
next Member.new(action) if action.is_a?(LightService::Action)
raise ArgumentError
end
self
end
def with(context)
@context = LightService::Context.make(context)
self
end
def after(*actions)
@after_actions = (after_actions + actions).flatten
self
end
def before(*actions)
@before_actions = (before_actions + actions).flatten
self
end
def after_each(*actions)
@members.each do |member|
member.after(actions)
end
self
end
def before_each(*actions)
@members.each do |member|
member.before(actions)
end
self
end
def call
before_actions.each do |action|
@context = run(action, context)
end
members.each do |member|
@context = run(member, context)
end
after_actions.each do |action|
@context = run(action, context)
end
@context
end
def flatten
FlatMap.new(self).map
end
end
class Member
include Runnable
attr_reader :action, :before_actions, :after_actions
def initialize(action)
@action = action
@before_actions = []
@after_actions = []
end
def call(context)
before_actions.each do |action|
context = run(action, context)
end
context = run(action, context)
after_actions.each do |action|
context = run(action, context)
end
context
end
def flatten
FlatMap.new(self).to_a
end
def after(*actions)
@after_actions = (after_actions + actions).flatten
self
end
def before(*actions)
@before_actions = (before_actions + actions).flatten
self
end
end
class FlatMap
attr_reader :map, :mappable
def initialize(mappable)
@map = []
@mappable = mappable
map << map_before_actions!
case mappable
in Sequence
map << map_members!
in Member
map << mappable.action.inspect
end
map << map_after_actions!
@map = map.flatten
end
def map_before_actions!
mappable.before_actions.map do |action|
handle_action_map(action)
end
end
def map_members!
mappable.members.map do |member|
handle_action_map(member)
end
end
def map_after_actions!
mappable.after_actions.map do |action|
handle_action_map(action)
end
end
def to_a
@map
end
private
def handle_action_map(action)
case action
in LightService::Action | Proc
return action.inspect
in Member
return action.flatten
in Sequence
return action.with(mappable.context).flatten
end
end
end
as = Sequence[
Sequence[Action1, Action2].before(Action11).after(Action12),
Member.new(Action3).after(
->(ctx) { p 'Into a proc mutating the context' and ctx[:bar] = :baz }
),
Action4
]
.after_each([Action7, Action8])
.before_each(Action5, Action6)
.before(Action9)
.after(Action10)
.with({ foo: [] })
pp as
pp as.flatten
outcome = as.call
p outcome
# The STDOUT
#
# #<Sequence:0x000000011805d368
# @after_actions=[Action10],
# @before_actions=[Action9],
# @context={:foo=>[]},
# @members=
# [
# #<Sequence:0x000000011805dac0 @after_actions=[Action12, Action7, Action8], @before_actions=[Action11, Action5, Action6],
# @members=[
# #<Member:0x000000011805d8b8 @action=Action1, @after_actions=[], @before_actions=[]>,
# #<Member:0x000000011805d7f0 @action=Action2, @after_actions=[], @before_actions=[]>
# ]>,
# #<Member:0x000000011805d548 @action=Action3, @after_actions=[#<Proc:0x000000011805d4d0 main.rb:299 (lambda)>, Action7, Action8], @before_actions=[Action5, Action6]>,
# #<Member:0x000000011805d228 @action=Action4, @after_actions=[Action7, Action8], @before_actions=[Action5, Action6]>
# ]
# >
#
# ["Action9", "Action11", "Action5", "Action6", "Action1", "Action2", "Action12", "Action7", "Action8", "Action5", "Action6", "Action3", "#<Proc:0x000000011805d4d0 main.rb:299 (lambda)>", "Action7", "Action8", "Action5", "Action6", "Action4", "Action7", "Action8", "Action10"]
#
# "Action -Action9- called. Context: {:foo=>[\"Action9\"]}"
# "Action -Action11- called. Context: {:foo=>[\"Action9\", \"Action11\"]}"
# "Action -Action5- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\"]}"
# "Action -Action6- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\"]}"
# "Action -Action1- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\"]}"
# "Action -Action2- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\"]}"
# "Action -Action12- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\"]}"
# "Action -Action7- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\"]}"
# "Action -Action8- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\"]}"
# "Action -Action5- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\"]}"
# "Action -Action6- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\"]}"
# "Action -Action3- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\"]}"
# "Into a proc mutating the context"
# "Action -Action7- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\"], :bar=>:baz}"
# "Action -Action8- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\"], :bar=>:baz}"
# "Action -Action5- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\"], :bar=>:baz}"
# "Action -Action6- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\", \"Action6\"], :bar=>:baz}"
# "Action -Action4- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action4\"], :bar=>:baz}"
# "Action -Action7- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action4\", \"Action7\"], :bar=>:baz}"
# "Action -Action8- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action4\", \"Action7\", \"Action8\"], :bar=>:baz}"
# "Action -Action10- called. Context: {:foo=>[\"Action9\", \"Action11\", \"Action5\", \"Action6\", \"Action1\", \"Action2\", \"Action12\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action3\", \"Action7\", \"Action8\", \"Action5\", \"Action6\", \"Action4\", \"Action7\", \"Action8\", \"Action10\"], :bar=>:baz}"
#
# LightService::Context({:foo=>["Action9", "Action11", "Action5", "Action6", "Action1", "Action2", "Action12", "Action7", "Action8", "Action5", "Action6", "Action3", "Action7", "Action8", "Action5", "Action6", "Action4", "Action7", "Action8", "Action10"], :bar=>:baz}, success: true, message: '', error_code: nil, skip_remaining: false, aliases: {})
AllCops:
TargetRubyVersion: 3.1.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment