mongoid-slug 6.0.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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