slugifiable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d06633ba0907f8ee400648856e3d8dbc95fc647be40d60780ae01ebbb6eeb7d3
4
+ data.tar.gz: d40cae2d3fc081af04bd6fac08ea8640d8174c1e59b07652c92bab72eb8a18ca
5
+ SHA512:
6
+ metadata.gz: 39dfad3e03b4cfef9946a929c5fe21585f2aab1ef1922d2f13ad8d10d6d4b732e3eaf6d4dcff545d9664d0395909fa6fa5e40a92801627625ef4034467b93bd9
7
+ data.tar.gz: 8b726b22d56141b633f415460f8a8f8e7042379bc06aebb383f15360914b294bf29a0fffe2aa5e97e25577dab3e962d2b221bf7f18ab0d8822437f77ca97bfef
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## [0.1.0] - 2024-10-30
2
+
3
+ - Initial release
4
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Javi R
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # 🐌 `slugifiable` - Rails gem to generate SEO-friendly slugs
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/slugifiable.svg)](https://badge.fury.io/rb/slugifiable)
4
+
5
+ Automatically generates unique slugs for your Rails' model records, so you can expose SEO-friendly URLs.
6
+
7
+ Example:
8
+ ```
9
+ https://myapp.com/products/big-red-backpack-321678
10
+ ```
11
+
12
+ Where `big-red-backpack-321678` is the slug.
13
+
14
+ `slugifiable` can generate:
15
+ - Slugs like `"big-red-backpack"` or `"big-red-backpack-321678"`: unique, string-based slugs based on any attribute, such as `product.name`
16
+ - Slugs like `"d4735e3a265"`: unique **hex string slugs**
17
+ - Slugs like `321678`: unique **number-only slugs**
18
+
19
+ ## Why
20
+
21
+ When building Rails apps, we usually need to expose _something_ in the URL to identify a record, like:
22
+ ```
23
+ https://myapp.com/products/123
24
+ ```
25
+
26
+ But exposing IDs (like `123`) is not usually good practice. It's not SEO-friendly, it can give away how many records you have in the database, it could also be an attack vector, and it just feels off.
27
+
28
+ It would be much better to have a random-like string or number instead, while still remaining unique and identifiable:
29
+ ```
30
+ https://myapp.com/products/d4735e3a265
31
+ ```
32
+
33
+ Or better yet, use other instance attribute (like `product.name`) to build the slug:
34
+ ```
35
+ https://myapp.com/products/big-red-backpack
36
+ ```
37
+
38
+ `slugifiable` takes care of building all these kinds of slugs for you.
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+ ```ruby
44
+ gem 'slugifiable'
45
+ ```
46
+
47
+ Then run `bundle install`.
48
+
49
+ After installing the gem, add `include Slugifiable::Model` to any ActiveRecord model, like this:
50
+ ```ruby
51
+ class Product < ApplicationRecord
52
+ include Slugifiable::Model # Adding this provides all the required slug-related methods to your model
53
+ end
54
+ ```
55
+
56
+ That's it!
57
+
58
+ Then you can, for example, get the slug for a product like this:
59
+ ```ruby
60
+ Product.first.slug
61
+ => "4e07408562b"
62
+ ```
63
+
64
+ You can also define how to generate slugs:
65
+ ```ruby
66
+ class Product < ApplicationRecord
67
+ include Slugifiable::Model
68
+ generate_slug_based_on :name
69
+ end
70
+ ```
71
+
72
+ And this will generate slugs based on your `Product` instance `name`, like:
73
+ ```ruby
74
+ Product.first.slug
75
+ => "big-red-backpack"
76
+ ```
77
+
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
+
80
+ If you're generating slugs based off the model `id`, you can also set a desired length:
81
+ ```ruby
82
+ class Product < ApplicationRecord
83
+ include Slugifiable::Model
84
+ generate_slug_based_on :id, length: 6
85
+ end
86
+ ```
87
+
88
+ Which would return something like:
89
+ ```ruby
90
+ Product.first.slug
91
+ => "6b86b2"
92
+ ```
93
+
94
+ More details in the "How to use" section.
95
+
96
+ ## How to use
97
+
98
+ Slugs should never change, so it's recommended you save your slugs to the database.
99
+
100
+ Therefore, all models that include `Slugifiable::Model` should have a `slug` attribute that persists the slug in the database. If your model doesn't have a `slug` attribute yet, just run:
101
+ ```
102
+ rails g migration addSlugTo<MODEL_NAME> slug:text
103
+ ```
104
+
105
+ where `<MODEL_NAME>` is your model name in plural, and then run:
106
+ ```
107
+ rails db:migrate
108
+ ```
109
+
110
+ And your model should now have a `slug` attribute in the database.
111
+
112
+ When a model has a `slug` attribute, `slugifiable` automatically generates a slug for that model upon instance creation, and saves it to the DB.
113
+
114
+ `slugifiable` can also work without persisting slugs to the databse, though: you can always run `.slug`, and that will give you a valid, unique slug for your record.
115
+
116
+ ### Define how slugs are generated
117
+
118
+ By default, when you include `Slugifiable::Model`, slugs will be generated as a random-looking string based off the record `id` (SHA hash)
119
+
120
+ `slugifiable` supports both `id` and `uuid`.
121
+
122
+ The default setting is:
123
+ ```ruby
124
+ generate_slug_based_on id: :hex_string
125
+ ```
126
+
127
+ Which returns slugs like: `d4735e3a265`
128
+
129
+ If you don't like hex strings, you can get number-only slugs with:
130
+ ```ruby
131
+ generate_slug_based_on id: :number
132
+ ```
133
+
134
+ Which will return slugs like: `321678` – nonconsecutive, nonincremental, not a total count.
135
+
136
+ When you're generating obfuscated slugs (based on `id`), you can specify a desired slug length:
137
+ ```ruby
138
+ generate_slug_based_on id: :number, length: 3
139
+ ```
140
+
141
+ The length should be a positive number between 1 and 64.
142
+
143
+ If instead of obfuscated slugs you want human-readable slugs, you can specify an attribute to base your slugs off of. For example:
144
+ ```ruby
145
+ generate_slug_based_on :name
146
+ ```
147
+
148
+ Will look for a `name` attribute in your instance, and use its value to generate the slug. So if you have a product like:
149
+ ```ruby
150
+ Product.first.name
151
+ => "Big Red Backpack"
152
+ ```
153
+
154
+ then the slug will be computed as:
155
+ ```ruby
156
+ Product.first.slug
157
+ => "big-red-backpack"
158
+ ```
159
+
160
+ 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
+ ```ruby
162
+ Product.first.slug
163
+ => "big-red-backpack-321678"
164
+ ```
165
+
166
+ ## Development
167
+
168
+ 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.
169
+
170
+ To install this gem onto your local machine, run `bundle exec rake install`.
171
+
172
+ ## Contributing
173
+
174
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/slugifiable. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
175
+
176
+ ## License
177
+
178
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,159 @@
1
+ module Slugifiable
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ # This concern makes objects have a string slug based on their ID or another specified attribute
6
+ #
7
+ # To use, include this in any ActiveRecord model:
8
+ # ```
9
+ # include Slugifiable::Model
10
+ # ```
11
+ #
12
+ # By default all slugs will be a string computed from the record ID:
13
+ # ```
14
+ # generate_slug_based_on :id
15
+ # ```
16
+ #
17
+ # but optionally, you can also specify to compute the slug as a number:
18
+ # ```
19
+ # generate_slug_based_on id: :number
20
+ # ```
21
+ #
22
+ # or compute the slug based off any other attribute:
23
+ # ```
24
+ # generate_slug_based_on :name
25
+ # ```
26
+
27
+ DEFAULT_SLUG_GENERATION_STRATEGY = :compute_slug_as_string
28
+ DEFAULT_SLUG_STRING_LENGTH = 11
29
+ DEFAULT_SLUG_NUMBER_LENGTH = 6
30
+
31
+ included do
32
+ after_create :set_slug
33
+ after_find :update_slug_if_nil
34
+ validates :slug, uniqueness: true
35
+ end
36
+
37
+ class_methods do
38
+ def generate_slug_based_on(strategy, options = {})
39
+ define_method(:generate_slug_based_on) do
40
+ [strategy, options]
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ def method_missing(missing_method, *args, &block)
47
+ if missing_method.to_s == "slug" && !self.methods.include?(:slug)
48
+ compute_slug
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def compute_slug
55
+ strategy, options = determine_slug_generation_method
56
+
57
+ length = options[:length] if options.is_a?(Hash) || nil
58
+
59
+ if strategy == :compute_slug_based_on_attribute
60
+ self.send(strategy, options)
61
+ else
62
+ self.send(strategy, length)
63
+ end
64
+ end
65
+
66
+ def compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH)
67
+ length ||= DEFAULT_SLUG_STRING_LENGTH
68
+ (Digest::SHA2.hexdigest self.id.to_s).first(length)
69
+ end
70
+
71
+ def compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH)
72
+ length ||= DEFAULT_SLUG_NUMBER_LENGTH
73
+ generate_random_number_based_on_id_hex(length)
74
+ end
75
+
76
+ def compute_slug_based_on_attribute(attribute_name)
77
+ return compute_slug_as_string unless self.attributes.include?(attribute_name.to_s)
78
+
79
+ base_slug = self.send(attribute_name)&.to_s&.strip&.parameterize
80
+ base_slug = base_slug.presence || generate_random_number_based_on_id_hex
81
+
82
+ unique_slug = generate_unique_slug(base_slug)
83
+ unique_slug.presence || generate_random_number_based_on_id_hex
84
+ end
85
+
86
+ private
87
+
88
+ def generate_random_number_based_on_id_hex(length = DEFAULT_SLUG_NUMBER_LENGTH)
89
+ length ||= DEFAULT_SLUG_NUMBER_LENGTH
90
+ ((Digest::SHA2.hexdigest(id.to_s)).hex % (10 ** length))
91
+ end
92
+
93
+ def generate_unique_slug(base_slug)
94
+ slug_candidate = base_slug
95
+
96
+ return slug_candidate unless slug_persisted?
97
+
98
+ # Collision resolution logic:
99
+
100
+ count = 0
101
+
102
+ while self.class.exists?(slug: slug_candidate)
103
+ count += 1
104
+ # slug_candidate = "#{base_slug}-#{count}"
105
+ slug_candidate = "#{base_slug}-#{compute_slug_as_number}"
106
+ end
107
+
108
+ slug_candidate
109
+ end
110
+
111
+ def determine_slug_generation_method
112
+ return [DEFAULT_SLUG_GENERATION_STRATEGY, {}] unless respond_to?(:generate_slug_based_on)
113
+
114
+ strategy, options = generate_slug_based_on
115
+ options.merge!(strategy) if strategy.is_a? Hash
116
+
117
+ if strategy.is_a?(Symbol)
118
+ if strategy == :id
119
+ return [:compute_slug_as_string, options]
120
+ else
121
+ return [:compute_slug_based_on_attribute, strategy]
122
+ end
123
+ end
124
+
125
+ if strategy.is_a?(Hash)
126
+ if strategy.key?(:id)
127
+ case strategy[:id]
128
+ when :hex_string
129
+ return [:compute_slug_as_string, options]
130
+ when :number
131
+ return [:compute_slug_as_number, options]
132
+ else
133
+ return [DEFAULT_SLUG_GENERATION_STRATEGY, options]
134
+ end
135
+ elsif strategy.key?(:attribute)
136
+ return [:compute_slug_based_on_attribute, strategy[:attribute], options]
137
+ end
138
+ end
139
+
140
+ [DEFAULT_SLUG_GENERATION_STRATEGY, options]
141
+ end
142
+
143
+ def slug_persisted?
144
+ self.methods.include?(:slug) && self.attributes.include?("slug")
145
+ end
146
+
147
+ def set_slug
148
+ return unless slug_persisted?
149
+
150
+ self.slug = compute_slug if id_changed? || slug.blank?
151
+ self.save
152
+ end
153
+
154
+ def update_slug_if_nil
155
+ set_slug if slug_persisted? && self.slug.nil?
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slugifiable
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slugifiable/version"
4
+ require_relative "slugifiable/model"
5
+
6
+ module Slugifiable
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
@@ -0,0 +1,4 @@
1
+ module Slugifiable
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slugifiable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rameerez
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-10-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ description: Automatically generates unique and SEO-friendly slugs for your Rails
28
+ ActiveRecord model records, so you can expose them in URLs. Allows for string and
29
+ number-based slugs.
30
+ email:
31
+ - rubygems@rameerez.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - lib/slugifiable.rb
41
+ - lib/slugifiable/model.rb
42
+ - lib/slugifiable/version.rb
43
+ - sig/slugifiable.rbs
44
+ homepage: https://github.com/rameerez/slugifiable
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ allowed_push_host: https://rubygems.org
49
+ homepage_uri: https://github.com/rameerez/slugifiable
50
+ source_code_uri: https://github.com/rameerez/slugifiable
51
+ changelog_uri: https://github.com/rameerez/slugifiable/blob/main/CHANGELOG.md
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.0.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.5.16
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Easily generate unique and SEO optimized slugs for your Rails ActiveRecord
71
+ model records, based on any attribute
72
+ test_files: []