Module: Slimak::Sluggable

Extended by:
ActiveSupport::Concern
Defined in:
lib/slimak/sluggable.rb

Overview

Sluggable concern for ActiveRecord models (also usable on plain Ruby objects).

Instance Method Summary collapse

Instance Method Details

#_slugObject



64
65
66
67
68
69
70
71
# File 'lib/slimak/sluggable.rb', line 64

def _slug
  col = self.class._slimak_slug_options[:column]
  if respond_to?(col) && !send(col).to_s.strip.empty?
    send(col).to_s
  else
    build_slug_string
  end
end

#assign_slug_column(slug_col, value) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/slimak/sluggable.rb', line 149

def assign_slug_column(slug_col, value)
  if respond_to?("#{slug_col}=")
    send("#{slug_col}=", value)
  elsif respond_to?(:write_attribute)
    write_attribute(slug_col, value)
  else
    (class << self; self; end).class_eval do
      attr_accessor slug_col unless method_defined?(slug_col)
    end
    instance_variable_set("@#{slug_col}", value)
  end
end

#build_slug_stringObject



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/slimak/sluggable.rb', line 86

def build_slug_string
  parts = self.class._slimak_slug_columns.map do |col|
    v = safe_read(col)
    next nil if v.nil?
    formatted = format_component(v.to_s, col)
    formatted unless formatted.to_s.empty?
  end.compact

  return "" if parts.empty?

  parameterize(parts.join(" "), self.class._slimak_slug_options[:separator])
end

#build_unique_slugObject



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/slimak/sluggable.rb', line 99

def build_unique_slug
  base = build_slug_string
  return base if base.to_s.strip.empty?
  return base unless defined?(ActiveRecord) && self.class.respond_to?(:unscoped)

  # Merge global options with model options so model options override global.
  opts = Slimak.config.to_hash.merge(self.class._slimak_slug_options || {})
  col = opts[:column]
  scope = opts[:scope]

  case opts[:conflict_strategy].to_sym
  when :random
    candidate = base.dup
    while slug_exists?(candidate, col, scope)
      suffix = SecureRandom.alphanumeric(opts[:random_suffix_length]).downcase
      candidate = [base, suffix].join(opts[:sequence_separator])
    end
    candidate
  else
    candidate = base.dup
    seq = 2
    while slug_exists?(candidate, col, scope)
      candidate = [base, seq].join(opts[:sequence_separator])
      seq += 1
    end
    candidate
  end
end

#format_component(str, column) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'lib/slimak/sluggable.rb', line 139

def format_component(str, column)
  s = str.dup
  s = ActiveSupport::Inflector.transliterate(s) if defined?(ActiveSupport::Inflector)
  s = s.strip
  limits = self.class._slimak_slug_column_limits || {}
  max = limits && limits[column.to_sym]
  s = s[0, max] if max && max > 0
  s.gsub(/[[:space:]]+/, " ")
end

#generate_slug_if_blankObject



73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/slimak/sluggable.rb', line 73

def generate_slug_if_blank
  return unless self.class._slimak_slug_columns.any?

  slug_col = self.class._slimak_slug_options[:column]
  if respond_to?(slug_col) && !send(slug_col).to_s.strip.empty?
    return
  end

  candidate = build_unique_slug
  assign_slug_column(slug_col, candidate)
  nil
end

#parameterize(str, separator = "-") ⇒ Object



162
163
164
165
166
167
168
169
# File 'lib/slimak/sluggable.rb', line 162

def parameterize(str, separator = "-")
  s = str.to_s
  begin
    s.parameterize(separator: separator)
  rescue ArgumentError
    s.parameterize(separator)
  end
end

#safe_read(name) ⇒ Object

Safe read helper



129
130
131
132
133
134
135
136
137
# File 'lib/slimak/sluggable.rb', line 129

def safe_read(name)
  if respond_to?(name)
    send(name)
  else
    nil
  end
rescue => _
  nil
end

#slug_exists?(candidate, col, scope) ⇒ Boolean

Returns:

  • (Boolean)


171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/slimak/sluggable.rb', line 171

def slug_exists?(candidate, col, scope)
  return false unless defined?(ActiveRecord) && self.class.respond_to?(:unscoped)
  rel = self.class.unscoped.where(col => candidate)
  if scope
    Array(scope).each do |sc|
      val = safe_read(sc)
      rel = rel.where(sc => val)
    end
  end
  if respond_to?(:persisted?) && persisted?
    rel = rel.where.not(id: id)
  end
  rel.exists?
end