Filterameter fornece filtros declarativos para controladores Rails para reduzir o código clichê e aumentar a legibilidade. Quantas vezes você viu (ou escreveu) essa ação do controlador?
def index
@films = Films . all
@films = @films . where ( name : params [ :name ] ) if params [ :name ]
@films = @films . joins ( :film_locations ) . merge ( FilmLocations . where ( location_id : params [ :location_id ] ) ) if params [ :location_id ]
@films = @films . directed_by ( params [ :director_id ] ) if params [ :director_id ]
@films = @films . written_by ( params [ :writer_id ] ) if params [ :writer_id ]
@films = @films . acted_by ( params [ :actor_id ] ) if params [ :actor_id ]
end
É um código redundante e um pouco complicado de escrever e manter. Sem falar no que o RuboCop vai dizer sobre isso. Não seria legal se você pudesse simplesmente declarar os filtros que o controlador aceita?
filter :name , partial : true
filter :location_id , association : :film_locations
filter :director_id , name : :directed_by
filter :writer_id , name : :written_by
filter :actor_id , name : :acted_by
def index
@films = build_query_from_filters
end
Simplifique e acelere o desenvolvimento de controladores Rails tornando os parâmetros de filtro declarativos com Filterameter.
Esta gem requer Rails 6.1+ e funciona com ActiveRecord.
Adicione esta linha ao Gemfile da sua aplicação:
gem 'filterameter'
E então execute:
$ bundle install
Ou instale você mesmo como:
$ gem install filterameter
Inclua o módulo Filterameter::DeclarativeFilters
no controlador para fornecer o filtro DSL. Ele pode ser incluído no ApplicationController
para disponibilizar a funcionalidade a todos os controladores ou pode ser combinado caso a caso.
filter :color
filter :size , validates : { inclusion : { in : %w[ Small Medium Large ] , allow_multiple_values : true } }
filter :brand_name , association : :brand , name : :name
filter :on_sale , association : :price , validates : [ { numericality : { greater_than : 0 } } ,
{ numericality : { less_than : 100 } } ]
Filtros sem opções podem ser declarados de uma só vez com filters
:
filters :color ,
:size ,
:name
As opções a seguir podem ser especificadas para cada filtro.
Se o nome do parâmetro for diferente do nome do atributo ou escopo, use o parâmetro name para especificar o nome do atributo ou escopo. Por exemplo, se o nome do atributo for current_status
, mas o filtro for exposto simplesmente como status
use o seguinte:
filter :status , name : :current_status
Esta opção também pode ser útil com filtros aninhados para que o parâmetro de consulta possa ser prefixado com o nome do modelo. Veja a opção association
para ver um exemplo.
Se o atributo ou escopo estiver aninhado, ele poderá ser referenciado nomeando a associação. Por exemplo, se o atributo manager_id estiver no registro do departamento de um funcionário, use o seguinte:
filter :manager_id , association : :department
O atributo ou escopo pode ser aninhado em mais de um nível. Declare o filtro com uma matriz especificando as associações em ordem. Por exemplo, se um funcionário pertencer a um departamento e um departamento pertencer a uma unidade de negócios, use o seguinte para consultar o nome da unidade de negócios:
filter :business_unit_name , name : :name , association : [ :department , :business_unit ]
Se uma associação for has_many
o método distinto será chamado na consulta.
Limitação: Se houver mais de uma associação para a mesma tabela e ambas as associações puderem fazer parte da consulta, não será possível usar um filtro aninhado diretamente. Em vez disso, crie um escopo que elimine a ambiguidade das associações e, em seguida, crie um filtro nesse escopo.
Se o valor do filtro precisar ser validado, use a opção validates
junto com as validações do ActiveModel. Aqui está um exemplo do validador de inclusão usado para restringir tamanhos:
filter :size , validates : { inclusion : { in : %w[ Small Medium Large ] } }
O validador inclusion
foi substituído para fornecer a opção adicional allow_multiple_values
. Quando verdadeiro, o valor pode ser um array e cada entrada no array será validada. Use isto quando o filtro puder especificar um ou mais valores.
filter :size , validates : { inclusion : { in : %w[ Small Medium Large ] , allow_multiple_values : true } }
Especifique a opção parcial se o filtro deve fazer uma pesquisa parcial (SQL's LIKE
). A opção parcial aceita um hash para especificar o comportamento da pesquisa. Aqui estão as opções disponíveis:
Existem dois atalhos: : a opção parcial pode ser declarada com true
, que usa apenas os padrões; ou a opção parcial pode ser declarada diretamente com a opção de correspondência, como partial: :from_start
.
filter :description , partial : true
filter :department_name , partial : :from_start
filter :reason , partial : { match : :dynamic , case_sensitive : true }
As opções match
definem onde você está pesquisando (que controla onde os curingas aparecem):
Especifique a opção de intervalo para ativar pesquisas por intervalos, valores mínimos ou valores máximos. (Tudo isso está incluído. Uma pesquisa por um valor mínimo de US$ 10,00 incluiria todos os itens com preço de US$ 10,00.)
Aqui estão as opções disponíveis:
Usar a opção range significa que, além do filtro de atributo, parâmetros de consulta mínimo e máximo também podem ser especificados. Os nomes dos parâmetros são o nome do atributo mais o sufixo _min ou _max .
filter :price , range : true
filter :approved_at , range : :min_only
filter :sale_price , range : :max_only
No primeiro exemplo, os parâmetros de consulta poderiam incluir price , price_min e price_max .
Por padrão, a maioria dos filtros são classificáveis. Para evitar que um filtro de atributo seja classificável, defina a opção como false.
filter :price , sortable : false
Os seguintes filtros não são classificáveis:
Para escopos que não aceitam argumentos, o filtro deve fornecer um booleano que indique se o escopo deve ou não ser invocado. Por exemplo, imagine um escopo chamado high_priority
com critérios que identificam registros de alta prioridade. O escopo seria invocado pelos parâmetros de consulta high_priority=true
.
Passar high_priority=false
não invocará o escopo. Isso facilita a inclusão de um filtro com uma interface de usuário de caixa de seleção.
Os escopos que aceitam argumentos devem ser escritos como métodos de classe, não como escopos embutidos. Por exemplo, imagine um escopo chamado recent
que usa uma data como argumento. Aqui está como isso pode ser:
def self . recent ( as_of_date )
where ( 'created_at > ?' , as_of_date )
end
Conforme observado acima, a maioria dos filtros de atributos são classificáveis por padrão. Se nenhum filtro tiver sido declarado para um atributo, a declaração sort
poderá ser usada. Use o mesmo name
e opções association
conforme necessário.
Por exemplo, a seguinte declaração poderia ser usada em um controlador de atividades para permitir que as atividades fossem classificadas por projeto criado em.
sort :project_created_at , name : :created_at , association : :project
Classificações sem opções podem ser declaradas de uma só vez com sorts
:
sorts :created_at ,
:updated_at ,
:description
Os escopos podem ser usados para classificação, mas devem ser declarados com sort
(ou sorts
). Por exemplo, se um modelo incluísse um escopo chamado by_created_at
você poderia adicionar o seguinte ao controlador para expô-lo.
sort :by_created_at
As opções de name
e association
também podem ser usadas. Por exemplo, se o escopo estivesse no modelo Projeto, ele também poderia ser usado em um controlador de atividade filho usando a opção association
:
sort :by_created_at , association : :project
Apenas associações singulares são válidas para classificação. Uma associação de coleção poderia retornar vários valores, tornando a classificação indeterminada.
Um escopo usado para classificação deve aceitar um único argumento. Será passado :asc
ou :desc
dependendo do parâmetro.
O escopo do exemplo acima pode ser definido da seguinte forma:
def self . by_created_at ( dir )
order ( created_at : dir )
end
Uma classificação padrão pode ser declarada usando default_sort
. O(s) argumento(s) deve(m) especificar uma ou mais das classificações declaradas ou filtros classificáveis por nome. Por padrão, a ordem é crescente. Se desejar ordem decrescente, você pode mapear o símbolo do nome da coluna para:desc.
default_sort updated_at : :desc , :description
Para fornecer resultados consistentes, uma classificação é sempre aplicada. Se nenhum padrão for especificado, ele usará a chave primária descendente.
Existem duas maneiras de aplicar os filtros e construir a consulta, dependendo de quanto controle e/ou visibilidade se deseja:
build_filtered_query
antes da açãobuild_query_from_filters
build_filtered_query
antes da ação Adicione o retorno de chamada antes da ação build_filtered_query
para ações do controlador que devem construir a consulta. Isso pode ser feito no ApplicationController
ou caso a caso.
Ao usar o retorno de chamada, o nome da variável é o nome do modelo pluralizado. Por exemplo, o modelo Photo usará a variável @photos
para armazenar a consulta. O nome da variável pode ser especificado explicitamente com filter_query_var_name
. Por exemplo, se a consulta estiver armazenada como @data
, use o seguinte:
filter_query_var_name :data
Além disso, o comando filter_model
utiliza um segundo parâmetro opcional para especificar o nome da variável. Tanto o modelo quanto o nome da variável podem ser especificados com este atalho. Por exemplo, para usar o modelo Picture e armazenar os resultados como @data
, use o seguinte:
filter_model 'Picture' , :data
No caminho feliz, o WidgetsController serve Widgets e pode filtrar por tamanho e cor. Esta é a aparência do controlador:
class WidgetsController < ApplicationController
include Filterameter :: DeclarativeFilters
before_action :build_filtered_query , only : :index
filter :size
filter :color
def index
render json : @widgets
end
end
build_query_from_filters
Para gerar a consulta manualmente, você pode chamar build_query_from_filters
diretamente em vez de usar o callback .
Aqui está o controlador Widgets novamente, desta vez construindo a consulta manualmente:
class WidgetsController < ApplicationController
include Filterameter :: DeclarativeFilters
filter :size
filter :color
def index
@widgets = build_query_from_filters
end
end
Este método opcionalmente utiliza uma consulta inicial. Se houvesse um controlador para Active Widgets que deveria retornar apenas widgets ativos, o seguinte poderia ser passado para o método como ponto de partida:
def index
@widgets = build_query_from_filters ( Widget . where ( active : true ) )
end
A consulta inicial também é um bom lugar para fornecer quaisquer inclusões para permitir o carregamento antecipado:
def index
@widgets = build_query_from_filters ( Widgets . includes ( :manufacturer ) )
end
Observe que a consulta inicial fornece o modelo, portanto o modelo não é consultado e a declaração model_name
não é necessária.
As convenções Rails são usadas para determinar o modelo do controlador. Por exemplo, o PhotosController cria uma consulta no modelo Photo. Se um controlador tiver namespace, o modelo será consultado primeiro sem o namespace e depois com o namespace.
Se as convenções não fornecerem o modelo correto , o modelo poderá ser nomeado explicitamente com o seguinte:
filter_model 'Picture'
Importante: Se a declaração filter_model
for usada, ela deverá estar antes de qualquer declaração de filtro ou classificação.
Existem três opções de configuração:
As opções de configuração podem ser definidas em um inicializador, um arquivo de ambiente ou em application.rb
.
As opções podem ser definidas diretamente...
Filterameter.configuration.action_on_undeclared_parameters = :log
...ou a configuração pode ser obtida:
Filterameter . configure do | config |
config . action_on_undeclared_parameters = :log
config . action_on_validation_failuer = :log
config . filter_key = :f
end
Ocorre quando o parâmetro filter contém chaves que não estão definidas. As ações válidas são :log
, :raise
e false
(não execute nenhuma ação). Por padrão, o desenvolvimento registrará, o teste aumentará e a produção não fará nada.
Ocorre quando um parâmetro de filtro falha na validação. As ações válidas são :log
, :raise
e false
(não execute nenhuma ação). Por padrão, o desenvolvimento registrará, o teste aumentará e a produção não fará nada.
Por padrão, os parâmetros de filtro estão aninhados na chave :filter
. Use esta configuração para substituir a chave.
Se os parâmetros do filtro NÃO estiverem aninhados, defina como falso. Fazer isso restringirá os parâmetros de filtro apenas àqueles que foram declarados, o que significa que os parâmetros não declarados serão ignorados (e a opção de configuração action_on_undeclared_parameters não entrará em ação).
As declarações podem ser testadas para cada controlador, detectando erros de digitação, escopos definidos incorretamente ou quaisquer outros problemas. O método declarations_validator
é adicionado a cada controlador, e um único teste de controlador pode ser adicionado para validar todas as declarações desse controlador.
Um teste RSpec pode ser assim:
expect ( WidgetsController . declarations_validator ) . to be_valid
No Minitest pode ficar assim:
validator = WidgetsController . declarations_validator
assert_predicate validator , :valid? , -> { validator . errors }
Os parâmetros do filtro são extraídos dos parâmetros do controlador, aninhados no filter
chave (por padrão; consulte Configuração para alterar a chave do filtro). Por exemplo, uma solicitação para widgets grandes e azuis pode ter os seguintes parâmetros de consulta na URL:
?filter[size]=large&filter[color]=blue
Em um formulário de pesquisa genérico, o auxiliar de formulário form_with
usa o scope
da opção que permite que os parâmetros sejam agrupados:
<%= form_with url: "/search", scope: :filter, method: :get do |form| %>
<%= form.label :size, "Size:" %>
<%= form.text_field :size %>
<%= form.label :color, "Color:" %>
<%= form.text_field :color %>
<%= form.submit "Search" %>
<% end %>
A classificação também está aninhada abaixo da chave de filtro:
/widgets?filter[sort]=size
Use uma matriz para passar várias classificações. A ordem dos parâmetros é a ordem em que as classificações serão aplicadas. Por exemplo, o seguinte classifica primeiro por tamanho e depois por cor:
/widgets?filter[sort]=size&filter[sort]=color
As classificações são crescentes por padrão, mas podem usar um prefixo que pode ser adicionado para controlar a classificação:
+
crescente (o padrão)-
descendentePor exemplo, as seguintes classificações por tamanho decrescente:
/widgets?filter[sort]=-size
Comentários, solicitações de recursos e alterações propostas são bem-vindos. Use o rastreador de problemas para feedback e solicitações de recursos. Para propor uma alteração diretamente, bifurque o repositório e abra uma solicitação pull. Fique de olho nas ações para ter certeza de que os testes e o Rubocop estão passando. Code Climate também é usado manualmente para avaliar a linha de código.
Para relatar um bug, use o rastreador de problemas e forneça as seguintes informações:
Serão concedidas estrelas douradas se você conseguir replicar o problema com um teste.
Os testes são escritos em RSpec e o aplicativo fictício usa um banco de dados docker. O script bin/start_db.sh
inicia e prepara o banco de dados de teste. É uma etapa única antes de executar os testes.
bin/start_db.rb
bundle exec rspec
Os testes também podem ser executados em todas as combinações Ruby e Rails usando avaliação. A instalação também é uma etapa única.
bundle exec appraisal install
bundle exec appraisal rspec
A gema está disponível como código aberto sob os termos da licença MIT.