slugifiable 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.simplecov +35 -0
- data/AGENTS.md +5 -0
- data/Appraisals +20 -0
- data/CHANGELOG.md +15 -1
- data/CLAUDE.md +5 -0
- data/README.md +54 -3
- data/Rakefile +9 -1
- data/gemfiles/rails_7.2.gemfile +17 -0
- data/gemfiles/rails_8.0.gemfile +17 -0
- data/gemfiles/rails_8.1.gemfile +17 -0
- data/lib/slugifiable/model.rb +93 -19
- data/lib/slugifiable/version.rb +1 -1
- data/lib/slugifiable.rb +2 -0
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f376f3d8ed6b225b61c6bd96cf207d4fb90970a7e138bd0685571225fe750343
|
|
4
|
+
data.tar.gz: 8d77cb7631dd2e4b7eb7be4b15e1d2fd1d0b3bcb6ddfabfcf0c505d776d50ddd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae99e80bff43688ca9402bb2b09074a0df88c685be8974d4120b5772f648b77d583c0cae3631dbbaee711a6477f51706499337c08fa3d46cfa0ba8888b05f4d5
|
|
7
|
+
data.tar.gz: ec724965c4aed46aa09207ef3a2cc0e761466c6097efc871ff0190d05b2833a207ee6afeb6150ca04da970036f727946c25252c574fbde4f9200779634c51d7d
|
data/.simplecov
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SimpleCov configuration file (auto-loaded before test suite)
|
|
4
|
+
# This keeps test_helper.rb clean and follows best practices
|
|
5
|
+
|
|
6
|
+
SimpleCov.start do
|
|
7
|
+
# Use SimpleFormatter for terminal-only output (no HTML generation)
|
|
8
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
9
|
+
|
|
10
|
+
# Track coverage for the lib directory (gem source code)
|
|
11
|
+
add_filter "/test/"
|
|
12
|
+
|
|
13
|
+
# Track the lib and app directories
|
|
14
|
+
track_files "{lib,app}/**/*.rb"
|
|
15
|
+
|
|
16
|
+
# Enable branch coverage for more detailed metrics
|
|
17
|
+
enable_coverage :branch
|
|
18
|
+
|
|
19
|
+
# Set minimum coverage threshold to prevent coverage regression
|
|
20
|
+
minimum_coverage line: 90, branch: 90
|
|
21
|
+
|
|
22
|
+
# Disambiguate parallel test runs
|
|
23
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Print coverage summary to terminal after tests complete
|
|
27
|
+
SimpleCov.at_exit do
|
|
28
|
+
SimpleCov.result.format!
|
|
29
|
+
puts "\n" + "=" * 60
|
|
30
|
+
puts "COVERAGE SUMMARY"
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
33
|
+
puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
|
|
34
|
+
puts "=" * 60
|
|
35
|
+
end
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/Appraisals
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Note: Rails < 7.2 is not compatible with Ruby 3.4
|
|
4
|
+
# (Logger became a bundled gem in Ruby 3.4, and only Rails 7.2+ handles this)
|
|
5
|
+
# See: https://stdgems.org/logger/
|
|
6
|
+
|
|
7
|
+
# Test against Rails 7.2 (minimum version compatible with Ruby 3.4)
|
|
8
|
+
appraise "rails-7.2" do
|
|
9
|
+
gem "rails", "~> 7.2.0"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Test against Rails 8.0
|
|
13
|
+
appraise "rails-8.0" do
|
|
14
|
+
gem "rails", "~> 8.0.0"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Test against Rails 8.1 (latest)
|
|
18
|
+
appraise "rails-8.1" do
|
|
19
|
+
gem "rails", "~> 8.1.2"
|
|
20
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
## [0.2.0] - 2026-01-16
|
|
2
|
+
|
|
3
|
+
- Added a full Minitest test suite
|
|
4
|
+
- Fixed `respond_to?(:slug)` returning false for models using method_missing by implementing `respond_to_missing?`
|
|
5
|
+
- Fixed collision resolution generating identical suffixes by switching from id-based deterministic suffixes to SecureRandom-based random suffixes
|
|
6
|
+
- Fixed length parameter edge cases (zero, negative, very large values) by adding length validation and clamping via `normalize_length` method
|
|
7
|
+
- Fixed timestamp fallback to include random suffix for additional uniqueness guarantee
|
|
8
|
+
- Added length validation constants (`MAX_HEX_STRING_LENGTH`, `MAX_NUMBER_LENGTH`) to prevent invalid length values
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2024-03-21
|
|
11
|
+
|
|
12
|
+
- Slugs can be now generated based off methods that return a string, not just based off attributes
|
|
13
|
+
- Enhanced collision resolution strategy so that it doesn't get stuck in infinite loops
|
|
14
|
+
- Added comprehensive test suite
|
|
15
|
+
|
|
1
16
|
## [0.1.0] - 2024-10-30
|
|
2
17
|
|
|
3
18
|
- Initial release
|
|
4
|
-
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
# 🐌 `slugifiable` -
|
|
1
|
+
# 🐌 `slugifiable` - Generate SEO-optimized URL slugs
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/slugifiable)
|
|
3
|
+
[](https://badge.fury.io/rb/slugifiable) [](https://github.com/rameerez/slugifiable/actions)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=slugifiable)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=slugifiable)!
|
|
7
|
+
|
|
8
|
+
Ruby gem to automatically generate unique slugs for your Rails' model records, so you can expose SEO-friendly URLs.
|
|
6
9
|
|
|
7
10
|
Example:
|
|
8
11
|
```
|
|
@@ -77,6 +80,10 @@ Product.first.slug
|
|
|
77
80
|
|
|
78
81
|
If your model has a `slug` attribute in the database, `slugifiable` will automatically generate a slug for that model upon instance creation, and save it to the DB.
|
|
79
82
|
|
|
83
|
+
> [!IMPORTANT]
|
|
84
|
+
> Your `slug` attribute **SHOULD NOT** have `null: false` in the migration / database. If it does, `slugifiable` will not be able to save the slug to the database, and will raise an error like `ERROR: null value in column "slug" of relation "posts" violates not-null constraint (PG::NotNullViolation)`
|
|
85
|
+
> This is because records are created without a slug, and the slug is generated later.
|
|
86
|
+
|
|
80
87
|
If you're generating slugs based off the model `id`, you can also set a desired length:
|
|
81
88
|
```ruby
|
|
82
89
|
class Product < ApplicationRecord
|
|
@@ -157,12 +164,56 @@ Product.first.slug
|
|
|
157
164
|
=> "big-red-backpack"
|
|
158
165
|
```
|
|
159
166
|
|
|
167
|
+
You can also use instance methods to generate more complex slugs. This is useful when you need to combine multiple attributes:
|
|
168
|
+
```ruby
|
|
169
|
+
class Event < ApplicationRecord
|
|
170
|
+
include Slugifiable::Model
|
|
171
|
+
belongs_to :location
|
|
172
|
+
|
|
173
|
+
generate_slug_based_on :title_with_location
|
|
174
|
+
|
|
175
|
+
# The method can return any string - slugifiable will handle the parameterization
|
|
176
|
+
def title_with_location
|
|
177
|
+
if location.present?
|
|
178
|
+
"#{title} #{location.city} #{location.region}" # Returns raw string, slugifiable parameterizes it
|
|
179
|
+
else
|
|
180
|
+
title
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This will generate slugs like:
|
|
187
|
+
```ruby
|
|
188
|
+
Event.first.slug
|
|
189
|
+
=> "my-awesome-event-new-york" # Automatically parameterized
|
|
190
|
+
```
|
|
191
|
+
|
|
160
192
|
There may be collisions if two records share the same name – but slugs should be unique! To resolve this, when this happens, `slugifiable` will append a unique string at the end to make the slug unique:
|
|
161
193
|
```ruby
|
|
162
194
|
Product.first.slug
|
|
163
195
|
=> "big-red-backpack-321678"
|
|
164
196
|
```
|
|
165
197
|
|
|
198
|
+
## Testing
|
|
199
|
+
|
|
200
|
+
The gem includes a comprehensive test suite that covers:
|
|
201
|
+
|
|
202
|
+
- Default slug generation behavior
|
|
203
|
+
- Attribute-based slug generation
|
|
204
|
+
- Method-based slug generation (including private/protected methods)
|
|
205
|
+
- Collision resolution and uniqueness handling
|
|
206
|
+
- Special cases and edge conditions
|
|
207
|
+
|
|
208
|
+
To run the tests:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bundle install
|
|
212
|
+
bundle exec rake test
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The test suite uses SQLite3 in-memory database and requires no additional setup.
|
|
216
|
+
|
|
166
217
|
## Development
|
|
167
218
|
|
|
168
219
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: :test
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.0"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "rack-test"
|
|
13
|
+
gem "simplecov", require: false
|
|
14
|
+
gem "sqlite3", ">= 2.1"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.0.0"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "rack-test"
|
|
13
|
+
gem "simplecov", require: false
|
|
14
|
+
gem "sqlite3", ">= 2.1"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.1.2"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "rack-test"
|
|
13
|
+
gem "simplecov", require: false
|
|
14
|
+
gem "sqlite3", ">= 2.1"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
gemspec path: "../"
|
data/lib/slugifiable/model.rb
CHANGED
|
@@ -28,6 +28,15 @@ module Slugifiable
|
|
|
28
28
|
DEFAULT_SLUG_STRING_LENGTH = 11
|
|
29
29
|
DEFAULT_SLUG_NUMBER_LENGTH = 6
|
|
30
30
|
|
|
31
|
+
# SHA256 produces 64 hex characters
|
|
32
|
+
MAX_HEX_STRING_LENGTH = 64
|
|
33
|
+
# 10^18 fits safely in a 64-bit integer
|
|
34
|
+
MAX_NUMBER_LENGTH = 18
|
|
35
|
+
|
|
36
|
+
# Maximum number of attempts to generate a unique slug
|
|
37
|
+
# before falling back to timestamp-based suffix
|
|
38
|
+
MAX_SLUG_GENERATION_ATTEMPTS = 10
|
|
39
|
+
|
|
31
40
|
included do
|
|
32
41
|
after_create :set_slug
|
|
33
42
|
after_find :update_slug_if_nil
|
|
@@ -36,25 +45,32 @@ module Slugifiable
|
|
|
36
45
|
|
|
37
46
|
class_methods do
|
|
38
47
|
def generate_slug_based_on(strategy, options = {})
|
|
48
|
+
# Remove previous definition if it exists to avoid warnings
|
|
49
|
+
remove_method(:generate_slug_based_on) if method_defined?(:generate_slug_based_on)
|
|
50
|
+
|
|
51
|
+
# Define the method that returns the strategy
|
|
39
52
|
define_method(:generate_slug_based_on) do
|
|
40
53
|
[strategy, options]
|
|
41
54
|
end
|
|
42
55
|
end
|
|
43
|
-
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
def method_missing(missing_method, *args, &block)
|
|
47
|
-
if missing_method.to_s == "slug" && !
|
|
59
|
+
if missing_method.to_s == "slug" && !has_slug_method?
|
|
48
60
|
compute_slug
|
|
49
61
|
else
|
|
50
62
|
super
|
|
51
63
|
end
|
|
52
64
|
end
|
|
53
65
|
|
|
66
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
+
method_name.to_s == "slug" && !has_slug_method? || super
|
|
68
|
+
end
|
|
69
|
+
|
|
54
70
|
def compute_slug
|
|
55
71
|
strategy, options = determine_slug_generation_method
|
|
56
72
|
|
|
57
|
-
length = options[:length] if options.is_a?(Hash)
|
|
73
|
+
length = options[:length] if options.is_a?(Hash)
|
|
58
74
|
|
|
59
75
|
if strategy == :compute_slug_based_on_attribute
|
|
60
76
|
self.send(strategy, options)
|
|
@@ -64,29 +80,72 @@ module Slugifiable
|
|
|
64
80
|
end
|
|
65
81
|
|
|
66
82
|
def compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH)
|
|
67
|
-
length
|
|
83
|
+
length = normalize_length(length, DEFAULT_SLUG_STRING_LENGTH, MAX_HEX_STRING_LENGTH)
|
|
68
84
|
(Digest::SHA2.hexdigest self.id.to_s).first(length)
|
|
69
85
|
end
|
|
70
86
|
|
|
71
87
|
def compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
|
72
|
-
length
|
|
88
|
+
length = normalize_length(length, DEFAULT_SLUG_NUMBER_LENGTH, MAX_NUMBER_LENGTH)
|
|
73
89
|
generate_random_number_based_on_id_hex(length)
|
|
74
90
|
end
|
|
75
91
|
|
|
76
92
|
def compute_slug_based_on_attribute(attribute_name)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
# This method generates a slug from either:
|
|
94
|
+
# 1. A database column (e.g. generate_slug_based_on :title)
|
|
95
|
+
# 2. An instance method (e.g. generate_slug_based_on :title_with_location)
|
|
96
|
+
#
|
|
97
|
+
# Priority:
|
|
98
|
+
# - Database columns take precedence over methods with the same name
|
|
99
|
+
# - Falls back to methods if no matching column exists
|
|
100
|
+
# - Falls back to ID-based slug if neither exists
|
|
101
|
+
#
|
|
102
|
+
# Flow:
|
|
103
|
+
# 1. Check if source exists (DB column first, then method)
|
|
104
|
+
# 2. Get raw value
|
|
105
|
+
# 3. Parameterize (convert "My Title" -> "my-title")
|
|
106
|
+
# 4. Ensure uniqueness
|
|
107
|
+
# 5. Fallback to random number if anything fails
|
|
108
|
+
|
|
109
|
+
# First check if we can get a value from the database
|
|
110
|
+
has_attribute = self.attributes.include?(attribute_name.to_s)
|
|
111
|
+
|
|
112
|
+
# Only check for methods if no DB attribute exists
|
|
113
|
+
# We check all method types to be thorough
|
|
114
|
+
responds_to_method = !has_attribute && (
|
|
115
|
+
self.class.method_defined?(attribute_name) ||
|
|
116
|
+
self.class.private_method_defined?(attribute_name) ||
|
|
117
|
+
self.class.protected_method_defined?(attribute_name)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# If we can't get a value from either source, fallback to using the record's ID
|
|
121
|
+
return compute_slug_as_string unless has_attribute || responds_to_method
|
|
122
|
+
|
|
123
|
+
# Get and clean the raw value (e.g. " My Title " -> "My Title")
|
|
124
|
+
# Works for both DB attributes and methods thanks to Ruby's send
|
|
125
|
+
raw_value = self.send(attribute_name)
|
|
126
|
+
return generate_random_number_based_on_id_hex if raw_value.nil?
|
|
127
|
+
|
|
128
|
+
# Convert to URL-friendly format
|
|
129
|
+
# e.g. "My Title" -> "my-title"
|
|
130
|
+
base_slug = raw_value.to_s.strip.parameterize
|
|
131
|
+
return generate_random_number_based_on_id_hex if base_slug.blank?
|
|
132
|
+
|
|
133
|
+
# Handle duplicate slugs by adding a random suffix if needed
|
|
134
|
+
# e.g. "my-title" -> "my-title-123456"
|
|
82
135
|
unique_slug = generate_unique_slug(base_slug)
|
|
83
136
|
unique_slug.presence || generate_random_number_based_on_id_hex
|
|
84
137
|
end
|
|
85
138
|
|
|
86
139
|
private
|
|
87
140
|
|
|
141
|
+
def normalize_length(length, default, max)
|
|
142
|
+
length = length.to_i
|
|
143
|
+
return default if length <= 0
|
|
144
|
+
[length, max].min
|
|
145
|
+
end
|
|
146
|
+
|
|
88
147
|
def generate_random_number_based_on_id_hex(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
|
89
|
-
length
|
|
148
|
+
length = normalize_length(length, DEFAULT_SLUG_NUMBER_LENGTH, MAX_NUMBER_LENGTH)
|
|
90
149
|
((Digest::SHA2.hexdigest(id.to_s)).hex % (10 ** length))
|
|
91
150
|
end
|
|
92
151
|
|
|
@@ -96,13 +155,23 @@ module Slugifiable
|
|
|
96
155
|
return slug_candidate unless slug_persisted?
|
|
97
156
|
|
|
98
157
|
# Collision resolution logic:
|
|
158
|
+
# Try up to MAX_SLUG_GENERATION_ATTEMPTS times with random suffixes
|
|
159
|
+
# This prevents infinite loops while still giving us good odds
|
|
160
|
+
# of finding a unique slug
|
|
161
|
+
attempts = 0
|
|
162
|
+
|
|
163
|
+
while self.class.exists?(slug: slug_candidate) && attempts < MAX_SLUG_GENERATION_ATTEMPTS
|
|
164
|
+
attempts += 1
|
|
165
|
+
# Use SecureRandom for truly random suffixes during collision resolution
|
|
166
|
+
# This ensures each attempt tries a different suffix
|
|
167
|
+
random_suffix = SecureRandom.random_number(10 ** DEFAULT_SLUG_NUMBER_LENGTH)
|
|
168
|
+
slug_candidate = "#{base_slug}-#{random_suffix}"
|
|
169
|
+
end
|
|
99
170
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# slug_candidate = "#{base_slug}-#{count}"
|
|
105
|
-
slug_candidate = "#{base_slug}-#{compute_slug_as_number}"
|
|
171
|
+
# If we couldn't find a unique slug after MAX_SLUG_GENERATION_ATTEMPTS,
|
|
172
|
+
# append timestamp + random to ensure uniqueness
|
|
173
|
+
if attempts == MAX_SLUG_GENERATION_ATTEMPTS
|
|
174
|
+
slug_candidate = "#{base_slug}-#{Time.current.to_i}-#{SecureRandom.random_number(1000)}"
|
|
106
175
|
end
|
|
107
176
|
|
|
108
177
|
slug_candidate
|
|
@@ -133,7 +202,7 @@ module Slugifiable
|
|
|
133
202
|
return [DEFAULT_SLUG_GENERATION_STRATEGY, options]
|
|
134
203
|
end
|
|
135
204
|
elsif strategy.key?(:attribute)
|
|
136
|
-
return [:compute_slug_based_on_attribute, strategy[:attribute]
|
|
205
|
+
return [:compute_slug_based_on_attribute, strategy[:attribute]]
|
|
137
206
|
end
|
|
138
207
|
end
|
|
139
208
|
|
|
@@ -141,7 +210,12 @@ module Slugifiable
|
|
|
141
210
|
end
|
|
142
211
|
|
|
143
212
|
def slug_persisted?
|
|
144
|
-
|
|
213
|
+
has_slug_method? && self.attributes.include?("slug")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def has_slug_method?
|
|
217
|
+
# Check if slug method exists from ActiveRecord (not from method_missing)
|
|
218
|
+
self.class.method_defined?(:slug) || self.class.private_method_defined?(:slug)
|
|
145
219
|
end
|
|
146
220
|
|
|
147
221
|
def set_slug
|
data/lib/slugifiable/version.rb
CHANGED
data/lib/slugifiable.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slugifiable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-01-16 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -33,10 +32,17 @@ executables: []
|
|
|
33
32
|
extensions: []
|
|
34
33
|
extra_rdoc_files: []
|
|
35
34
|
files:
|
|
35
|
+
- ".simplecov"
|
|
36
|
+
- AGENTS.md
|
|
37
|
+
- Appraisals
|
|
36
38
|
- CHANGELOG.md
|
|
39
|
+
- CLAUDE.md
|
|
37
40
|
- LICENSE.txt
|
|
38
41
|
- README.md
|
|
39
42
|
- Rakefile
|
|
43
|
+
- gemfiles/rails_7.2.gemfile
|
|
44
|
+
- gemfiles/rails_8.0.gemfile
|
|
45
|
+
- gemfiles/rails_8.1.gemfile
|
|
40
46
|
- lib/slugifiable.rb
|
|
41
47
|
- lib/slugifiable/model.rb
|
|
42
48
|
- lib/slugifiable/version.rb
|
|
@@ -49,7 +55,6 @@ metadata:
|
|
|
49
55
|
homepage_uri: https://github.com/rameerez/slugifiable
|
|
50
56
|
source_code_uri: https://github.com/rameerez/slugifiable
|
|
51
57
|
changelog_uri: https://github.com/rameerez/slugifiable/blob/main/CHANGELOG.md
|
|
52
|
-
post_install_message:
|
|
53
58
|
rdoc_options: []
|
|
54
59
|
require_paths:
|
|
55
60
|
- lib
|
|
@@ -64,8 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
64
69
|
- !ruby/object:Gem::Version
|
|
65
70
|
version: '0'
|
|
66
71
|
requirements: []
|
|
67
|
-
rubygems_version: 3.
|
|
68
|
-
signing_key:
|
|
72
|
+
rubygems_version: 3.6.2
|
|
69
73
|
specification_version: 4
|
|
70
74
|
summary: Easily generate unique and SEO optimized slugs for your Rails ActiveRecord
|
|
71
75
|
model records, based on any attribute
|