searchable: factor out metatag value parser.
Factor out the code that parses metatag values (e.g. `score:>5`) and search URL params (e.g. `search[score]=>5`) into a RangeParser class. Also fix a bug where, if the `search[order]=custom` param was used without a `search[id]` param, an exception would be raised. Fix another bug where if an invalid `search[id]` was provided, then the custom order would be ignored and the search would be returned with the default order instead. Now if you use `search[order]=custom` without a valid `search[id]` param, the search will return no results.
This commit is contained in:
@@ -12,8 +12,6 @@ require "strscan"
|
||||
class PostQueryBuilder
|
||||
extend Memoist
|
||||
|
||||
class ParseError < StandardError; end
|
||||
|
||||
# How many tags a `blah*` search should match.
|
||||
MAX_WILDCARD_TAGS = 100
|
||||
|
||||
@@ -269,7 +267,13 @@ class PostQueryBuilder
|
||||
elsif post_query.has_metatag?(:date, :age) && post_query.find_metatag(:order).in?(["id_desc", nil])
|
||||
relation = search_order(relation, "created_at_desc")
|
||||
elsif post_query.find_metatag(:order) == "custom"
|
||||
relation = search_order_custom(relation, post_query.select_metatags(:id).map(&:value))
|
||||
ids = post_query.select_metatags(:id).map(&:value)
|
||||
|
||||
if ids.size == 1
|
||||
relation = relation.order_custom(ids.first)
|
||||
else
|
||||
relation = relation.none
|
||||
end
|
||||
elsif post_query.has_metatag?(:ordfav)
|
||||
# no-op
|
||||
else
|
||||
@@ -491,141 +495,4 @@ class PostQueryBuilder
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
def search_order_custom(relation, id_metatags)
|
||||
return relation.none unless id_metatags.present? && id_metatags.size == 1
|
||||
|
||||
operator, ids = PostQueryBuilder.parse_range(id_metatags.first, :integer)
|
||||
return relation.none unless operator == :in
|
||||
|
||||
relation.in_order_of(:id, ids)
|
||||
end
|
||||
|
||||
concerning :ParseMethods do
|
||||
class_methods do
|
||||
# Parse a simple string value into a Ruby type.
|
||||
# @param string [String] the value to parse
|
||||
# @param type [Symbol] the value's type
|
||||
# @return [Object] the parsed value
|
||||
def parse_cast(string, type)
|
||||
case type
|
||||
when :enum
|
||||
string.downcase
|
||||
|
||||
when :integer
|
||||
Integer(string) # raises ArgumentError if string is invalid
|
||||
|
||||
when :float
|
||||
Float(string) # raises ArgumentError if string is invalid
|
||||
|
||||
when :md5
|
||||
raise ParseError, "#{string} is not a valid MD5" unless string.match?(/\A[0-9a-fA-F]{32}\z/)
|
||||
string.downcase
|
||||
|
||||
when :date, :datetime
|
||||
date = Time.zone.parse(string)
|
||||
raise ParseError, "#{string} is not a valid date" if date.nil?
|
||||
date
|
||||
|
||||
when :age
|
||||
DurationParser.parse(string).ago
|
||||
|
||||
when :interval
|
||||
DurationParser.parse(string)
|
||||
|
||||
when :ratio
|
||||
string = string.tr(":", "/") # "2:3" => "2/3"
|
||||
Rational(string).to_f.round(2) # raises ArgumentError or ZeroDivisionError if string is invalid
|
||||
|
||||
when :filesize
|
||||
raise ParseError, "#{string} is not a valid filesize" unless string =~ /\A(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?\Z/
|
||||
|
||||
size = Float($1)
|
||||
unit = $2
|
||||
|
||||
conversion_factor = case unit
|
||||
when /m/i
|
||||
1024 * 1024
|
||||
when /k/i
|
||||
1024
|
||||
else
|
||||
1
|
||||
end
|
||||
|
||||
(size * conversion_factor).to_i
|
||||
|
||||
else
|
||||
raise NotImplementedError, "unrecognized type #{type} for #{string}"
|
||||
end
|
||||
|
||||
rescue ArgumentError, ZeroDivisionError => e
|
||||
raise ParseError, e.message
|
||||
end
|
||||
|
||||
def parse_metatag_value(string, type)
|
||||
if type == :enum
|
||||
[:in, string.split(/[, ]+/).map { |x| parse_cast(x, type) }]
|
||||
else
|
||||
parse_range(string, type)
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a metatag range value of the given type. For example: 1..10.
|
||||
# @param string [String] the metatag value
|
||||
# @param type [Symbol] the value's type
|
||||
def parse_range(string, type)
|
||||
range = case string
|
||||
when /\A(.+?)\.\.\.(.+)/ # A...B
|
||||
lo, hi = [parse_cast($1, type), parse_cast($2, type)].sort
|
||||
[:between, (lo...hi)]
|
||||
when /\A(.+?)\.\.(.+)/
|
||||
lo, hi = [parse_cast($1, type), parse_cast($2, type)].sort
|
||||
[:between, (lo..hi)]
|
||||
when /\A<=(.+)/, /\A\.\.(.+)/
|
||||
[:lteq, parse_cast($1, type)]
|
||||
when /\A<(.+)/
|
||||
[:lt, parse_cast($1, type)]
|
||||
when /\A>=(.+)/, /\A(.+)\.\.\Z/
|
||||
[:gteq, parse_cast($1, type)]
|
||||
when /\A>(.+)/
|
||||
[:gt, parse_cast($1, type)]
|
||||
when /[, ]/
|
||||
[:in, string.split(/[, ]+/).map {|x| parse_cast(x, type)}]
|
||||
when "any"
|
||||
[:not_eq, nil]
|
||||
when "none"
|
||||
[:eq, nil]
|
||||
else
|
||||
# add a 5% tolerance for float and filesize values
|
||||
if type == :float || (type == :filesize && string =~ /[km]b?\z/i)
|
||||
value = parse_cast(string, type)
|
||||
[:between, (value * 0.95..value * 1.05)]
|
||||
elsif type.in?([:date, :age])
|
||||
value = parse_cast(string, type)
|
||||
[:between, (value.beginning_of_day..value.end_of_day)]
|
||||
else
|
||||
[:eq, parse_cast(string, type)]
|
||||
end
|
||||
end
|
||||
|
||||
range = reverse_range(range) if type == :age
|
||||
range
|
||||
end
|
||||
|
||||
def reverse_range(range)
|
||||
case range
|
||||
in [:lteq, value]
|
||||
[:gteq, value]
|
||||
in [:lt, value]
|
||||
[:gt, value]
|
||||
in [:gteq, value]
|
||||
[:lteq, value]
|
||||
in [:gt, value]
|
||||
[:lt, value]
|
||||
else
|
||||
range
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user