class Ransack::Search
def add_scope(key, args)
def add_scope(key, args) sanitized_args = @context.sanitize_scope_args(key, args) if @context.scope_arity(key) == 1 @scope_args[key] = args.is_a?(Array) ? args[0] : args else @scope_args[key] = args.is_a?(Array) ? sanitized_args : args end @context.chain_scope(key, sanitized_args) end
def add_sort(sort)
same as an unknown attribute in a condition (#1427); otherwise it is
scope produces no ORDER BY. Under a strict search that is an error, the
A sort that names neither a sortable attribute nor a `sort_by_
def add_sort(sort) if @context.strict_conditions? && !sort.valid? && !sort_scope?(sort) raise InvalidSearchError, "Invalid sort term #{sort.name}" end self.sorts << sort end
def blank_advanced_condition?(condition)
def blank_advanced_condition?(condition) return false unless condition.is_a?(Hash) values = condition[:v] || condition['v'] case values when Array then values.all? { |v| blank_condition_value?(unwrap_value(v)) } when Hash then values.all? { |_k, v| blank_condition_value?(unwrap_value(v)) } end end
def blank_condition_value?(value)
an explicit nil inside an array is kept so `name_in: [nil]` still reaches
blank value is a value to search for. `false` is always a real value, and
field was left empty"; with it off, only nil is treated that way and a
`ignore_blank_values` on (the default) a blank value means "this form
True when a condition's value should be dropped before building. With
def blank_condition_value?(value) if Ransack.options[:ignore_blank_values] [*value].all? { |i| i.blank? && i != false && !i.nil? } else value.nil? || (value.is_a?(Array) && !value.empty? && value.all?(&:nil?)) end end
def build(params)
def build(params) collapse_multiparameter_attributes!(params).each do |key, value| if ['s'.freeze, 'sorts'.freeze].freeze.include?(key) send("#{key}=", value) elsif @context.ransackable_scope?(key, @context.object) add_scope(key, value) elsif base.attribute_method?(key) base.send("#{key}=", value) elsif !Ransack.options[:ignore_unknown_conditions] || !@ignore_unknown_conditions raise InvalidSearchError, "Invalid search term #{key}" end end self end
def build_sort(opts = {})
def build_sort(opts = {}) new_sort(opts).tap do |sort| self.sorts << sort end end
def collapse_multiparameter_attributes!(attrs)
on: no position (`created_at(`), a position outside 1 to the limit,
keys are untrusted, so a malformed one is dropped rather than raised
array of cast values, the way Active Record does for form input. The
Folds `created_at(1i)`, `created_at(2i)`, ... into `created_at` as an
def collapse_multiparameter_attributes!(attrs) attrs.keys.each do |k| if k.include?(Constants::LEFT_PARENTHESIS) real_attribute, position = k.split(/\(|\)/) value = attrs.delete(k) next if position.nil? cast = Constants::A_S_I.include?(position.last) ? position.last : nil position = position.to_i - 1 next if position < 0 || position >= MULTIPARAMETER_POSITION_LIMIT attrs[real_attribute] ||= [] next unless attrs[real_attribute].is_a?(Array) attrs[real_attribute][position] = if cast if value.blank? && cast == Constants::I nil else value.send("to_#{cast}") end else value end elsif Hash === attrs[k] collapse_multiparameter_attributes!(attrs[k]) end end attrs end
def fetch_either(hash, short, long)
Params are not yet indifferent-access here, so look under both spellings
def fetch_either(hash, short, long) hash[short] || hash[short.to_s] || hash[long] || hash[long.to_s] end
def initialize(object, params = {}, options = {})
def initialize(object, params = {}, options = {}) strip_whitespace = options.fetch(:strip_whitespace, Ransack.options[:strip_whitespace]) # Strong parameters carry whether the controller permitted them. Read # that before the unwrap below discards it: a model with no allowlist # of its own lets a permitted controller be the boundary, and a plain # Hash never counts (#1403). permitted = params.respond_to?(:permitted?) && params.permitted? params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h) if params.is_a? Hash # deep_transform_values rebuilds every nested hash and array, which # also gives the pruning below a private copy to edit — the caller's # params come back untouched. Values nested inside `g:` groupings and # `c:` conditions are stripped too, not just the top level. params = params.deep_transform_values { |v| v.is_a?(String) && strip_whitespace ? v.strip : v } params.delete_if { |_k, v| blank_condition_value?(v) } prune_blank_advanced_conditions!(params) else params = {} end @context = options[:context] || Context.for(object, options) @context.auth_object = options[:auth_object] @context.permitted = permitted @context.ignore_unknown_conditions = options[:ignore_unknown_conditions] @context.null_sentinel = options[:null_sentinel] @base = Nodes::Grouping.new( @context, options[:grouping] || Constants::AND ) @scope_args = {} @sorts ||= [] @ignore_unknown_conditions = options[:ignore_unknown_conditions] == false ? false : true build(params.with_indifferent_access) end
def inspect
def inspect details = [ [:class, klass.name], ([:scope, @scope_args] if @scope_args.present?), [:base, base.inspect] ] .compact .map { |d| d.join(': '.freeze) } .join(', '.freeze) "Ransack::Search<#{details}>" end
def method_missing(method_id, *args)
A scope wins over an attribute of the same name, as it does when the
def method_missing(method_id, *args) method_name = method_id.to_s getter_name = method_name.sub(/=$/, ''.freeze) if @context.ransackable_scope?(getter_name, @context.object) if method_name =~ /=$/ add_scope getter_name, args.size == 1 ? args.first : args else @scope_args[method_name] end elsif base.attribute_method?(getter_name) base.send(method_id, *args) else super end end
def new_sort(opts = {})
def new_sort(opts = {}) Nodes::Sort.new(@context).build(opts) end
def prune_blank_advanced_conditions!(node)
an empty value still builds its attribute and contributes a join, giving a
form, so the filter above never sees them. Left in place, a condition with
The low-level `c:` API nests its values a level deeper than the shorthand
def prune_blank_advanced_conditions!(node) return unless node.is_a?(Hash) conditions = fetch_either(node, :c, :conditions) case conditions when Array then conditions.delete_if { |c| blank_advanced_condition?(c) } when Hash then conditions.delete_if { |_k, c| blank_advanced_condition?(c) } end # Groupings nest to any depth and carry their own conditions, so descend # into each of them too. groupings = fetch_either(node, :g, :groupings) groupings = groupings.values if groupings.is_a?(Hash) Array(groupings).each { |grouping| prune_blank_advanced_conditions!(grouping) } end
def respond_to_missing?(method_id, include_private = false)
before calling. Mirror method_missing so a search_form_with field is
says it responds to the reader; `form_with` in particular checks
Rails' form helpers only read a field's value back when the object
def respond_to_missing?(method_id, include_private = false) getter_name = method_id.to_s.sub(/=$/, ''.freeze) base.attribute_method?(getter_name) || @context.ransackable_scope?(getter_name, @context.object) || super end
def result(opts = {})
def result(opts = {}) @context.evaluate(self, opts) end
def sort_scope?(sort)
def sort_scope?(sort) @context.object.respond_to?(:"sort_by_#{sort.name}_#{sort.dir}") end
def sorts
def sorts @sorts end
def sorts=(args)
Replaces the sorts, so `search.sorts = []` clears them. Before 6.0 each
def sorts=(args) @sorts = [] unless args.is_a?(String) case args when Array args.each do |sort| if sort.kind_of? Hash sort = Nodes::Sort.new(@context).build(sort) else sort = Nodes::Sort.extract(@context, sort) end add_sort(sort) if sort end when Hash args.each do |index, attrs| add_sort(Nodes::Sort.new(@context).build(attrs)) end when String self.sorts = [args] else raise InvalidSearchError, "Invalid argument (#{args.class}) supplied to sorts=" end end
def unwrap_value(value)
for, and `value[:value] || value['value']` read it as nil, which the
opened by key rather than by truthiness: `false` is a value to search
envelope, the same two forms `Condition#values=` accepts. The envelope is
Values in the `c:` API may be given bare or wrapped in a `{ value: ... }`
def unwrap_value(value) return value unless value.is_a?(Hash) value.key?(:value) ? value[:value] : value['value'] end