slugifiable 0.1.1 → 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 +7 -4
- 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 +31 -9
- data/lib/slugifiable/version.rb +1 -1
- data/lib/slugifiable.rb +2 -0
- metadata +9 -2
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
|
```
|
|
@@ -183,7 +186,7 @@ end
|
|
|
183
186
|
This will generate slugs like:
|
|
184
187
|
```ruby
|
|
185
188
|
Event.first.slug
|
|
186
|
-
=> "my-awesome-event-new-york
|
|
189
|
+
=> "my-awesome-event-new-york" # Automatically parameterized
|
|
187
190
|
```
|
|
188
191
|
|
|
189
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:
|
|
@@ -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,11 @@ 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
|
+
|
|
31
36
|
# Maximum number of attempts to generate a unique slug
|
|
32
37
|
# before falling back to timestamp-based suffix
|
|
33
38
|
MAX_SLUG_GENERATION_ATTEMPTS = 10
|
|
@@ -51,13 +56,17 @@ module Slugifiable
|
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def method_missing(missing_method, *args, &block)
|
|
54
|
-
if missing_method.to_s == "slug" && !
|
|
59
|
+
if missing_method.to_s == "slug" && !has_slug_method?
|
|
55
60
|
compute_slug
|
|
56
61
|
else
|
|
57
62
|
super
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
+
method_name.to_s == "slug" && !has_slug_method? || super
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
def compute_slug
|
|
62
71
|
strategy, options = determine_slug_generation_method
|
|
63
72
|
|
|
@@ -71,12 +80,12 @@ module Slugifiable
|
|
|
71
80
|
end
|
|
72
81
|
|
|
73
82
|
def compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH)
|
|
74
|
-
length
|
|
83
|
+
length = normalize_length(length, DEFAULT_SLUG_STRING_LENGTH, MAX_HEX_STRING_LENGTH)
|
|
75
84
|
(Digest::SHA2.hexdigest self.id.to_s).first(length)
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
def compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
|
79
|
-
length
|
|
88
|
+
length = normalize_length(length, DEFAULT_SLUG_NUMBER_LENGTH, MAX_NUMBER_LENGTH)
|
|
80
89
|
generate_random_number_based_on_id_hex(length)
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -129,8 +138,14 @@ module Slugifiable
|
|
|
129
138
|
|
|
130
139
|
private
|
|
131
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
|
+
|
|
132
147
|
def generate_random_number_based_on_id_hex(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
|
133
|
-
length
|
|
148
|
+
length = normalize_length(length, DEFAULT_SLUG_NUMBER_LENGTH, MAX_NUMBER_LENGTH)
|
|
134
149
|
((Digest::SHA2.hexdigest(id.to_s)).hex % (10 ** length))
|
|
135
150
|
end
|
|
136
151
|
|
|
@@ -147,14 +162,16 @@ module Slugifiable
|
|
|
147
162
|
|
|
148
163
|
while self.class.exists?(slug: slug_candidate) && attempts < MAX_SLUG_GENERATION_ATTEMPTS
|
|
149
164
|
attempts += 1
|
|
150
|
-
|
|
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)
|
|
151
168
|
slug_candidate = "#{base_slug}-#{random_suffix}"
|
|
152
169
|
end
|
|
153
170
|
|
|
154
171
|
# If we couldn't find a unique slug after MAX_SLUG_GENERATION_ATTEMPTS,
|
|
155
|
-
# append timestamp to ensure uniqueness
|
|
172
|
+
# append timestamp + random to ensure uniqueness
|
|
156
173
|
if attempts == MAX_SLUG_GENERATION_ATTEMPTS
|
|
157
|
-
slug_candidate = "#{base_slug}-#{Time.current.to_i}"
|
|
174
|
+
slug_candidate = "#{base_slug}-#{Time.current.to_i}-#{SecureRandom.random_number(1000)}"
|
|
158
175
|
end
|
|
159
176
|
|
|
160
177
|
slug_candidate
|
|
@@ -185,7 +202,7 @@ module Slugifiable
|
|
|
185
202
|
return [DEFAULT_SLUG_GENERATION_STRATEGY, options]
|
|
186
203
|
end
|
|
187
204
|
elsif strategy.key?(:attribute)
|
|
188
|
-
return [:compute_slug_based_on_attribute, strategy[:attribute]
|
|
205
|
+
return [:compute_slug_based_on_attribute, strategy[:attribute]]
|
|
189
206
|
end
|
|
190
207
|
end
|
|
191
208
|
|
|
@@ -193,7 +210,12 @@ module Slugifiable
|
|
|
193
210
|
end
|
|
194
211
|
|
|
195
212
|
def slug_persisted?
|
|
196
|
-
|
|
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)
|
|
197
219
|
end
|
|
198
220
|
|
|
199
221
|
def set_slug
|
data/lib/slugifiable/version.rb
CHANGED
data/lib/slugifiable.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +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
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -32,10 +32,17 @@ executables: []
|
|
|
32
32
|
extensions: []
|
|
33
33
|
extra_rdoc_files: []
|
|
34
34
|
files:
|
|
35
|
+
- ".simplecov"
|
|
36
|
+
- AGENTS.md
|
|
37
|
+
- Appraisals
|
|
35
38
|
- CHANGELOG.md
|
|
39
|
+
- CLAUDE.md
|
|
36
40
|
- LICENSE.txt
|
|
37
41
|
- README.md
|
|
38
42
|
- Rakefile
|
|
43
|
+
- gemfiles/rails_7.2.gemfile
|
|
44
|
+
- gemfiles/rails_8.0.gemfile
|
|
45
|
+
- gemfiles/rails_8.1.gemfile
|
|
39
46
|
- lib/slugifiable.rb
|
|
40
47
|
- lib/slugifiable/model.rb
|
|
41
48
|
- lib/slugifiable/version.rb
|