slugifiable 0.1.0 → 0.1.1
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/README.md +48 -0
- data/Rakefile +9 -1
- data/lib/slugifiable/model.rb +65 -13
- data/lib/slugifiable/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9f707db7b8e857da8289a7e17754261aa3dd4b06b517673b983936cc449e530
|
4
|
+
data.tar.gz: 36144ba06043096a3ead2aa5e3728a3c6c0f54bf9be15c27d085e944d47a1b76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 848886284f316ccb3086afcb268ddf09bf136c040a055c353fa4492021d578e439367ce137d471334a339fb43ff2997271267b2c0668d25f7654803cd4e73cad
|
7
|
+
data.tar.gz: 984f18e9268a0fff5b64f3e8d28e654c2586fa99c02f2199134bbac798cb311e68e5714b17c22ef6583eae75f51cce22b209ac08791d973dbe7f71a2e8830645
|
data/README.md
CHANGED
@@ -77,6 +77,10 @@ Product.first.slug
|
|
77
77
|
|
78
78
|
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
79
|
|
80
|
+
> [!IMPORTANT]
|
81
|
+
> 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)`
|
82
|
+
> This is because records are created without a slug, and the slug is generated later.
|
83
|
+
|
80
84
|
If you're generating slugs based off the model `id`, you can also set a desired length:
|
81
85
|
```ruby
|
82
86
|
class Product < ApplicationRecord
|
@@ -157,12 +161,56 @@ Product.first.slug
|
|
157
161
|
=> "big-red-backpack"
|
158
162
|
```
|
159
163
|
|
164
|
+
You can also use instance methods to generate more complex slugs. This is useful when you need to combine multiple attributes:
|
165
|
+
```ruby
|
166
|
+
class Event < ApplicationRecord
|
167
|
+
include Slugifiable::Model
|
168
|
+
belongs_to :location
|
169
|
+
|
170
|
+
generate_slug_based_on :title_with_location
|
171
|
+
|
172
|
+
# The method can return any string - slugifiable will handle the parameterization
|
173
|
+
def title_with_location
|
174
|
+
if location.present?
|
175
|
+
"#{title} #{location.city} #{location.region}" # Returns raw string, slugifiable parameterizes it
|
176
|
+
else
|
177
|
+
title
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
This will generate slugs like:
|
184
|
+
```ruby
|
185
|
+
Event.first.slug
|
186
|
+
=> "my-awesome-event-new-york-new-york" # Automatically parameterized
|
187
|
+
```
|
188
|
+
|
160
189
|
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
190
|
```ruby
|
162
191
|
Product.first.slug
|
163
192
|
=> "big-red-backpack-321678"
|
164
193
|
```
|
165
194
|
|
195
|
+
## Testing
|
196
|
+
|
197
|
+
The gem includes a comprehensive test suite that covers:
|
198
|
+
|
199
|
+
- Default slug generation behavior
|
200
|
+
- Attribute-based slug generation
|
201
|
+
- Method-based slug generation (including private/protected methods)
|
202
|
+
- Collision resolution and uniqueness handling
|
203
|
+
- Special cases and edge conditions
|
204
|
+
|
205
|
+
To run the tests:
|
206
|
+
|
207
|
+
```bash
|
208
|
+
bundle install
|
209
|
+
bundle exec rake test
|
210
|
+
```
|
211
|
+
|
212
|
+
The test suite uses SQLite3 in-memory database and requires no additional setup.
|
213
|
+
|
166
214
|
## Development
|
167
215
|
|
168
216
|
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
|
data/lib/slugifiable/model.rb
CHANGED
@@ -28,6 +28,10 @@ module Slugifiable
|
|
28
28
|
DEFAULT_SLUG_STRING_LENGTH = 11
|
29
29
|
DEFAULT_SLUG_NUMBER_LENGTH = 6
|
30
30
|
|
31
|
+
# Maximum number of attempts to generate a unique slug
|
32
|
+
# before falling back to timestamp-based suffix
|
33
|
+
MAX_SLUG_GENERATION_ATTEMPTS = 10
|
34
|
+
|
31
35
|
included do
|
32
36
|
after_create :set_slug
|
33
37
|
after_find :update_slug_if_nil
|
@@ -36,11 +40,14 @@ module Slugifiable
|
|
36
40
|
|
37
41
|
class_methods do
|
38
42
|
def generate_slug_based_on(strategy, options = {})
|
43
|
+
# Remove previous definition if it exists to avoid warnings
|
44
|
+
remove_method(:generate_slug_based_on) if method_defined?(:generate_slug_based_on)
|
45
|
+
|
46
|
+
# Define the method that returns the strategy
|
39
47
|
define_method(:generate_slug_based_on) do
|
40
48
|
[strategy, options]
|
41
49
|
end
|
42
50
|
end
|
43
|
-
|
44
51
|
end
|
45
52
|
|
46
53
|
def method_missing(missing_method, *args, &block)
|
@@ -54,7 +61,7 @@ module Slugifiable
|
|
54
61
|
def compute_slug
|
55
62
|
strategy, options = determine_slug_generation_method
|
56
63
|
|
57
|
-
length = options[:length] if options.is_a?(Hash)
|
64
|
+
length = options[:length] if options.is_a?(Hash)
|
58
65
|
|
59
66
|
if strategy == :compute_slug_based_on_attribute
|
60
67
|
self.send(strategy, options)
|
@@ -74,11 +81,48 @@ module Slugifiable
|
|
74
81
|
end
|
75
82
|
|
76
83
|
def compute_slug_based_on_attribute(attribute_name)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
84
|
+
# This method generates a slug from either:
|
85
|
+
# 1. A database column (e.g. generate_slug_based_on :title)
|
86
|
+
# 2. An instance method (e.g. generate_slug_based_on :title_with_location)
|
87
|
+
#
|
88
|
+
# Priority:
|
89
|
+
# - Database columns take precedence over methods with the same name
|
90
|
+
# - Falls back to methods if no matching column exists
|
91
|
+
# - Falls back to ID-based slug if neither exists
|
92
|
+
#
|
93
|
+
# Flow:
|
94
|
+
# 1. Check if source exists (DB column first, then method)
|
95
|
+
# 2. Get raw value
|
96
|
+
# 3. Parameterize (convert "My Title" -> "my-title")
|
97
|
+
# 4. Ensure uniqueness
|
98
|
+
# 5. Fallback to random number if anything fails
|
99
|
+
|
100
|
+
# First check if we can get a value from the database
|
101
|
+
has_attribute = self.attributes.include?(attribute_name.to_s)
|
102
|
+
|
103
|
+
# Only check for methods if no DB attribute exists
|
104
|
+
# We check all method types to be thorough
|
105
|
+
responds_to_method = !has_attribute && (
|
106
|
+
self.class.method_defined?(attribute_name) ||
|
107
|
+
self.class.private_method_defined?(attribute_name) ||
|
108
|
+
self.class.protected_method_defined?(attribute_name)
|
109
|
+
)
|
110
|
+
|
111
|
+
# If we can't get a value from either source, fallback to using the record's ID
|
112
|
+
return compute_slug_as_string unless has_attribute || responds_to_method
|
113
|
+
|
114
|
+
# Get and clean the raw value (e.g. " My Title " -> "My Title")
|
115
|
+
# Works for both DB attributes and methods thanks to Ruby's send
|
116
|
+
raw_value = self.send(attribute_name)
|
117
|
+
return generate_random_number_based_on_id_hex if raw_value.nil?
|
118
|
+
|
119
|
+
# Convert to URL-friendly format
|
120
|
+
# e.g. "My Title" -> "my-title"
|
121
|
+
base_slug = raw_value.to_s.strip.parameterize
|
122
|
+
return generate_random_number_based_on_id_hex if base_slug.blank?
|
123
|
+
|
124
|
+
# Handle duplicate slugs by adding a random suffix if needed
|
125
|
+
# e.g. "my-title" -> "my-title-123456"
|
82
126
|
unique_slug = generate_unique_slug(base_slug)
|
83
127
|
unique_slug.presence || generate_random_number_based_on_id_hex
|
84
128
|
end
|
@@ -96,13 +140,21 @@ module Slugifiable
|
|
96
140
|
return slug_candidate unless slug_persisted?
|
97
141
|
|
98
142
|
# Collision resolution logic:
|
143
|
+
# Try up to MAX_SLUG_GENERATION_ATTEMPTS times with random suffixes
|
144
|
+
# This prevents infinite loops while still giving us good odds
|
145
|
+
# of finding a unique slug
|
146
|
+
attempts = 0
|
147
|
+
|
148
|
+
while self.class.exists?(slug: slug_candidate) && attempts < MAX_SLUG_GENERATION_ATTEMPTS
|
149
|
+
attempts += 1
|
150
|
+
random_suffix = compute_slug_as_number
|
151
|
+
slug_candidate = "#{base_slug}-#{random_suffix}"
|
152
|
+
end
|
99
153
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
# slug_candidate = "#{base_slug}-#{count}"
|
105
|
-
slug_candidate = "#{base_slug}-#{compute_slug_as_number}"
|
154
|
+
# If we couldn't find a unique slug after MAX_SLUG_GENERATION_ATTEMPTS,
|
155
|
+
# append timestamp to ensure uniqueness
|
156
|
+
if attempts == MAX_SLUG_GENERATION_ATTEMPTS
|
157
|
+
slug_candidate = "#{base_slug}-#{Time.current.to_i}"
|
106
158
|
end
|
107
159
|
|
108
160
|
slug_candidate
|
data/lib/slugifiable/version.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.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- rameerez
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-02-23 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -49,7 +48,6 @@ metadata:
|
|
49
48
|
homepage_uri: https://github.com/rameerez/slugifiable
|
50
49
|
source_code_uri: https://github.com/rameerez/slugifiable
|
51
50
|
changelog_uri: https://github.com/rameerez/slugifiable/blob/main/CHANGELOG.md
|
52
|
-
post_install_message:
|
53
51
|
rdoc_options: []
|
54
52
|
require_paths:
|
55
53
|
- lib
|
@@ -64,8 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
62
|
- !ruby/object:Gem::Version
|
65
63
|
version: '0'
|
66
64
|
requirements: []
|
67
|
-
rubygems_version: 3.
|
68
|
-
signing_key:
|
65
|
+
rubygems_version: 3.6.2
|
69
66
|
specification_version: 4
|
70
67
|
summary: Easily generate unique and SEO optimized slugs for your Rails ActiveRecord
|
71
68
|
model records, based on any attribute
|