Escopos de modelo de proxy rails dinamicamente para operações pioneiras

No JMI Labs, recentemente tivemos um modelo ActiveRecord com mais de 30 escopos usados ​​para filtrar dinamicamente nosso modelo. Como já estávamos fortemente investidos no Trailblazer , decidimos encontrar uma maneira de criar proxy dinamicamente para as operações pioneiras a fim de limpar nosso modelo e obter algumas das sutilezas oferecidas pelas operações Trailblazer (contratos, políticas, etc.).


Primeiro, desenvolvemos um escopo que atuaria como o método proxy para chamar escopos de filtro individuais (como filter_by_organism).

# Use meta programming to build a filter_by_* scope automatically.
# If a Isolate::FilterScope operation exists that matches the filter key,
# pass in the arguments for the filter_by_ method, and return the model
# of the operation that contains the ActiveRecord::Relation class.
scope
:filter_by_, proc{|filter_key, *arguments|
begin
operation
= "Isolate::FilterScope::#{filter_key.camelize}".constantize
next operation.(
current_scope
: all,
arguments
: arguments
).model
rescue NameError => e
if e.message == "uninitialized constant Isolate::FilterScope::#{filter_key.camelize}"
raise "Don't have Isolate::FilterScope::#{filter_key.camelize} operation defined."
else
raise e
end
end
}

Em seguida, tivemos que dizer ao nosso modelo para proxy de todos os escopos de filtro para o escopo do delegador acima. Isso foi feito com o seguinte código.

# Leverage the method missing function to automatically call the filter_by_
# scope to build the scope methods on the fly.
def self.method_missing(method_sym, *arguments, &block)
# the first argument is a Symbol, so you need to_s it if you want to pattern match
if method_sym.to_s =~ /^filter_by_(.*)$/
send
(:filter_by_, $1, *arguments)
else
super
end
end

Por fim, tivemos que dizer ao Rails que nosso modelo Isolate,, poderia responder aos filter_by_*escopos para que eles fossem procurados corretamente.

# We have to say that Isolates can respond to the dynamic :filter_by scope
def self.respond_to?(method_name, include_private = false)
method_name
.to_s.start_with?('filter_by_') || super
end

Agora podemos colocar as operações no módulo filter_scope conforme necessário …

# app/concepts/isolate/filter_scope/operations/age_range.rb
class Isolate < ActiveRecord::Base
module FilterScope

# Filter isolates based on age range
# => Isolate.filter_by_age_range({include: {min: 5, max: 65}})
# => Isolate.filter_by_age_range({exclude: {min: 5, max: 65}})
#
class AgeRange < Trailblazer::Operation

...
end
end
end

Achamos essa arquitetura útil para modelos com muitos métodos de escopo para ajudar a secar nosso código. Sinta-se à vontade para deixar suas ideias e sugestões de maneiras de melhorar este acritecutre!