Skip to content

Instantly share code, notes, and snippets.

@lucaswinningham
Last active September 29, 2023 13:35
Show Gist options
  • Save lucaswinningham/015c136ae541d5dbbc3a3a9ce7488ec3 to your computer and use it in GitHub Desktop.
Save lucaswinningham/015c136ae541d5dbbc3a3a9ce7488ec3 to your computer and use it in GitHub Desktop.
Simple Ruby Middleware Chain
# frozen_string_literal: true
module Middleware
class Entry
attr_reader :klass, :args, :kwargs, :block
def initialize(klass, *args, **kwargs, &block)
@klass = klass
@args = args
@kwargs = kwargs
@block = block
end
end
end
# frozen_string_literal: true
require_relative 'entry'
module Middleware
class Chain
IDENTITY = proc { |x| x }.freeze
def initialize(&block)
block.call(self) if block_given?
end
def remove(klass)
entries.delete_if { |entry| entry.klass == klass }
end
def add(klass, *args, **kwargs, &block)
remove(klass) if exists?(klass)
entries << Entry.new(klass, *args, **kwargs, &block)
end
def prepend(klass, *args, **kwargs, &block)
remove(klass) if exists?(klass)
entries.insert(0, Entry.new(klass, *args, **kwargs, &block))
end
def insert_before(old_klass, klass, *args, **kwargs, &block)
i = entries.index { |entry| entry.klass == klass }
new_entry = i.nil? ? Entry.new(klass, *args, **kwargs, &block) : entries.delete_at(i)
i = entries.index { |entry| entry.klass == old_klass } || 0
entries.insert(i, new_entry)
end
def insert_after(old_klass, klass, *args, **kwargs, &block)
i = entries.index { |entry| entry.klass == klass }
new_entry = i.nil? ? Entry.new(klass, *args, **kwargs, &block) : entries.delete_at(i)
i = entries.index { |entry| entry.klass == old_klass } || entries.count - 1
entries.insert(i + 1, new_entry)
end
def exists?(klass)
entries.any? { |entry| entry.klass == klass }
end
def call(payload)
chain = materialize.dup.reverse.reduce(IDENTITY) do |composition, entry|
entry.call(composition)
end
chain.call(payload)
end
private
def entries
@entries ||= []
end
def materialize
entries.map do |entry|
proc { |succ| entry.klass.new(succ, *entry.args, **entry.kwargs, &entry.block) }
end
end
end
end
# frozen_string_literal: true
class Outer
def initialize(succ)
@succ = succ
end
def call(payload)
puts "Outer#call payload: #{payload.inspect}"
result = @succ.call(payload.merge(final: 'outer'))
puts "Outer#call result: #{result.inspect}"
result.merge(final: 'outer')
end
end
class Middle
def initialize(succ)
@succ = succ
end
def call(payload)
puts "Middle#call payload: #{payload.inspect}"
result = @succ.call(payload.merge(final: 'middle'))
puts "Middle#call result: #{result.inspect}"
result.merge(final: 'middle')
end
end
class Inner
def initialize(succ)
@succ = succ
end
def call(payload)
puts "Inner#call payload: #{payload.inspect}"
result = @succ.call(payload.merge(final: 'inner'))
puts "Inner#call result: #{result.inspect}"
result.merge(final: 'inner')
end
end
my_middleware = Middleware::Chain.new do |middleware|
middleware.add(Outer)
middleware.add(Middle)
middleware.add(Inner)
end
my_middleware.call({})
# Outer#call payload: {}
# Middle#call payload: {:final=>"outer"}
# Inner#call payload: {:final=>"middle"}
# Inner#call result: {:final=>"inner"}
# Middle#call result: {:final=>"inner"}
# Outer#call result: {:final=>"middle"}
# => {:final=>"outer"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment