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?
ISSUES:
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‘smethod_missingafter all, it didn’t turn out too ugly. Here’s the full process, from beginning to end.My gem defines an
Interceptorclass, intended to be a DSL that developers may subclass. This object takes in somerootARel, from aModelor aRelation, and manipulates the query further before rendering.Implemented:
Then I give models the
has_interceptormethod that defines new interceptors and builds aninterceptorsmapping:Implemented:
With that alone, you can call User.interceptor and build an
Interceptorwith a clean query as the root for all interceptor query manipulation. However, with a little more effort, we can extendActiveRecord::Relationso that you can call interceptor methods as an endpoint in a chain of scopes:Now,
User.where('created_at > (?)', Time.current - 2.weeks).custom_interceptorwill apply all the scoping set up in theInterceptorDSL on top of whatever query you build on the model.