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.
- checksums.yaml +4 -4
- data/LICENSE +20 -20
- data/README.md +392 -336
- data/lib/mongoid/slug/criteria.rb +111 -107
- data/lib/mongoid/slug/{index.rb → index_builder.rb} +69 -45
- data/lib/mongoid/slug/railtie.rb +11 -9
- data/lib/mongoid/slug/slug_id_strategy.rb +5 -3
- data/lib/mongoid/slug/unique_slug.rb +172 -173
- data/lib/mongoid/slug/version.rb +7 -5
- data/lib/mongoid/slug.rb +333 -328
- data/lib/mongoid_slug.rb +4 -2
- data/lib/tasks/mongoid_slug.rake +17 -19
- metadata +13 -173
- data/spec/models/alias.rb +0 -6
- data/spec/models/article.rb +0 -9
- data/spec/models/artist.rb +0 -8
- data/spec/models/artwork.rb +0 -10
- data/spec/models/author.rb +0 -15
- data/spec/models/author_polymorphic.rb +0 -15
- data/spec/models/book.rb +0 -12
- data/spec/models/book_polymorphic.rb +0 -12
- data/spec/models/caption.rb +0 -17
- data/spec/models/entity.rb +0 -11
- data/spec/models/friend.rb +0 -7
- data/spec/models/incorrect_slug_persistence.rb +0 -9
- data/spec/models/integer_id.rb +0 -9
- data/spec/models/magazine.rb +0 -7
- data/spec/models/page.rb +0 -9
- data/spec/models/page_localize.rb +0 -9
- data/spec/models/page_slug_localized.rb +0 -9
- data/spec/models/page_slug_localized_custom.rb +0 -10
- data/spec/models/page_slug_localized_history.rb +0 -9
- data/spec/models/partner.rb +0 -7
- data/spec/models/person.rb +0 -12
- data/spec/models/relationship.rb +0 -8
- data/spec/models/string_id.rb +0 -9
- data/spec/models/subject.rb +0 -7
- data/spec/models/without_slug.rb +0 -5
- data/spec/mongoid/criteria_spec.rb +0 -207
- data/spec/mongoid/index_spec.rb +0 -33
- data/spec/mongoid/slug_spec.rb +0 -1169
- data/spec/shared/indexes.rb +0 -41
- data/spec/spec_helper.rb +0 -61
- data/spec/tasks/mongoid_slug_rake_spec.rb +0 -73
@@ -1,173 +1,172 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
|
20
|
-
@
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
@state.
|
112
|
-
|
113
|
-
#
|
114
|
-
@state.include_slug
|
115
|
-
|
116
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
appropriate_class =
|
168
|
-
appropriate_class
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
data/lib/mongoid/slug/version.rb
CHANGED