Skip to content

Instantly share code, notes, and snippets.

@wkirby
Last active February 8, 2018 23:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wkirby/612841a28720538d620c3260f6ae238d to your computer and use it in GitHub Desktop.
Save wkirby/612841a28720538d620c3260f6ae238d to your computer and use it in GitHub Desktop.
Boring Presenters
# Optionally extend the ActiveRecord model to
# allow you to take an ActiveRecord instance and call
# `#presenter` on it to get an automatically-bound
# presenter.
#
# The downside to this is that it will use a separate
# instance of the presenter for each instance of
# a model, instead of sharing a single presenter
# that gets re-bound to each instance of a model.
module Boring::ActiveRecordExtension
extend ActiveSupport::Concern
def presenter(view_context:)
if @__presenter.nil?
presenter_class = #{self.class.name}Presenter".classify.safe_constantize
if presenter_class.present?
@__presenter = presenter_class.new(view_context: view_context).bind(self)
else
fail "Could not locate presenter class for #{self.class.name}."
end
end
@__presenter
end
end
# include the extension
ActiveRecord::Base.send(:include, Boring::ActiveRecordExtension)
module Boring
class Presenter
private
attr_reader :view_context
alias_method :v, :view_context
@__arguments = {}
class << self
attr_accessor :__arguments
end
def self.arguments(args)
@__arguments = args
class_eval do
define_method(:initialize) do |view_context:, **bindings|
unless view_context.kind_of?(ActionView::Base)
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}."
end
@view_context = view_context;
self.class.__arguments.each do |arg_name, arg_class| # dies if nil or empty
arg_value = bindings[arg_name]
# Ensure all of our bindings are the appropriate type
if bindings.has_key?(arg_name) && !arg_value.kind_of?(arg_class)
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}."
end
instance_variable_set("@#{arg_name}", arg_value)
end
# Ensure we don't have any unexpected arguments
extra_bindings = (bindings.keys - args.keys)
raise "Unexpected argument: #{extra_bindings.join(",")}." unless extra_bindings.empty?
end unless method_defined?(:initialize)
define_method(:bind) do |**bindings|
self.class.__arguments.each.each do |arg_name, arg_class|
arg_value = bindings[arg_name]
unless arg_value.kind_of?(arg_class)
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}."
end
instance_variable_set("@#{arg_name}", arg_value)
end
end unless method_defined?(:bind)
private
attr_reader *args.keys
end
end
def before_each_method(*)
# Ensure everything is properly bound before invoking this method
self.class.__arguments.each do |arg_name, arg_class|
arg_value = send(arg_name.to_sym)
raise "Argument '#{arg_name}' is not bound." unless arg_name.present?
raise "Argument '#{arg_name}' is of type #{arg_value.class}, expecting #{arg_class}." unless arg_value.kind_of?(arg_class)
end
end
def self.method_added(method_name)
return if self == Boring::Presenter
return if @__last_methods_added && @__last_methods_added.include?(method_name)
skipped_methods = [:initialize, :v, :view_context, :bind]
return if skipped_methods.include?(method_name)
skipped_methods = @__arguments.keys
return if skipped_methods.include?(method_name)
with = :"#{method_name}_with_before_each_method"
without = :"#{method_name}_without_before_each_method"
@__last_methods_added = [method_name, with, without]
define_method with do |*args, &block|
before_each_method method_name
send without, *args, &block
end
alias_method without, method_name
alias_method method_name, with
@__last_methods_added = nil
end
end
end
# views/users/index.html.erb
<ul>
<% @users.each do |user| %>
<% @user_presenter.bind(user: user) %>
<li>
<p>Full Name: <%= @user_presenter.name %></p>
<p>Birthday: <%= @user_presenter.birth_date %></p>
</li>
<% end %>
</ul>
# presenters/user_presenter.rb
class UserPresenter < Boring::Presenter
# Declare the arguments needed to bind to presenter and their type
arguments user: User
# Declare pass-through methods
delegate :birth_date, to: :user
# Methods to be handled by the presenter
def name
[user.first_name, user.last_name].reject(&:blank?).join(" ")
end
end
# controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
@user_presenter = UsersPresenter.new(view_context: view_context)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment