Skip to content

Commit

Permalink
param filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffpeterson committed Nov 21, 2024
1 parent aa4dbcd commit acc1a6e
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 9 deletions.
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ PATH
specs:
cafe_car (0.1.0)
activerecord_where_assoc
chronic
haml-rails
image_processing
importmap-rails
Expand Down Expand Up @@ -112,6 +113,7 @@ GEM
brakeman (6.2.2)
racc
builder (3.3.0)
chronic (0.10.2)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crass (1.0.6)
Expand Down
1 change: 1 addition & 0 deletions cafe_car.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "turbo-rails"
spec.add_dependency "kaminari"
spec.add_dependency "pundit"
spec.add_dependency "chronic"
end
7 changes: 3 additions & 4 deletions lib/cafe_car/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def authorize!

def current_user = CafeCar[:Current].user

def filtered(scope) = scope
def sorted(scope)
scope.sorted(*params[:sort].presence)
end
Expand All @@ -101,9 +100,9 @@ def assign_attributes
object.assign_attributes(permitted_attributes(object))
end

def scope = policy_scope(model.all).try { sorted _1 }
.try { filtered _1 }
.try { paginated _1 }
def scope = policy_scope(model.all).then { sorted _1 }
.then { filtered _1 }
.then { paginated _1 }
def objects = instance_variable_get("@#{model_name.plural}")
def objects=(value)
instance_variable_set("@#{model_name.plural}", value)
Expand Down
18 changes: 17 additions & 1 deletion lib/cafe_car/controller/filtering.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
module CafeCar::Controller::Filtering
extend ActiveSupport::Concern

class_methods do
included do
helper_method :dot_params
end

private

def filtered(scope)
scope.query(dot_params[""])
end

def parsed_params
@parsed_params ||= params.reject {|k, *| k.include?('.') }.
merge(dot_params)
end

def dot_params
@dot_params ||= CafeCar::ParamParser.new(request.params).parsed
end
end
7 changes: 7 additions & 0 deletions lib/cafe_car/engine.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "chronic"
require "haml"
require "kaminari"
require "image_processing"
Expand Down Expand Up @@ -32,6 +33,12 @@ class Engine < ::Rails::Engine
end
end

initializer "cafe_car.chronic" do |app|
app.reloader.to_prepare do
Chronic.time_class = Time.zone
end
end

initializer "cafe_car.sqlite_regexp" do |app|
ActiveSupport.on_load :active_record_sqlite3adapter do
::Arel::Visitors::SQLite.include(Visitors::SQLite)
Expand Down
56 changes: 56 additions & 0 deletions lib/cafe_car/param_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class CafeCar::ParamParser
def initialize(params)
@params = params
end

def params = parsed

def parsed
@parsed ||= parse(@params)
end

def parse(params)
params.then { _1.try(:to_unsafe_h) || _1 }
.select {|k, *| k.include?('.') }
.map {|k, v| k.split('.').reverse.reduce(value(v)) { {_2 => _1} } }
.reduce({}) {|prms, p| prms.deep_merge(p, &method(:merge)) }
.with_indifferent_access
end

def merge(_key, a, b)
if a.is_a?(Array) || b.is_a?(Array)
[*Array.wrap(a), *Array.wrap(b)]
else
b
end
end

def value(v)
case v
when '""', "''"
''
when 'nil', ''
nil
when /[{}\[\]]/
JSON.parse(v)
when /,/
value v.split(',')
when /\.\./
a, b = v.split('..').map(&:presence).map { value(_1) }
a..b
when /\A(\d*\.)?\d+\z/
v.try(:to_f) || v
when /^\$(\w+)\.(\w+)$/
$1.constantize.arel_table[$2]
when Array
v.map { value(_1) }
when Hash
v.reject {|k, *| k.include?('.') }.
transform_values { value(_1) }.
merge(parse(v)).
tap {|h| h.merge!(h.delete('')) if h.key?('') }
else
v
end
end
end
23 changes: 19 additions & 4 deletions lib/cafe_car/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ def unscoped = QueryBuilder.new(@scope.unscoped)

def arel = @scope.arel_table

def parse_time(value)
Chronic.parse(value, guess: false, context: :past)
rescue NoMethodError
nil
end

def parse_value(key, value)
case column(key)&.type
when :datetime
parse_time(value) || value
else value
end
end

def update!(&)
scope = @scope.instance_exec(@scope, &)
@scope = scope if scope
Expand All @@ -23,16 +37,17 @@ def not!(&)
update! { _1.and(inverted) }
end

def column(name) = @scope.columns_hash[name.to_s]
def association?(name) = @scope.reflect_on_association(name).present?
def attribute?(name) = @scope.columns_hash[name.to_s].present?
def attribute?(name) = column(name).present?
def scope?(name) = name.intern.in? @scope.local_methods

def param!(key, value)
case key
when /^(.*)~$/
param!($1, Regexp.new(value))
when /^(.*)!$/
not! { param!($1, value) }
when /^(.*)~$/
param!($1, Regexp.new(value, Regexp::IGNORECASE))
when method(:association?)
association!(key, value)
when method(:attribute?)
Expand All @@ -48,7 +63,7 @@ def attribute!(key, value)
case [key, value]
in _, Regexp
@scope.where!(arel[key].matches_regexp(value.source, !value.casefold?))
else @scope.where!(key => value)
else @scope.where!(key => parse_value(key, value))
end
self
end
Expand Down

0 comments on commit acc1a6e

Please sign in to comment.