lib/friendly_id/finder_methods.rb
module FriendlyId
module FinderMethods
# Finds a record using the given id.
#
# If the id is "unfriendly", it will call the original find method.
# If the id is a numeric string like '123' it will first look for a friendly
# id matching '123' and then fall back to looking for a record with the
# numeric id '123'.
#
# @param [Boolean] allow_nil (default: false)
# Use allow_nil: true if you'd like the finder to return nil instead of
# raising ActivRecord::RecordNotFound
#
# ### Example
#
# MyModel.friendly.find("bad-slug")
# #=> raise ActiveRecord::RecordNotFound
#
# MyModel.friendly.find("bad-slug", allow_nil: true)
# #=> nil
#
# Since FriendlyId 5.0, if the id is a nonnumeric string like '123-foo' it
# will *only* search by friendly id and not fall back to the regular find
# method.
#
# If you want to search only by the friendly id, use {#find_by_friendly_id}.
# @raise ActiveRecord::RecordNotFound
def find(*args, allow_nil: false)
id = args.first
return super(*args) if args.count != 1 || id.unfriendly_id?
first_by_friendly_id(id).tap { |result| return result unless result.nil? }
return super(*args) if potential_primary_key?(id)
raise_not_found_exception(id) unless allow_nil
rescue ActiveRecord::RecordNotFound => exception
raise exception unless allow_nil
end
# Returns true if a record with the given id exists.
def exists?(conditions = :none)
return super if conditions.unfriendly_id?
return true if exists_by_friendly_id?(conditions)
super
end
# Finds exclusively by the friendly id, completely bypassing original
# `find`.
# @raise ActiveRecord::RecordNotFound
def find_by_friendly_id(id)
first_by_friendly_id(id) or raise_not_found_exception(id)
end
def exists_by_friendly_id?(id)
where(friendly_id_config.query_field => parse_friendly_id(id)).exists?
end
private
def potential_primary_key?(id)
key_type = primary_key_type
# Hook for "ActiveModel::Type::Integer" instance.
key_type = key_type.type if key_type.respond_to?(:type)
case key_type
when :integer
begin
Integer(id, 10)
rescue
false
end
when :uuid
id.match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
else
true
end
end
def first_by_friendly_id(id)
find_by(friendly_id_config.query_field => parse_friendly_id(id))
end
# Parse the given value to make it suitable for use as a slug according to
# your application's rules.
#
# This method is not intended to be invoked directly; FriendlyId uses it
# internally to process a slug into string to use as a finder.
#
# However, if FriendlyId's default slug parsing doesn't suit your needs,
# you can override this method in your model class to control exactly how
# slugs are generated.
#
# ### Example
#
# class Person < ActiveRecord::Base
# extend FriendlyId
# friendly_id :name_and_location
#
# def name_and_location
# "#{name} from #{location}"
# end
#
# # Use default slug, but lower case
# # If `id` is "Jane-Doe" or "JANE-DOE", this finds data by "jane-doe"
# def parse_friendly_id(slug)
# super.downcase
# end
# end
#
# @param [#to_s] value The slug to be parsed.
# @return The parsed slug, which is not modified by default.
def parse_friendly_id(value)
value
end
def raise_not_found_exception(id)
message = "can't find record with friendly id: #{id.inspect}"
if ActiveRecord.version < Gem::Version.create("5.0")
raise ActiveRecord::RecordNotFound.new(message)
else
raise ActiveRecord::RecordNotFound.new(message, name, friendly_id_config.query_field, id)
end
end
end
end