In the gem I'm making, I want to allow developer to add a class method I've written, let's call it interceptor
, to a model, in classic Devise syntax:
class User < ActiveRecord::Base
has_interceptor
end
This allows you to call User.interceptor
, which returns an Interceptor
object that does magic things with querying the database through the Squeel
gem. All good.
However, I'd like to find a graceful way of allowing the developer to scope the queries the interceptor performs, first. This can be accomplished by allowing interceptor
to take in an ActiveRecord::Relation
and chain Squeel
off of that, and otherwise fall back on the model. This implementation works as follows:
# Builds on blank ARel from User:
User.interceptor.perform_magic
#=> "SELECT `users`.* FROM `users` WHERE interceptor magic"
# Build on scoped ARel from Relation:
User.interceptor( User.where('name LIKE (?)', 'chris') ).perform_magic
#=> "SELECT `users`.* FROM `users` WHERE `users`.`name` LIKE 'chris' AND interceptor magic"
Which is effective, but ugly. What I really want is something like:
# Build on scoped ARel:
User.where('name LIKE (?)', 'chris').interceptor.perform_magic
#=> "SELECT `users`.* FROM `users` WHERE `users`.`name` LIKE 'chris' AND interceptor magic"
Essentially, I'd like to 'tap in' to the ActiveRecord::Relation
chain and steal it's ARel, passing it into my Interceptor
object to modify it before I evaluate it. But every way I can think of to do this involves code so horrifying, I know God would kill a kitten if I implemented it. I don't need that blood on my hands. Help me save a kitten?
Adding to my complications,
class User < ActiveRecord::Base
has_interceptor :other_interceptor_name
end
allows you to call User.other_interceptor_name
, and models can have multiple interceptors. It works well, but makes using method_missing
an even worse idea than normal.
I ended up hacking ActiveRecord::Relation
's method_missing
after all, it didn't turn out too ugly. Here's the full process, from beginning to end.
My gem defines an Interceptor
class, intended to be a DSL that developers may subclass. This object takes in some root
ARel, from a Model
or a Relation
, and manipulates the query further before rendering.
# gem/app/interceptors/interceptor.rb
class Interceptor
attr_accessor :name, :root, :model
def initialize(name, root)
self.name = name
self.root = root
self.model = root.respond_to?(:klass) ? root.klass : root
end
def render
self.root.apply_dsl_methods.all.to_json
end
...DSL methods...
end
Implemented:
# sample/app/interceptors/user_interceptor.rb
class UserInterceptor < Interceptor
...DSL...
end
Then I give models the has_interceptor
method that defines new interceptors and builds an interceptors
mapping:
# gem/lib/interceptors/model_additions.rb
module Interceptor::ModelAdditions
def has_interceptor(name=:interceptor, klass=Interceptor)
cattr_accessor :interceptors unless self.respond_to? :interceptors
self.interceptors ||= {}
if self.has_interceptor? name
raise Interceptor::NameError,
"#{self.name} already has a interceptor with the name '#{name}'. "\
"Please supply a parameter to has_interceptor other than:"\
"#{self.interceptors.join(', ')}"
else
self.interceptors[name] = klass
cattr_accessor name
# Creates new Interceptor that builds off the Model
self.send("#{name}=", klass.new(name, self))
end
end
def has_interceptor?(name=:interceptor)
self.respond_to? :interceptors and self.interceptors.keys.include? name.to_s
end
end
ActiveRecord::Base.extend Interceptor::ModelAdditions
Implemented:
# sample/app/models/user.rb
class User < ActiveRecord::Base
# User.interceptor, uses default Interceptor Class
has_interceptor
# User.custom_interceptor, uses custom CustomInterceptor Class
has_interceptor :custom_interceptor, CustomInterceptor
# User.interceptors #show interceptor mappings
#=> {
# interceptor: #<Class:Interceptor>,
# custom_interceptor: #<Class:CustomInterceptor>
# }
# User.custom_interceptor #gets an instance
#=> #<CustomInterceptor:0x005h3h234h33>
end
With that alone, you can call User.interceptor and build an Interceptor
with a clean query as the root for all interceptor query manipulation. However, with a little more effort, we can extend ActiveRecord::Relation
so that you can call interceptor methods as an endpoint in a chain of scopes:
# gem/lib/interceptor/relation_additions.rb
module Interceptor::RelationAdditions
delegate :has_interceptor?, to: :klass
def respond_to?(method, include_private = false)
self.has_interceptor? method
end
protected
def method_missing(method, *args, &block)
if self.has_interceptor? method
# Creates new Interceptor that builds off of a Relation
self.klass.interceptors[method.to_s].new(method.to_s, self)
else
super
end
end
end
ActiveRecord::Relation.send :include, Interceptor::RelationAdditions
Now, User.where('created_at > (?)', Time.current - 2.weeks).custom_interceptor
will apply all the scoping set up in the Interceptor
DSL on top of whatever query you build on the model.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.