mongoid-slug 7.0.0 → 7.1.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.
- checksums.yaml +4 -4
- data/LICENSE +20 -20
- data/README.md +408 -392
- data/lib/mongoid/slug/criteria.rb +120 -111
- data/lib/mongoid/slug/index_builder.rb +70 -69
- data/lib/mongoid/slug/railtie.rb +11 -11
- data/lib/mongoid/slug/slug_id_strategy.rb +5 -5
- data/lib/mongoid/slug/unique_slug.rb +204 -172
- data/lib/mongoid/slug/version.rb +7 -7
- data/lib/mongoid/slug.rb +343 -333
- data/lib/mongoid_slug.rb +4 -4
- data/lib/tasks/mongoid_slug.rake +17 -17
- metadata +3 -3
|
@@ -1,172 +1,204 @@
|
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
where_hash =
|
|
100
|
-
where_hash[:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
@sorted_existing = existing_slugs.map do |s|
|
|
48
|
+
s.delete_prefix(@slug).to_i.abs
|
|
49
|
+
end.sort
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inspect
|
|
53
|
+
{
|
|
54
|
+
slug: @slug,
|
|
55
|
+
existing_slugs: existing_slugs,
|
|
56
|
+
last_entered_slug: last_entered_slug,
|
|
57
|
+
existing_history_slugs: existing_history_slugs,
|
|
58
|
+
sorted_existing: sorted_existing
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
extend Forwardable
|
|
64
|
+
|
|
65
|
+
attr_reader :model, :_slug
|
|
66
|
+
|
|
67
|
+
def_delegators :@model, :slug_scope, :reflect_on_association, :read_attribute,
|
|
68
|
+
:check_against_id, :slug_reserved_words, :slug_url_builder, :collection_name,
|
|
69
|
+
:embedded?, :reflect_on_all_associations, :reflect_on_all_association,
|
|
70
|
+
:slug_by_model_type, :slug_max_length
|
|
71
|
+
|
|
72
|
+
def initialize(model)
|
|
73
|
+
@model = model
|
|
74
|
+
@_slug = ''
|
|
75
|
+
@state = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def metadata
|
|
79
|
+
if @model.respond_to?(:_association)
|
|
80
|
+
@model.send(:_association)
|
|
81
|
+
elsif @model.respond_to?(:relation_metadata)
|
|
82
|
+
@model.relation_metadata
|
|
83
|
+
else
|
|
84
|
+
@model.metadata
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_unique(attempt = nil)
|
|
89
|
+
MUTEX_FOR_SLUG.synchronize do
|
|
90
|
+
@_slug = if attempt
|
|
91
|
+
attempt.to_url
|
|
92
|
+
else
|
|
93
|
+
slug_url_builder.call(model)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@_slug = @_slug[0...slug_max_length] if slug_max_length
|
|
97
|
+
|
|
98
|
+
where_hash = {}
|
|
99
|
+
where_hash[:_slugs.all] = [regex_for_slug]
|
|
100
|
+
where_hash[:_id.ne] = model._id
|
|
101
|
+
|
|
102
|
+
Array(slug_scope).each do |individual_scope|
|
|
103
|
+
next unless reflect_on_association(individual_scope).nil?
|
|
104
|
+
|
|
105
|
+
# scope is not an association, so it's scoped to a local field
|
|
106
|
+
# (e.g. an association id in a denormalized db design)
|
|
107
|
+
where_hash[individual_scope] = model.try(:read_attribute, individual_scope)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
where_hash[:_type] = model.try(:read_attribute, :_type) if slug_by_model_type
|
|
111
|
+
|
|
112
|
+
@state = SlugState.new @_slug, uniqueness_scope.unscoped.where(where_hash), escaped_pattern
|
|
113
|
+
|
|
114
|
+
# do not allow a slug that can be interpreted as the current document id
|
|
115
|
+
@state.include_slug unless model.class.look_like_slugs?([@_slug])
|
|
116
|
+
|
|
117
|
+
# make sure that the slug is not equal to a reserved word
|
|
118
|
+
@state.include_slug if slug_reserved_words.any? { |word| word === @_slug } # rubocop:disable Style/CaseEquality
|
|
119
|
+
|
|
120
|
+
# only look for a new unique slug if the existing slugs contains the current slug
|
|
121
|
+
# - e.g if the slug 'foo-2' is taken, but 'foo' is available, the user can use 'foo'.
|
|
122
|
+
if @state.slug_included?
|
|
123
|
+
highest = @state.highest_existing_counter
|
|
124
|
+
@_slug += "-#{highest.succ}"
|
|
125
|
+
end
|
|
126
|
+
@_slug
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def escaped_pattern
|
|
131
|
+
"^#{Regexp.escape(@_slug)}(?:-(\\d+))?$"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Regular expression that matches slug, slug-1, ... slug-n
|
|
135
|
+
# If slug_name field was indexed, MongoDB will utilize that
|
|
136
|
+
# index to match /^.../ pattern.
|
|
137
|
+
# Use Regexp::Raw to avoid the multiline option when querying the server.
|
|
138
|
+
def regex_for_slug
|
|
139
|
+
if embedded?
|
|
140
|
+
Regexp.new(escaped_pattern)
|
|
141
|
+
else
|
|
142
|
+
BSON::Regexp::Raw.new(escaped_pattern)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def uniqueness_scope
|
|
147
|
+
if embedded?
|
|
148
|
+
parent_metadata = reflect_on_all_association(:embedded_in)[0]
|
|
149
|
+
return model._parent.send(parent_metadata.inverse_of || metadata.name)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# If slug_scope is present, we need to handle whether it's a single scope or multiple scopes.
|
|
153
|
+
if slug_scope
|
|
154
|
+
# We'll track individual scope results in an array.
|
|
155
|
+
scope_results = []
|
|
156
|
+
|
|
157
|
+
Array(slug_scope).each do |individual_scope|
|
|
158
|
+
next unless (metadata = reflect_on_association(individual_scope))
|
|
159
|
+
|
|
160
|
+
# For each scope, we identify its association metadata and fetch the parent record.
|
|
161
|
+
parent = model.send(metadata.name)
|
|
162
|
+
|
|
163
|
+
# It's important to handle nil cases if the parent record doesn't exist.
|
|
164
|
+
if parent.nil?
|
|
165
|
+
# You might want to handle this scenario differently based on your application's logic.
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Make sure doc is actually associated with something, and that
|
|
170
|
+
# some referenced docs have been persisted to the parent
|
|
171
|
+
#
|
|
172
|
+
# TODO: we need better reflection for reference associations,
|
|
173
|
+
# like association_name instead of forcing collection_name here
|
|
174
|
+
# -- maybe in the forthcoming Mongoid refactorings?
|
|
175
|
+
inverse = metadata.inverse_of || collection_name
|
|
176
|
+
next unless parent.respond_to?(inverse)
|
|
177
|
+
|
|
178
|
+
# Add the associated records of the parent (based on the inverse) to our results.
|
|
179
|
+
scope_results << parent.send(inverse)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# After iterating through all scopes, we need to decide how to combine the results (if there are multiple).
|
|
183
|
+
# This part depends on how your application should treat multiple scopes.
|
|
184
|
+
# Here, we'll simply return the first non-empty scope result as an example.
|
|
185
|
+
scope_results.each do |result|
|
|
186
|
+
return result if result.present? # or any other logic for selecting among multiple scope results
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# If we reach this point, it means no valid parent scope was found (all were nil or didn't match the
|
|
190
|
+
# conditions).
|
|
191
|
+
# You might want to raise an error, return a default scope, or handle this scenario based on your
|
|
192
|
+
# application's logic.
|
|
193
|
+
# For this example, we're returning the model's class as a default.
|
|
194
|
+
return model.class
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Unless embedded or slug scope, return the deepest document superclass.
|
|
198
|
+
appropriate_class = model.class
|
|
199
|
+
appropriate_class = appropriate_class.superclass while appropriate_class.superclass.include?(Mongoid::Document)
|
|
200
|
+
appropriate_class
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
data/lib/mongoid/slug/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mongoid # :nodoc:
|
|
4
|
-
module Slug
|
|
5
|
-
VERSION = '7.
|
|
6
|
-
end
|
|
7
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid # :nodoc:
|
|
4
|
+
module Slug
|
|
5
|
+
VERSION = '7.1.0'
|
|
6
|
+
end
|
|
7
|
+
end
|