friendly_id 5.4.2 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/test.yml +34 -25
  5. data/Changelog.md +6 -0
  6. data/Gemfile +9 -13
  7. data/README.md +21 -0
  8. data/Rakefile +24 -27
  9. data/bench.rb +30 -27
  10. data/certs/parndt.pem +25 -23
  11. data/friendly_id.gemspec +26 -29
  12. data/gemfiles/Gemfile.rails-5.2.rb +11 -16
  13. data/gemfiles/Gemfile.rails-6.0.rb +11 -16
  14. data/gemfiles/Gemfile.rails-6.1.rb +22 -0
  15. data/gemfiles/Gemfile.rails-7.0.rb +22 -0
  16. data/guide.rb +5 -5
  17. data/lib/friendly_id/base.rb +57 -60
  18. data/lib/friendly_id/candidates.rb +9 -11
  19. data/lib/friendly_id/configuration.rb +6 -7
  20. data/lib/friendly_id/finder_methods.rb +26 -11
  21. data/lib/friendly_id/finders.rb +63 -66
  22. data/lib/friendly_id/history.rb +59 -63
  23. data/lib/friendly_id/initializer.rb +4 -4
  24. data/lib/friendly_id/migration.rb +6 -6
  25. data/lib/friendly_id/object_utils.rb +2 -2
  26. data/lib/friendly_id/reserved.rb +28 -32
  27. data/lib/friendly_id/scoped.rb +97 -102
  28. data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
  29. data/lib/friendly_id/sequentially_slugged.rb +17 -64
  30. data/lib/friendly_id/simple_i18n.rb +75 -69
  31. data/lib/friendly_id/slug.rb +1 -2
  32. data/lib/friendly_id/slug_generator.rb +1 -3
  33. data/lib/friendly_id/slugged.rb +234 -238
  34. data/lib/friendly_id/version.rb +1 -1
  35. data/lib/friendly_id.rb +41 -45
  36. data/lib/generators/friendly_id_generator.rb +9 -9
  37. data/test/base_test.rb +10 -13
  38. data/test/benchmarks/finders.rb +28 -26
  39. data/test/benchmarks/object_utils.rb +13 -13
  40. data/test/candidates_test.rb +17 -18
  41. data/test/configuration_test.rb +7 -11
  42. data/test/core_test.rb +1 -2
  43. data/test/databases.yml +4 -3
  44. data/test/finders_test.rb +52 -5
  45. data/test/generator_test.rb +16 -26
  46. data/test/helper.rb +29 -22
  47. data/test/history_test.rb +70 -74
  48. data/test/numeric_slug_test.rb +4 -4
  49. data/test/object_utils_test.rb +0 -2
  50. data/test/reserved_test.rb +9 -11
  51. data/test/schema.rb +5 -4
  52. data/test/scoped_test.rb +18 -20
  53. data/test/sequentially_slugged_test.rb +65 -50
  54. data/test/shared.rb +15 -16
  55. data/test/simple_i18n_test.rb +22 -12
  56. data/test/slugged_test.rb +102 -121
  57. data/test/sti_test.rb +19 -21
  58. data.tar.gz.sig +0 -0
  59. metadata +35 -30
  60. metadata.gz.sig +1 -1
@@ -0,0 +1,22 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 7.0.0"
6
+ gem "railties", "~> 7.0.0"
7
+
8
+ # Database Configuration
9
+ group :development, :test do
10
+ platforms :jruby do
11
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0"
12
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0"
13
+ gem "kramdown"
14
+ end
15
+
16
+ platforms :ruby, :rbx do
17
+ gem "sqlite3"
18
+ gem "mysql2"
19
+ gem "pg"
20
+ gem "redcarpet"
21
+ end
22
+ end
data/guide.rb CHANGED
@@ -3,14 +3,14 @@
3
3
  # This script generates the Guide.md file included in the Yard docs.
4
4
 
5
5
  def comments_from path
6
- path = File.expand_path("../lib/friendly_id/#{path}", __FILE__)
6
+ path = File.expand_path("../lib/friendly_id/#{path}", __FILE__)
7
7
  match = File.read(path).match(/\n=begin(.*)\n=end/m)[1].to_s
8
- match.split("\n").reject {|x| x =~ /^@/}.join("\n").strip
8
+ match.split("\n").reject { |x| x =~ /^@/ }.join("\n").strip
9
9
  end
10
10
 
11
- File.open(File.expand_path('../Guide.md', __FILE__), 'w:utf-8') do |guide|
12
- ['../friendly_id.rb', 'base.rb', 'finders.rb', 'slugged.rb', 'history.rb',
13
- 'scoped.rb', 'simple_i18n.rb', 'reserved.rb'].each do |file|
11
+ File.open(File.expand_path("../Guide.md", __FILE__), "w:utf-8") do |guide|
12
+ ["../friendly_id.rb", "base.rb", "finders.rb", "slugged.rb", "history.rb",
13
+ "scoped.rb", "simple_i18n.rb", "reserved.rb"].each do |file|
14
14
  guide.write comments_from file
15
15
  guide.write "\n"
16
16
  end
@@ -1,62 +1,59 @@
1
1
  module FriendlyId
2
- =begin
3
-
4
- ## Setting Up FriendlyId in Your Model
5
-
6
- To use FriendlyId in your ActiveRecord models, you must first either extend or
7
- include the FriendlyId module (it makes no difference), then invoke the
8
- {FriendlyId::Base#friendly_id friendly_id} method to configure your desired
9
- options:
10
-
11
- class Foo < ActiveRecord::Base
12
- include FriendlyId
13
- friendly_id :bar, :use => [:slugged, :simple_i18n]
14
- end
15
-
16
- The most important option is `:use`, which you use to tell FriendlyId which
17
- addons it should use. See the documentation for {FriendlyId::Base#friendly_id} for a list of all
18
- available addons, or skim through the rest of the docs to get a high-level
19
- overview.
20
-
21
- *A note about single table inheritance (STI): you must extend FriendlyId in
22
- all classes that participate in STI, both your parent classes and their
23
- children.*
24
-
25
- ### The Default Setup: Simple Models
26
-
27
- The simplest way to use FriendlyId is with a model that has a uniquely indexed
28
- column with no spaces or special characters, and that is seldom or never
29
- updated. The most common example of this is a user name:
30
-
31
- class User < ActiveRecord::Base
32
- extend FriendlyId
33
- friendly_id :login
34
- validates_format_of :login, :with => /\A[a-z0-9]+\z/i
35
- end
36
-
37
- @user = User.friendly.find "joe" # the old User.find(1) still works, too
38
- @user.to_param # returns "joe"
39
- redirect_to @user # the URL will be /users/joe
40
-
41
- In this case, FriendlyId assumes you want to use the column as-is; it will never
42
- modify the value of the column, and your application should ensure that the
43
- value is unique and admissible in a URL:
44
-
45
- class City < ActiveRecord::Base
46
- extend FriendlyId
47
- friendly_id :name
48
- end
49
-
50
- @city.friendly.find "Viña del Mar"
51
- redirect_to @city # the URL will be /cities/Viña%20del%20Mar
52
-
53
- Writing the code to process an arbitrary string into a good identifier for use
54
- in a URL can be repetitive and surprisingly tricky, so for this reason it's
55
- often better and easier to use {FriendlyId::Slugged slugs}.
56
-
57
- =end
2
+ #
3
+ ## Setting Up FriendlyId in Your Model
4
+ #
5
+ # To use FriendlyId in your ActiveRecord models, you must first either extend or
6
+ # include the FriendlyId module (it makes no difference), then invoke the
7
+ # {FriendlyId::Base#friendly_id friendly_id} method to configure your desired
8
+ # options:
9
+ #
10
+ # class Foo < ActiveRecord::Base
11
+ # include FriendlyId
12
+ # friendly_id :bar, :use => [:slugged, :simple_i18n]
13
+ # end
14
+ #
15
+ # The most important option is `:use`, which you use to tell FriendlyId which
16
+ # addons it should use. See the documentation for {FriendlyId::Base#friendly_id} for a list of all
17
+ # available addons, or skim through the rest of the docs to get a high-level
18
+ # overview.
19
+ #
20
+ # *A note about single table inheritance (STI): you must extend FriendlyId in
21
+ # all classes that participate in STI, both your parent classes and their
22
+ # children.*
23
+ #
24
+ ### The Default Setup: Simple Models
25
+ #
26
+ # The simplest way to use FriendlyId is with a model that has a uniquely indexed
27
+ # column with no spaces or special characters, and that is seldom or never
28
+ # updated. The most common example of this is a user name:
29
+ #
30
+ # class User < ActiveRecord::Base
31
+ # extend FriendlyId
32
+ # friendly_id :login
33
+ # validates_format_of :login, :with => /\A[a-z0-9]+\z/i
34
+ # end
35
+ #
36
+ # @user = User.friendly.find "joe" # the old User.find(1) still works, too
37
+ # @user.to_param # returns "joe"
38
+ # redirect_to @user # the URL will be /users/joe
39
+ #
40
+ # In this case, FriendlyId assumes you want to use the column as-is; it will never
41
+ # modify the value of the column, and your application should ensure that the
42
+ # value is unique and admissible in a URL:
43
+ #
44
+ # class City < ActiveRecord::Base
45
+ # extend FriendlyId
46
+ # friendly_id :name
47
+ # end
48
+ #
49
+ # @city.friendly.find "Viña del Mar"
50
+ # redirect_to @city # the URL will be /cities/Viña%20del%20Mar
51
+ #
52
+ # Writing the code to process an arbitrary string into a good identifier for use
53
+ # in a URL can be repetitive and surprisingly tricky, so for this reason it's
54
+ # often better and easier to use {FriendlyId::Slugged slugs}.
55
+ #
58
56
  module Base
59
-
60
57
  # Configure FriendlyId's behavior in a model.
61
58
  #
62
59
  # class Post < ActiveRecord::Base
@@ -205,10 +202,10 @@ often better and easier to use {FriendlyId::Slugged slugs}.
205
202
  #
206
203
  # @yieldparam config The model class's {FriendlyId::Configuration friendly_id_config}.
207
204
  def friendly_id(base = nil, options = {}, &block)
208
- yield friendly_id_config if block_given?
205
+ yield friendly_id_config if block
209
206
  friendly_id_config.dependent = options.delete :dependent
210
207
  friendly_id_config.use options.delete :use
211
- friendly_id_config.send :set, base ? options.merge(:base => base) : options
208
+ friendly_id_config.send :set, base ? options.merge(base: base) : options
212
209
  include Model
213
210
  end
214
211
 
@@ -270,7 +267,7 @@ often better and easier to use {FriendlyId::Slugged slugs}.
270
267
 
271
268
  # Clears slug on duplicate records when calling `dup`.
272
269
  def dup
273
- super.tap { |duplicate| duplicate.slug = nil if duplicate.respond_to?('slug=') }
270
+ super.tap { |duplicate| duplicate.slug = nil if duplicate.respond_to?("slug=") }
274
271
  end
275
272
  end
276
273
  end
@@ -1,11 +1,9 @@
1
- require 'securerandom'
1
+ require "securerandom"
2
2
 
3
3
  module FriendlyId
4
-
5
4
  # This class provides the slug candidate functionality.
6
5
  # @see FriendlyId::Slugged
7
6
  class Candidates
8
-
9
7
  include Enumerable
10
8
 
11
9
  def initialize(object, *array)
@@ -14,8 +12,8 @@ module FriendlyId
14
12
  end
15
13
 
16
14
  def each(*args, &block)
17
- return candidates unless block_given?
18
- candidates.each{ |candidate| yield candidate }
15
+ return candidates unless block
16
+ candidates.each { |candidate| yield candidate }
19
17
  end
20
18
 
21
19
  private
@@ -29,13 +27,13 @@ module FriendlyId
29
27
 
30
28
  def normalize(candidates)
31
29
  candidates.map do |candidate|
32
- @object.normalize_friendly_id(candidate.map(&:call).join(' '))
33
- end.select {|x| wanted?(x)}
30
+ @object.normalize_friendly_id(candidate.map(&:call).join(" "))
31
+ end.select { |x| wanted?(x) }
34
32
  end
35
33
 
36
34
  def filter(candidates)
37
- unless candidates.all? {|x| reserved?(x)}
38
- candidates.reject! {|x| reserved?(x)}
35
+ unless candidates.all? { |x| reserved?(x) }
36
+ candidates.reject! { |x| reserved?(x) }
39
37
  end
40
38
  candidates
41
39
  end
@@ -44,7 +42,7 @@ module FriendlyId
44
42
  array.map do |candidate|
45
43
  case candidate
46
44
  when String
47
- [->{candidate}]
45
+ [-> { candidate }]
48
46
  when Array
49
47
  to_candidate_array(object, candidate).flatten
50
48
  when Symbol
@@ -53,7 +51,7 @@ module FriendlyId
53
51
  if candidate.respond_to?(:call)
54
52
  [candidate]
55
53
  else
56
- [->{candidate.to_s}]
54
+ [-> { candidate.to_s }]
57
55
  end
58
56
  end
59
57
  end
@@ -2,7 +2,6 @@ module FriendlyId
2
2
  # The configuration parameters passed to {Base#friendly_id} will be stored in
3
3
  # this object.
4
4
  class Configuration
5
-
6
5
  attr_writer :base
7
6
 
8
7
  # The default configuration options.
@@ -25,10 +24,10 @@ module FriendlyId
25
24
  attr_accessor :routes
26
25
 
27
26
  def initialize(model_class, values = nil)
28
- @base = nil
29
- @model_class = model_class
30
- @defaults = {}
31
- @modules = []
27
+ @base = nil
28
+ @model_class = model_class
29
+ @defaults = {}
30
+ @modules = []
32
31
  @finder_methods = FriendlyId::FinderMethods
33
32
  self.routes = :friendly
34
33
  set values
@@ -102,11 +101,11 @@ module FriendlyId
102
101
  private
103
102
 
104
103
  def get_module(object)
105
- Module === object ? object : FriendlyId.const_get(object.to_s.titleize.camelize.gsub(/\s+/, ''))
104
+ Module === object ? object : FriendlyId.const_get(object.to_s.titleize.camelize.gsub(/\s+/, ""))
106
105
  end
107
106
 
108
107
  def set(values)
109
- values and values.each {|name, value| self.send "#{name}=", value}
108
+ values&.each { |name, value| send "#{name}=", value }
110
109
  end
111
110
  end
112
111
  end
@@ -1,7 +1,5 @@
1
1
  module FriendlyId
2
-
3
2
  module FinderMethods
4
-
5
3
  # Finds a record using the given id.
6
4
  #
7
5
  # If the id is "unfriendly", it will call the original find method.
@@ -9,19 +7,33 @@ module FriendlyId
9
7
  # id matching '123' and then fall back to looking for a record with the
10
8
  # numeric id '123'.
11
9
  #
10
+ # @param [Boolean] allow_nil (default: false)
11
+ # Use allow_nil: true if you'd like the finder to return nil instead of
12
+ # raising ActivRecord::RecordNotFound
13
+ #
14
+ # ### Example
15
+ #
16
+ # MyModel.friendly.find("bad-slug")
17
+ # #=> raise ActiveRecord::RecordNotFound
18
+ #
19
+ # MyModel.friendly.find("bad-slug", allow_nil: true)
20
+ # #=> nil
21
+ #
12
22
  # Since FriendlyId 5.0, if the id is a nonnumeric string like '123-foo' it
13
23
  # will *only* search by friendly id and not fall back to the regular find
14
24
  # method.
15
25
  #
16
26
  # If you want to search only by the friendly id, use {#find_by_friendly_id}.
17
27
  # @raise ActiveRecord::RecordNotFound
18
- def find(*args)
28
+ def find(*args, allow_nil: false)
19
29
  id = args.first
20
- return super if args.count != 1 || id.unfriendly_id?
21
- first_by_friendly_id(id).tap {|result| return result unless result.nil?}
22
- return super if potential_primary_key?(id)
30
+ return super(*args) if args.count != 1 || id.unfriendly_id?
31
+ first_by_friendly_id(id).tap { |result| return result unless result.nil? }
32
+ return super(*args) if potential_primary_key?(id)
23
33
 
24
- raise_not_found_exception(id)
34
+ raise_not_found_exception(id) unless allow_nil
35
+ rescue ActiveRecord::RecordNotFound => exception
36
+ raise exception unless allow_nil
25
37
  end
26
38
 
27
39
  # Returns true if a record with the given id exists.
@@ -35,7 +47,7 @@ module FriendlyId
35
47
  # `find`.
36
48
  # @raise ActiveRecord::RecordNotFound
37
49
  def find_by_friendly_id(id)
38
- first_by_friendly_id(id) or raise raise_not_found_exception(id)
50
+ first_by_friendly_id(id) or raise_not_found_exception(id)
39
51
  end
40
52
 
41
53
  def exists_by_friendly_id?(id)
@@ -50,7 +62,11 @@ module FriendlyId
50
62
  key_type = key_type.type if key_type.respond_to?(:type)
51
63
  case key_type
52
64
  when :integer
53
- Integer(id, 10) rescue false
65
+ begin
66
+ Integer(id, 10)
67
+ rescue
68
+ false
69
+ end
54
70
  when :uuid
55
71
  id.match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
56
72
  else
@@ -97,12 +113,11 @@ module FriendlyId
97
113
 
98
114
  def raise_not_found_exception(id)
99
115
  message = "can't find record with friendly id: #{id.inspect}"
100
- if ActiveRecord.version < Gem::Version.create('5.0')
116
+ if ActiveRecord.version < Gem::Version.create("5.0")
101
117
  raise ActiveRecord::RecordNotFound.new(message)
102
118
  else
103
119
  raise ActiveRecord::RecordNotFound.new(message, name, friendly_id_config.query_field, id)
104
120
  end
105
121
  end
106
-
107
122
  end
108
123
  end
@@ -1,73 +1,70 @@
1
1
  module FriendlyId
2
- =begin
3
- ## Performing Finds with FriendlyId
4
-
5
- FriendlyId offers enhanced finders which will search for your record by
6
- friendly id, and fall back to the numeric id if necessary. This makes it easy
7
- to add FriendlyId to an existing application with minimal code modification.
8
-
9
- By default, these methods are available only on the `friendly` scope:
10
-
11
- Restaurant.friendly.find('plaza-diner') #=> works
12
- Restaurant.friendly.find(23) #=> also works
13
- Restaurant.find(23) #=> still works
14
- Restaurant.find('plaza-diner') #=> will not work
15
-
16
- ### Restoring FriendlyId 4.0-style finders
17
-
18
- Prior to version 5.0, FriendlyId overrode the default finder methods to perform
19
- friendly finds all the time. This required modifying parts of Rails that did
20
- not have a public API, which was harder to maintain and at times caused
21
- compatiblity problems. In 5.0 we decided to change the library's defaults and add
22
- the friendly finder methods only to the `friendly` scope in order to boost
23
- compatiblity. However, you can still opt-in to original functionality very
24
- easily by using the `:finders` addon:
25
-
26
- class Restaurant < ActiveRecord::Base
27
- extend FriendlyId
28
-
29
- scope :active, -> {where(:active => true)}
30
-
31
- friendly_id :name, :use => [:slugged, :finders]
32
- end
33
-
34
- Restaurant.friendly.find('plaza-diner') #=> works
35
- Restaurant.find('plaza-diner') #=> now also works
36
- Restaurant.active.find('plaza-diner') #=> now also works
37
-
38
- ### Updating your application to use FriendlyId's finders
39
-
40
- Unless you've chosen to use the `:finders` addon, be sure to modify the finders
41
- in your controllers to use the `friendly` scope. For example:
42
-
43
- # before
44
- def set_restaurant
45
- @restaurant = Restaurant.find(params[:id])
46
- end
47
-
48
- # after
49
- def set_restaurant
50
- @restaurant = Restaurant.friendly.find(params[:id])
51
- end
52
-
53
- #### Active Admin
54
-
55
- Unless you use the `:finders` addon, you should modify your admin controllers
56
- for models that use FriendlyId with something similar to the following:
57
-
58
- controller do
59
- def find_resource
60
- scoped_collection.friendly.find(params[:id])
61
- end
62
- end
63
-
64
- =end
2
+ # ## Performing Finds with FriendlyId
3
+ #
4
+ # FriendlyId offers enhanced finders which will search for your record by
5
+ # friendly id, and fall back to the numeric id if necessary. This makes it easy
6
+ # to add FriendlyId to an existing application with minimal code modification.
7
+ #
8
+ # By default, these methods are available only on the `friendly` scope:
9
+ #
10
+ # Restaurant.friendly.find('plaza-diner') #=> works
11
+ # Restaurant.friendly.find(23) #=> also works
12
+ # Restaurant.find(23) #=> still works
13
+ # Restaurant.find('plaza-diner') #=> will not work
14
+ #
15
+ ### Restoring FriendlyId 4.0-style finders
16
+ #
17
+ # Prior to version 5.0, FriendlyId overrode the default finder methods to perform
18
+ # friendly finds all the time. This required modifying parts of Rails that did
19
+ # not have a public API, which was harder to maintain and at times caused
20
+ # compatiblity problems. In 5.0 we decided to change the library's defaults and add
21
+ # the friendly finder methods only to the `friendly` scope in order to boost
22
+ # compatiblity. However, you can still opt-in to original functionality very
23
+ # easily by using the `:finders` addon:
24
+ #
25
+ # class Restaurant < ActiveRecord::Base
26
+ # extend FriendlyId
27
+ #
28
+ # scope :active, -> {where(:active => true)}
29
+ #
30
+ # friendly_id :name, :use => [:slugged, :finders]
31
+ # end
32
+ #
33
+ # Restaurant.friendly.find('plaza-diner') #=> works
34
+ # Restaurant.find('plaza-diner') #=> now also works
35
+ # Restaurant.active.find('plaza-diner') #=> now also works
36
+ #
37
+ ### Updating your application to use FriendlyId's finders
38
+ #
39
+ # Unless you've chosen to use the `:finders` addon, be sure to modify the finders
40
+ # in your controllers to use the `friendly` scope. For example:
41
+ #
42
+ # # before
43
+ # def set_restaurant
44
+ # @restaurant = Restaurant.find(params[:id])
45
+ # end
46
+ #
47
+ # # after
48
+ # def set_restaurant
49
+ # @restaurant = Restaurant.friendly.find(params[:id])
50
+ # end
51
+ #
52
+ #### Active Admin
53
+ #
54
+ # Unless you use the `:finders` addon, you should modify your admin controllers
55
+ # for models that use FriendlyId with something similar to the following:
56
+ #
57
+ # controller do
58
+ # def find_resource
59
+ # scoped_collection.friendly.find(params[:id])
60
+ # end
61
+ # end
62
+ #
65
63
  module Finders
66
-
67
64
  module ClassMethods
68
65
  if (ActiveRecord::VERSION::MAJOR == 4) && (ActiveRecord::VERSION::MINOR == 0)
69
66
  def relation_delegate_class(klass)
70
- relation_class_name = :"#{klass.to_s.gsub('::', '_')}_#{self.to_s.gsub('::', '_')}"
67
+ relation_class_name = :"#{klass.to_s.gsub("::", "_")}_#{to_s.gsub("::", "_")}"
71
68
  klass.const_get(relation_class_name)
72
69
  end
73
70
  end
@@ -82,7 +79,7 @@ for models that use FriendlyId with something similar to the following:
82
79
  end
83
80
 
84
81
  # Support for friendly finds on associations for Rails 4.0.1 and above.
85
- if ::ActiveRecord.const_defined?('AssociationRelation')
82
+ if ::ActiveRecord.const_defined?("AssociationRelation")
86
83
  model_class.extend(ClassMethods)
87
84
  association_relation_delegate_class = model_class.relation_delegate_class(::ActiveRecord::AssociationRelation)
88
85
  association_relation_delegate_class.send(:include, model_class.friendly_id_config.finder_methods)