app/models/concerns/slug.rb
# frozen_string_literal: true
# The Slug module allows a model to have a slug for routing. To use it in a
# model, which induces the slug from the +name+ attribute, the model needs to
# call +slug_base :name+ (required).
#
# It can optionally call +slug_condition :condition_method+ which determines
# whether or not to change the slug.
#
# If the condition holds, the slug gets set/updated in a before_save hook.
#
# At the end of the update, the optional +slug_postprocess+ lambda is called,
# which expects one argument (the preset slug string). This method must return
# the slug as it is supposed to be saved.
#
# During the validations, the format of the slug is checked against the optional
# +slug_format+ regular expression. If no format is given, a default format is
# used.
#
#
# The model must have a `slug` column in the database.
#
# Usage:
# class MyModel
# include Slug
#
# slug_base :name
#
# # Optional: Specify when the slug gets set/updated.
# slug_condition :new?
# # OR
# slug_condition ->() { name.changed? }
#
# # Optional: Add some post-processing to set the slug.
# slug_postprocess ->(slug) { slug.upcase }
#
# # Optional: Specify the validation format of the slug.
# slug_format /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/
# end
module Slug
extend ActiveSupport::Concern
DEFAULT_SLUG_FORMAT = /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/
class_methods do
def slug_base(attribute)
@slug_base = attribute
end
def slug_condition(method)
@slug_condition = method
end
def slug_postprocess(callable)
@slug_postprocess = callable
end
def slug_format(format)
@slug_format = format
end
def inherited(subclass)
%w(@slug_base @slug_condition @slug_postprocess @slug_format).
each do |instance_var|
instance_var_value = instance_variable_get(instance_var)
subclass.instance_variable_set(instance_var, instance_var_value)
end
super
end
end
def self.sluggify(string)
string&.
downcase&.
gsub(/[*.=]/,
'*' => 'Star',
'=' => 'Eq')&.
parameterize(preserve_case: true)&.
gsub(/\s/, '_')
end
def to_param
slug
end
def before_validation
set_slug if set_slug?
super
end
def validate
if set_slug?
validates_presence slug_base
validates_unique :slug
validates_format(slug_format, :slug)
end
super
validate_move_slug_errors_to_base_errors
end
private
def set_slug?
if slug_condition.nil?
# :nocov:
# This is not yet used
true
# :nocov:
elsif slug_condition.respond_to?(:call)
# The proc/block is defined on the class, but must be executed on this
# instance. `instance_exec` changes the context.
# :nocov:
# This is not yet used
instance_exec(&slug_condition)
# :nocov:
else
send(slug_condition)
end
end
def set_slug
self.slug = Slug.sluggify(send(slug_base))
self.slug = do_slug_postprocess(slug)
end
def do_slug_postprocess(slug)
instance_exec(slug, &slug_postprocess)
end
def slug_base
self.class.instance_variable_get(:'@slug_base')
end
def slug_condition
self.class.instance_variable_get(:'@slug_condition')
end
def slug_postprocess
identity = ->(x) { x }
self.class.instance_variable_get(:'@slug_postprocess') || identity
end
def slug_format
self.class.instance_variable_get(:'@slug_format') || DEFAULT_SLUG_FORMAT
end
def validate_move_slug_errors_to_base_errors
return unless errors[:slug].any?
errors[slug_base] += errors.delete(:slug)
errors[slug_base].uniq!
end
end