mongoid-slug 6.0.0 → 7.0.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -20
  3. data/README.md +392 -336
  4. data/lib/mongoid/slug/criteria.rb +111 -107
  5. data/lib/mongoid/slug/{index.rb → index_builder.rb} +69 -45
  6. data/lib/mongoid/slug/railtie.rb +11 -9
  7. data/lib/mongoid/slug/slug_id_strategy.rb +5 -3
  8. data/lib/mongoid/slug/unique_slug.rb +172 -173
  9. data/lib/mongoid/slug/version.rb +7 -5
  10. data/lib/mongoid/slug.rb +333 -328
  11. data/lib/mongoid_slug.rb +4 -2
  12. data/lib/tasks/mongoid_slug.rake +17 -19
  13. metadata +13 -173
  14. data/spec/models/alias.rb +0 -6
  15. data/spec/models/article.rb +0 -9
  16. data/spec/models/artist.rb +0 -8
  17. data/spec/models/artwork.rb +0 -10
  18. data/spec/models/author.rb +0 -15
  19. data/spec/models/author_polymorphic.rb +0 -15
  20. data/spec/models/book.rb +0 -12
  21. data/spec/models/book_polymorphic.rb +0 -12
  22. data/spec/models/caption.rb +0 -17
  23. data/spec/models/entity.rb +0 -11
  24. data/spec/models/friend.rb +0 -7
  25. data/spec/models/incorrect_slug_persistence.rb +0 -9
  26. data/spec/models/integer_id.rb +0 -9
  27. data/spec/models/magazine.rb +0 -7
  28. data/spec/models/page.rb +0 -9
  29. data/spec/models/page_localize.rb +0 -9
  30. data/spec/models/page_slug_localized.rb +0 -9
  31. data/spec/models/page_slug_localized_custom.rb +0 -10
  32. data/spec/models/page_slug_localized_history.rb +0 -9
  33. data/spec/models/partner.rb +0 -7
  34. data/spec/models/person.rb +0 -12
  35. data/spec/models/relationship.rb +0 -8
  36. data/spec/models/string_id.rb +0 -9
  37. data/spec/models/subject.rb +0 -7
  38. data/spec/models/without_slug.rb +0 -5
  39. data/spec/mongoid/criteria_spec.rb +0 -207
  40. data/spec/mongoid/index_spec.rb +0 -33
  41. data/spec/mongoid/slug_spec.rb +0 -1169
  42. data/spec/shared/indexes.rb +0 -41
  43. data/spec/spec_helper.rb +0 -61
  44. data/spec/tasks/mongoid_slug_rake_spec.rb +0 -73
@@ -1,173 +1,172 @@
1
- require 'forwardable'
2
-
3
- # Can use e.g. Mongoid::Slug::UniqueSlug.new(ModelClass.new).find_unique "slug-1" for auto-suggest ui
4
- module Mongoid
5
- module Slug
6
- class UniqueSlug
7
- MUTEX_FOR_SLUG = Mutex.new
8
- class SlugState
9
- attr_reader :last_entered_slug, :existing_slugs, :existing_history_slugs, :sorted_existing
10
-
11
- def initialize(slug, documents, pattern)
12
- @slug = slug
13
- @documents = documents
14
- @pattern = pattern
15
- @last_entered_slug = []
16
- @existing_slugs = []
17
- @existing_history_slugs = []
18
- @sorted_existing = []
19
- regexp_pattern = Regexp.new(@pattern)
20
- @documents.each do |doc|
21
- history_slugs = doc._slugs
22
- next if history_slugs.nil?
23
- existing_slugs.push(*history_slugs.find_all { |cur_slug| cur_slug =~ regexp_pattern })
24
- last_entered_slug.push(*history_slugs.last) if history_slugs.last =~ regexp_pattern
25
- existing_history_slugs.push(*history_slugs.first(history_slugs.length - 1).find_all { |cur_slug| cur_slug =~ regexp_pattern })
26
- end
27
- end
28
-
29
- def slug_included?
30
- existing_slugs.include? @slug
31
- end
32
-
33
- def include_slug
34
- existing_slugs << @slug
35
- end
36
-
37
- def highest_existing_counter
38
- sort_existing_slugs
39
- @sorted_existing.last || 0
40
- end
41
-
42
- def sort_existing_slugs
43
- # remove the slug part and leave the absolute integer part and sort
44
- re = /^#{Regexp.escape(@slug)}/
45
- @sorted_existing = existing_slugs.map do |s|
46
- s.sub(re, '').to_i.abs
47
- end.sort
48
- end
49
-
50
- def inspect
51
- {
52
- slug: @slug,
53
- existing_slugs: existing_slugs,
54
- last_entered_slug: last_entered_slug,
55
- existing_history_slugs: existing_history_slugs,
56
- sorted_existing: sorted_existing
57
- }
58
- end
59
- end
60
-
61
- extend Forwardable
62
-
63
- attr_reader :model, :_slug
64
-
65
- def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
66
- :check_against_id, :slug_reserved_words, :slug_url_builder, :collection_name,
67
- :embedded?, :reflect_on_all_associations, :reflect_on_all_association,
68
- :slug_by_model_type, :slug_max_length
69
-
70
- def initialize(model)
71
- @model = model
72
- @_slug = ''
73
- @state = nil
74
- end
75
-
76
- def metadata
77
- if @model.respond_to?(:_association)
78
- @model.send(:_association)
79
- elsif @model.respond_to?(:relation_metadata)
80
- @model.relation_metadata
81
- else
82
- @model.metadata
83
- end
84
- end
85
-
86
- def find_unique(attempt = nil)
87
- MUTEX_FOR_SLUG.synchronize do
88
- @_slug = if attempt
89
- attempt.to_url
90
- else
91
- slug_url_builder.call(model)
92
- end
93
-
94
- @_slug = @_slug[0...slug_max_length] if slug_max_length
95
-
96
- where_hash = {}
97
- where_hash[:_slugs.all] = [regex_for_slug]
98
- where_hash[:_id.ne] = model._id
99
-
100
- if (scope = slug_scope) && reflect_on_association(scope).nil?
101
- # scope is not an association, so it's scoped to a local field
102
- # (e.g. an association id in a denormalized db design)
103
- where_hash[scope] = model.try(:read_attribute, scope)
104
- end
105
-
106
- where_hash[:_type] = model.try(:read_attribute, :_type) if slug_by_model_type
107
-
108
- @state = SlugState.new @_slug, uniqueness_scope.unscoped.where(where_hash), escaped_pattern
109
-
110
- # do not allow a slug that can be interpreted as the current document id
111
- @state.include_slug unless model.class.look_like_slugs?([@_slug])
112
-
113
- # make sure that the slug is not equal to a reserved word
114
- @state.include_slug if slug_reserved_words.any? { |word| word === @_slug }
115
-
116
- # only look for a new unique slug if the existing slugs contains the current slug
117
- # - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
118
- if @state.slug_included?
119
- highest = @state.highest_existing_counter
120
- @_slug += "-#{highest.succ}"
121
- end
122
- @_slug
123
- end
124
- end
125
-
126
- def escaped_pattern
127
- "^#{Regexp.escape(@_slug)}(?:-(\\d+))?$"
128
- end
129
-
130
- # Regular expression that matches slug, slug-1, ... slug-n
131
- # If slug_name field was indexed, MongoDB will utilize that
132
- # index to match /^.../ pattern.
133
- # Use Regexp::Raw to avoid the multiline option when querying the server.
134
- def regex_for_slug
135
- if embedded? || Mongoid::Compatibility::Version.mongoid3? || Mongoid::Compatibility::Version.mongoid4?
136
- Regexp.new(escaped_pattern)
137
- else
138
- BSON::Regexp::Raw.new(escaped_pattern)
139
- end
140
- end
141
-
142
- def uniqueness_scope
143
- if slug_scope && (metadata = reflect_on_association(slug_scope))
144
-
145
- parent = model.send(metadata.name)
146
-
147
- # Make sure doc is actually associated with something, and that
148
- # some referenced docs have been persisted to the parent
149
- #
150
- # TODO: we need better reflection for reference associations,
151
- # like association_name instead of forcing collection_name here
152
- # -- maybe in the forthcoming Mongoid refactorings?
153
- inverse = metadata.inverse_of || collection_name
154
- return parent.respond_to?(inverse) ? parent.send(inverse) : model.class
155
- end
156
-
157
- if embedded?
158
- parent_metadata = if Mongoid::Compatibility::Version.mongoid7_or_newer?
159
- reflect_on_all_association(:embedded_in)[0]
160
- else
161
- reflect_on_all_associations(:embedded_in)[0]
162
- end
163
- return model._parent.send(parent_metadata.inverse_of || self.metadata.name)
164
- end
165
-
166
- # unless embedded or slug scope, return the deepest document superclass
167
- appropriate_class = model.class
168
- appropriate_class = appropriate_class.superclass while appropriate_class.superclass.include?(Mongoid::Document)
169
- appropriate_class
170
- end
171
- end
172
- end
173
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ # Can use e.g. Mongoid::Slug::UniqueSlug.new(ModelClass.new).find_unique "slug-1" for auto-suggest ui
6
+ module Mongoid
7
+ module Slug
8
+ class UniqueSlug
9
+ MUTEX_FOR_SLUG = Mutex.new
10
+ class SlugState
11
+ attr_reader :last_entered_slug, :existing_slugs, :existing_history_slugs, :sorted_existing
12
+
13
+ def initialize(slug, documents, pattern)
14
+ @slug = slug
15
+ @documents = documents
16
+ @pattern = pattern
17
+ @last_entered_slug = []
18
+ @existing_slugs = []
19
+ @existing_history_slugs = []
20
+ @sorted_existing = []
21
+ regexp_pattern = Regexp.new(@pattern)
22
+ @documents.each do |doc|
23
+ history_slugs = doc._slugs
24
+ next if history_slugs.nil?
25
+
26
+ existing_slugs.push(*history_slugs.grep(regexp_pattern))
27
+ last_entered_slug.push(*history_slugs.last) if history_slugs.last =~ regexp_pattern
28
+ existing_history_slugs.push(*history_slugs.first(history_slugs.length - 1).grep(regexp_pattern))
29
+ end
30
+ end
31
+
32
+ def slug_included?
33
+ existing_slugs.include? @slug
34
+ end
35
+
36
+ def include_slug
37
+ existing_slugs << @slug
38
+ end
39
+
40
+ def highest_existing_counter
41
+ sort_existing_slugs
42
+ @sorted_existing.last || 0
43
+ end
44
+
45
+ def sort_existing_slugs
46
+ # remove the slug part and leave the absolute integer part and sort
47
+ re = /^#{Regexp.escape(@slug)}/
48
+ @sorted_existing = existing_slugs.map do |s|
49
+ s.sub(re, '').to_i.abs
50
+ end.sort
51
+ end
52
+
53
+ def inspect
54
+ {
55
+ slug: @slug,
56
+ existing_slugs: existing_slugs,
57
+ last_entered_slug: last_entered_slug,
58
+ existing_history_slugs: existing_history_slugs,
59
+ sorted_existing: sorted_existing
60
+ }
61
+ end
62
+ end
63
+
64
+ extend Forwardable
65
+
66
+ attr_reader :model, :_slug
67
+
68
+ def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
69
+ :check_against_id, :slug_reserved_words, :slug_url_builder, :collection_name,
70
+ :embedded?, :reflect_on_all_associations, :reflect_on_all_association,
71
+ :slug_by_model_type, :slug_max_length
72
+
73
+ def initialize(model)
74
+ @model = model
75
+ @_slug = ''
76
+ @state = nil
77
+ end
78
+
79
+ def metadata
80
+ if @model.respond_to?(:_association)
81
+ @model.send(:_association)
82
+ elsif @model.respond_to?(:relation_metadata)
83
+ @model.relation_metadata
84
+ else
85
+ @model.metadata
86
+ end
87
+ end
88
+
89
+ def find_unique(attempt = nil)
90
+ MUTEX_FOR_SLUG.synchronize do
91
+ @_slug = if attempt
92
+ attempt.to_url
93
+ else
94
+ slug_url_builder.call(model)
95
+ end
96
+
97
+ @_slug = @_slug[0...slug_max_length] if slug_max_length
98
+
99
+ where_hash = {}
100
+ where_hash[:_slugs.all] = [regex_for_slug]
101
+ where_hash[:_id.ne] = model._id
102
+
103
+ if (scope = slug_scope) && reflect_on_association(scope).nil?
104
+ # scope is not an association, so it's scoped to a local field
105
+ # (e.g. an association id in a denormalized db design)
106
+ where_hash[scope] = model.try(:read_attribute, scope)
107
+ end
108
+
109
+ where_hash[:_type] = model.try(:read_attribute, :_type) if slug_by_model_type
110
+
111
+ @state = SlugState.new @_slug, uniqueness_scope.unscoped.where(where_hash), escaped_pattern
112
+
113
+ # do not allow a slug that can be interpreted as the current document id
114
+ @state.include_slug unless model.class.look_like_slugs?([@_slug])
115
+
116
+ # make sure that the slug is not equal to a reserved word
117
+ @state.include_slug if slug_reserved_words.any? { |word| word === @_slug } # rubocop:disable Style/CaseEquality
118
+
119
+ # only look for a new unique slug if the existing slugs contains the current slug
120
+ # - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
121
+ if @state.slug_included?
122
+ highest = @state.highest_existing_counter
123
+ @_slug += "-#{highest.succ}"
124
+ end
125
+ @_slug
126
+ end
127
+ end
128
+
129
+ def escaped_pattern
130
+ "^#{Regexp.escape(@_slug)}(?:-(\\d+))?$"
131
+ end
132
+
133
+ # Regular expression that matches slug, slug-1, ... slug-n
134
+ # If slug_name field was indexed, MongoDB will utilize that
135
+ # index to match /^.../ pattern.
136
+ # Use Regexp::Raw to avoid the multiline option when querying the server.
137
+ def regex_for_slug
138
+ if embedded?
139
+ Regexp.new(escaped_pattern)
140
+ else
141
+ BSON::Regexp::Raw.new(escaped_pattern)
142
+ end
143
+ end
144
+
145
+ def uniqueness_scope
146
+ if slug_scope && (metadata = reflect_on_association(slug_scope))
147
+
148
+ parent = model.send(metadata.name)
149
+
150
+ # Make sure doc is actually associated with something, and that
151
+ # some referenced docs have been persisted to the parent
152
+ #
153
+ # TODO: we need better reflection for reference associations,
154
+ # like association_name instead of forcing collection_name here
155
+ # -- maybe in the forthcoming Mongoid refactorings?
156
+ inverse = metadata.inverse_of || collection_name
157
+ return parent.respond_to?(inverse) ? parent.send(inverse) : model.class
158
+ end
159
+
160
+ if embedded?
161
+ parent_metadata = reflect_on_all_association(:embedded_in)[0]
162
+ return model._parent.send(parent_metadata.inverse_of || self.metadata.name)
163
+ end
164
+
165
+ # unless embedded or slug scope, return the deepest document superclass
166
+ appropriate_class = model.class
167
+ appropriate_class = appropriate_class.superclass while appropriate_class.superclass.include?(Mongoid::Document)
168
+ appropriate_class
169
+ end
170
+ end
171
+ end
172
+ end
@@ -1,5 +1,7 @@
1
- module Mongoid #:nodoc:
2
- module Slug
3
- VERSION = '6.0.0'.freeze
4
- end
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid # :nodoc:
4
+ module Slug
5
+ VERSION = '7.0.0'
6
+ end
7
+ end