make_taggable 0.6.6 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/mysql_tests.yml +56 -0
  3. data/.github/workflows/pg_tests.yml +56 -0
  4. data/.github/workflows/{ci.yml → sqlite_tests.yml} +10 -10
  5. data/.github/workflows/standardrb-check.yml +37 -0
  6. data/.gitignore +3 -0
  7. data/Appraisals +4 -0
  8. data/README.md +47 -27
  9. data/Rakefile +1 -0
  10. data/lib/make_taggable.rb +3 -3
  11. data/lib/make_taggable/version.rb +1 -2
  12. data/lib/tasks/setup_test_db.rake +6 -0
  13. data/make_taggable.gemspec +10 -8
  14. data/spec/dummy/README.md +0 -24
  15. data/spec/dummy/app/models/cached_model_with_array.rb +0 -6
  16. data/spec/dummy/app/models/taggable_model_with_json.rb +6 -0
  17. data/spec/dummy/config/application.rb +2 -8
  18. data/spec/dummy/config/database.yml +1 -19
  19. data/spec/dummy/config/mysql_database.yml.ci +8 -0
  20. data/spec/dummy/config/pg_database.yml.ci +8 -0
  21. data/spec/dummy/db/migrate/{20201119220853_create_taggable_models.rb → 020201119220853_create_taggable_models.rb} +0 -0
  22. data/spec/dummy/db/migrate/{20201119221037_create_columns_override_models.rb → 020201119221037_create_columns_override_models.rb} +0 -0
  23. data/spec/dummy/db/migrate/{20201119221121_create_non_standard_id_taggable_models.rb → 020201119221121_create_non_standard_id_taggable_models.rb} +0 -0
  24. data/spec/dummy/db/migrate/{20201119221228_create_untaggable_models.rb → 020201119221228_create_untaggable_models.rb} +0 -0
  25. data/spec/dummy/db/migrate/{20201119221247_create_cached_models.rb → 020201119221247_create_cached_models.rb} +0 -0
  26. data/spec/dummy/db/migrate/{20201119221314_create_other_cached_models.rb → 020201119221314_create_other_cached_models.rb} +0 -0
  27. data/spec/dummy/db/migrate/{20201119221343_create_companies.rb → 020201119221343_create_companies.rb} +0 -0
  28. data/spec/dummy/db/migrate/{20201119221416_create_users.rb → 020201119221416_create_users.rb} +0 -0
  29. data/spec/dummy/db/migrate/{20201119221434_create_other_taggable_models.rb → 020201119221434_create_other_taggable_models.rb} +0 -0
  30. data/spec/dummy/db/migrate/{20201119221507_create_ordered_taggable_models.rb → 020201119221507_create_ordered_taggable_models.rb} +0 -0
  31. data/spec/dummy/db/migrate/{20201119221530_create_cache_methods_injected_models.rb → 020201119221530_create_cache_methods_injected_models.rb} +0 -0
  32. data/spec/dummy/db/migrate/{20201119221629_create_other_cached_with_array_models.rb → 020201119221629_create_other_cached_with_array_models.rb} +0 -0
  33. data/spec/dummy/db/migrate/{20201119221746_create_taggable_model_with_jsons.rb → 020201119221746_create_taggable_model_with_jsons.rb} +0 -0
  34. data/spec/make_taggable/tag_spec.rb +88 -250
  35. data/spec/make_taggable/taggable_spec.rb +1 -1
  36. data/spec/spec_helper.rb +0 -2
  37. metadata +77 -59
  38. data/.travis.yml +0 -36
  39. data/gemfiles/rails_5.gemfile +0 -9
  40. data/gemfiles/rails_6.gemfile +0 -9
  41. data/gemfiles/rails_master.gemfile +0 -9
  42. data/spec/dummy/db/migrate/20201121222007_create_make_taggable_tags.make_taggable_engine.rb +0 -11
  43. data/spec/dummy/db/migrate/20201121222008_create_make_taggable_taggings.make_taggable_engine.rb +0 -13
  44. data/spec/dummy/db/migrate/20201121222009_change_tag_name_collation_mysql.make_taggable_engine.rb +0 -8
  45. data/spec/dummy/db/migrate/20201121222010_add_index_to_tags.make_taggable_engine.rb +0 -6
  46. data/spec/dummy/db/migrate/20201121222011_add_index_to_taggings.make_taggable_engine.rb +0 -13
  47. data/spec/dummy/db/schema.rb +0 -117
  48. data/spec/dummy/db/seeds.rb +0 -7
@@ -1,24 +0,0 @@
1
- # README
2
-
3
- This README would normally document whatever steps are necessary to get the
4
- application up and running.
5
-
6
- Things you may want to cover:
7
-
8
- * Ruby version
9
-
10
- * System dependencies
11
-
12
- * Configuration
13
-
14
- * Database creation
15
-
16
- * Database initialization
17
-
18
- * How to run the test suite
19
-
20
- * Services (job queues, cache servers, search engines, etc.)
21
-
22
- * Deployment instructions
23
-
24
- * ...
@@ -2,10 +2,4 @@ if using_postgresql?
2
2
  class CachedModelWithArray < ActiveRecord::Base
3
3
  acts_as_taggable
4
4
  end
5
- if postgresql_support_json?
6
- class TaggableModelWithJson < ActiveRecord::Base
7
- acts_as_taggable
8
- make_taggable :skills
9
- end
10
- end
11
5
  end
@@ -0,0 +1,6 @@
1
+ if using_postgresql? && postgresql_support_json?
2
+ class TaggableModelWithJson < ActiveRecord::Base
3
+ acts_as_taggable
4
+ make_taggable :skills
5
+ end
6
+ end
@@ -5,15 +5,9 @@ require "rails/all"
5
5
  Bundler.require(*Rails.groups)
6
6
  require "make_taggable"
7
7
 
8
+
8
9
  module Dummy
9
10
  class Application < Rails::Application
10
- # Settings in config/environments/* take precedence over those specified here.
11
- # Application configuration can go into files in config/initializers
12
- # -- all .rb files in that directory are automatically loaded after loading
13
- # the framework and any gems in your application.
14
-
15
- if Rails.gem_version < Gem::Version.new("6.0") && config.active_record.sqlite3
16
- config.active_record.sqlite3.represent_boolean_as_integer = true
17
- end
11
+ config.generators.system_tests = nil
18
12
  end
19
13
  end
@@ -1,25 +1,7 @@
1
- # SQLite. Versions 3.8.0 and up are supported.
2
- # gem install sqlite3
3
- #
4
- # Ensure the SQLite 3 gem is defined in your Gemfile
5
- # gem 'sqlite3'
6
- #
1
+ ---
7
2
  default: &default
8
3
  adapter: sqlite3
9
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10
- timeout: 5000
11
4
 
12
- development:
13
- <<: *default
14
- database: db/development.sqlite3
15
-
16
- # Warning: The database defined as "test" will be erased and
17
- # re-generated from your development database when you run "rake".
18
- # Do not set this db to the same as development or production.
19
5
  test:
20
6
  <<: *default
21
7
  database: db/test.sqlite3
22
-
23
- production:
24
- <<: *default
25
- database: db/production.sqlite3
@@ -0,0 +1,8 @@
1
+ ---
2
+ default: &default
3
+ adapter: mysql2
4
+ encoding: utf8mb4
5
+
6
+ test:
7
+ <<: *default
8
+ database: my_sql_test_db
@@ -0,0 +1,8 @@
1
+ ---
2
+ default: &default
3
+ adapter: postgresql
4
+ encoding: unicode
5
+
6
+ test:
7
+ <<: *default
8
+ database: pg_test
@@ -1,297 +1,135 @@
1
- require "spec_helper"
1
+ module MakeTaggable
2
+ class Tag < ::ActiveRecord::Base
3
+ self.table_name = MakeTaggable.tags_table
2
4
 
3
- describe MakeTaggable::Tag do
4
- before(:each) do
5
- @tag = MakeTaggable::Tag.new
6
- @user = TaggableModel.create(name: "Pablo")
7
- end
5
+ ### ASSOCIATIONS:
8
6
 
9
- describe "named like any" do
10
- context "case insensitive collation and unique index on tag name", if: using_case_insensitive_collation? do
11
- before(:each) do
12
- MakeTaggable::Tag.create(name: "Awesome")
13
- MakeTaggable::Tag.create(name: "epic")
14
- end
7
+ has_many :taggings, dependent: :destroy, class_name: "::MakeTaggable::Tagging"
15
8
 
16
- it "should find both tags" do
17
- expect(MakeTaggable::Tag.named_like_any(%w[awesome epic]).count).to eq(2)
18
- end
19
- end
9
+ ### VALIDATIONS:
20
10
 
21
- context "case insensitive collation without indexes or case sensitive collation with indexes" do
22
- before(:each) do
23
- MakeTaggable::Tag.create(name: "Awesome")
24
- MakeTaggable::Tag.create(name: "awesome")
25
- MakeTaggable::Tag.create(name: "epic")
26
- end
11
+ validates_presence_of :name
12
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?
13
+ validates_length_of :name, maximum: 255
27
14
 
28
- it "should find both tags" do
29
- expect(MakeTaggable::Tag.named_like_any(%w[awesome epic]).count).to eq(3)
30
- end
15
+ # monkey patch this method if don't need name uniqueness validation
16
+ def validates_name_uniqueness?
17
+ true
31
18
  end
32
- end
33
19
 
34
- describe "named any" do
35
- context "with some special characters combinations", if: using_mysql? do
36
- it "should not raise an invalid encoding exception" do
37
- expect { MakeTaggable::Tag.named_any(["holä", "hol'ä"]) }.not_to raise_error
38
- end
39
- end
40
- end
41
-
42
- describe "for context" do
43
- before(:each) do
44
- @user.skill_list.add("ruby")
45
- @user.save
46
- end
47
-
48
- it "should return tags that have been used in the given context" do
49
- expect(MakeTaggable::Tag.for_context("skills").pluck(:name)).to include("ruby")
50
- end
51
-
52
- it "should not return tags that have been used in other contexts" do
53
- expect(MakeTaggable::Tag.for_context("needs").pluck(:name)).to_not include("ruby")
54
- end
55
- end
56
-
57
- describe "find or create by name" do
58
- before(:each) do
59
- @tag.name = "awesome"
60
- @tag.save
61
- end
20
+ ### SCOPES:
21
+ scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
22
+ scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }
62
23
 
63
- it "should find by name" do
64
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("awesome")).to eq(@tag)
65
- end
66
-
67
- it "should find by name case insensitive" do
68
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("AWESOME")).to eq(@tag)
69
- end
70
-
71
- it "should create by name" do
72
- expect(-> {
73
- MakeTaggable::Tag.find_or_create_with_like_by_name("epic")
74
- }).to change(MakeTaggable::Tag, :count).by(1)
75
- end
76
- end
77
-
78
- describe "find or create by unicode name", unless: using_sqlite? do
79
- before(:each) do
80
- @tag.name = "привет"
81
- @tag.save
82
- end
83
-
84
- it "should find by name" do
85
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("привет")).to eq(@tag)
24
+ def self.named(name)
25
+ if MakeTaggable.strict_case_match
26
+ where(["name = #{binary}?", as_8bit_ascii(name)])
27
+ else
28
+ where(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(name))])
29
+ end
86
30
  end
87
31
 
88
- it "should find by name case insensitive" do
89
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("ПРИВЕТ")).to eq(@tag)
32
+ def self.named_any(list)
33
+ clause = list.map { |tag|
34
+ sanitize_sql_for_named_any(tag).force_encoding("BINARY")
35
+ }.join(" OR ")
36
+ where(clause)
90
37
  end
91
38
 
92
- it "should find by name accent insensitive", if: using_case_insensitive_collation? do
93
- @tag.name = "inupiat"
94
- @tag.save
95
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("Iñupiat")).to eq(@tag)
39
+ def self.named_like(name)
40
+ clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
41
+ where(clause)
96
42
  end
97
- end
98
43
 
99
- describe "find or create all by any name" do
100
- before(:each) do
101
- @tag.name = "awesome"
102
- @tag.save
44
+ def self.named_like_any(list)
45
+ clause = list.map { |tag|
46
+ sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
47
+ }.join(" OR ")
48
+ where(clause)
103
49
  end
104
50
 
105
- it "should find by name" do
106
- expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("awesome")).to eq([@tag])
51
+ def self.for_context(context)
52
+ joins(:taggings)
53
+ .where(["#{MakeTaggable.taggings_table}.context = ?", context])
54
+ .select("DISTINCT #{MakeTaggable.tags_table}.*")
107
55
  end
108
56
 
109
- it "should find by name case insensitive" do
110
- expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME")).to eq([@tag])
111
- end
57
+ ### CLASS METHODS:
112
58
 
113
- context "case sensitive" do
114
- it "should find by name case sensitive" do
115
- MakeTaggable.strict_case_match = true
116
- expect {
117
- MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME")
118
- }.to change(MakeTaggable::Tag, :count).by(1)
59
+ def self.find_or_create_with_like_by_name(name)
60
+ if MakeTaggable.strict_case_match
61
+ find_or_create_all_with_like_by_name([name]).first
62
+ else
63
+ named_like(name).first || create(name: name)
119
64
  end
120
65
  end
121
66
 
122
- it "should create by name" do
123
- expect {
124
- MakeTaggable::Tag.find_or_create_all_with_like_by_name("epic")
125
- }.to change(MakeTaggable::Tag, :count).by(1)
126
- end
127
-
128
- context "case sensitive" do
129
- it "should find or create by name case sensitive" do
130
- MakeTaggable.strict_case_match = true
131
- expect {
132
- expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME", "awesome").map(&:name)).to eq(%w[AWESOME awesome])
133
- }.to change(MakeTaggable::Tag, :count).by(1)
134
- end
135
- end
136
-
137
- it "should find or create by name" do
138
- expect {
139
- expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name)).to eq(%w[awesome epic])
140
- }.to change(MakeTaggable::Tag, :count).by(1)
141
- end
142
-
143
- it "should return an empty array if no tags are specified" do
144
- expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name([])).to be_empty
145
- end
146
- end
147
-
148
- it "should require a name" do
149
- @tag.valid?
150
- # TODO, we should find another way to check this
151
- expect(@tag.errors[:name]).to eq(["can't be blank"])
152
-
153
- @tag.name = "something"
154
- @tag.valid?
67
+ def self.find_or_create_all_with_like_by_name(*list)
68
+ list = Array(list).flatten
155
69
 
156
- expect(@tag.errors[:name]).to be_empty
157
- end
158
-
159
- it "should limit the name length to 255 or less characters" do
160
- @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxranr"
161
- @tag.valid?
162
- # TODO, we should find another way to check this
163
- expect(@tag.errors[:name]).to eq(["is too long (maximum is 255 characters)"])
70
+ return [] if list.empty?
164
71
 
165
- @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxran"
166
- @tag.valid?
167
- expect(@tag.errors[:name]).to be_empty
168
- end
169
-
170
- it "should equal a tag with the same name" do
171
- @tag.name = "awesome"
172
- new_tag = MakeTaggable::Tag.new(name: "awesome")
173
- expect(new_tag).to eq(@tag)
174
- end
72
+ existing_tags = named_any(list)
73
+ list.map do |tag_name|
74
+ tries ||= 3
75
+ comparable_tag_name = comparable_name(tag_name)
76
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
77
+ existing_tag || create(name: tag_name)
78
+ rescue ActiveRecord::RecordNotUnique
79
+ if (tries -= 1).positive?
80
+ ActiveRecord::Base.connection.execute "ROLLBACK"
81
+ existing_tags = named_any(list)
82
+ retry
83
+ end
175
84
 
176
- it "should return its name when to_s is called" do
177
- @tag.name = "cool"
178
- expect(@tag.to_s).to eq("cool")
179
- end
180
-
181
- it "have named_scope named(something)" do
182
- @tag.name = "cool"
183
- @tag.save!
184
- expect(MakeTaggable::Tag.named("cool")).to include(@tag)
185
- end
186
-
187
- it "have named_scope named_like(something)" do
188
- @tag.name = "cool"
189
- @tag.save!
190
- @another_tag = MakeTaggable::Tag.create!(name: "coolip")
191
- expect(MakeTaggable::Tag.named_like("cool")).to include(@tag, @another_tag)
192
- end
193
-
194
- describe "escape wildcard symbols in like requests" do
195
- before(:each) do
196
- @tag.name = "cool"
197
- @tag.save
198
- @another_tag = MakeTaggable::Tag.create!(name: "coo%")
199
- @another_tag2 = MakeTaggable::Tag.create!(name: "coolish")
85
+ raise DuplicateTagError.new("'#{tag_name}' has already been taken")
86
+ end
200
87
  end
201
88
 
202
- it "return escaped result when '%' char present in tag" do
203
- expect(MakeTaggable::Tag.named_like("coo%")).to_not include(@tag)
204
- expect(MakeTaggable::Tag.named_like("coo%")).to include(@another_tag)
205
- end
206
- end
89
+ ### INSTANCE METHODS:
207
90
 
208
- describe "when using strict_case_match" do
209
- before do
210
- MakeTaggable.strict_case_match = true
211
- @tag.name = "awesome"
212
- @tag.save!
91
+ def ==(other)
92
+ super || (other.is_a?(Tag) && name == other.name)
213
93
  end
214
94
 
215
- after do
216
- MakeTaggable.strict_case_match = false
95
+ def to_s
96
+ name
217
97
  end
218
98
 
219
- it "should find by name" do
220
- expect(MakeTaggable::Tag.find_or_create_with_like_by_name("awesome")).to eq(@tag)
99
+ def count
100
+ read_attribute(:count).to_i
221
101
  end
222
102
 
223
- context "case sensitive" do
224
- it "should find by name case sensitively" do
225
- expect {
226
- MakeTaggable::Tag.find_or_create_with_like_by_name("AWESOME")
227
- }.to change(MakeTaggable::Tag, :count)
103
+ class << self
104
+ private
228
105
 
229
- expect(MakeTaggable::Tag.last.name).to eq("AWESOME")
106
+ def comparable_name(str)
107
+ if MakeTaggable.strict_case_match
108
+ str
109
+ else
110
+ unicode_downcase(str.to_s)
111
+ end
230
112
  end
231
- end
232
-
233
- context "case sensitive" do
234
- it "should have a named_scope named(something) that matches exactly" do
235
- uppercase_tag = MakeTaggable::Tag.create(name: "Cool")
236
- @tag.name = "cool"
237
- @tag.save!
238
113
 
239
- expect(MakeTaggable::Tag.named("cool")).to include(@tag)
240
- expect(MakeTaggable::Tag.named("cool")).to_not include(uppercase_tag)
114
+ def binary
115
+ MakeTaggable::Utils.using_mysql? ? "BINARY " : nil
241
116
  end
242
- end
243
-
244
- it "should not change encoding" do
245
- name = "\u3042"
246
- original_encoding = name.encoding
247
- record = MakeTaggable::Tag.find_or_create_with_like_by_name(name)
248
- record.reload
249
- expect(record.name.encoding).to eq(original_encoding)
250
- end
251
-
252
- context "named any with some special characters combinations", if: using_mysql? do
253
- it "should not raise an invalid encoding exception" do
254
- expect { MakeTaggable::Tag.named_any(["holä", "hol'ä"]) }.not_to raise_error
255
- end
256
- end
257
- end
258
117
 
259
- describe "name uniqeness validation" do
260
- let(:duplicate_tag) { MakeTaggable::Tag.new(name: "ror") }
261
-
262
- before { MakeTaggable::Tag.create(name: "ror") }
263
-
264
- context "when do need unique names" do
265
- it "should run uniqueness validation" do
266
- expect(duplicate_tag).to_not be_valid
118
+ def as_8bit_ascii(string)
119
+ string.to_s.mb_chars
267
120
  end
268
121
 
269
- it "add error to name" do
270
- duplicate_tag.save
271
-
272
- expect(duplicate_tag.errors.size).to eq(1)
273
- expect(duplicate_tag.errors.messages[:name]).to include("has already been taken")
122
+ def unicode_downcase(string)
123
+ as_8bit_ascii(string).downcase
274
124
  end
275
- end
276
- end
277
125
 
278
- describe "popular tags" do
279
- before do
280
- %w[sports rails linux tennis golden_syrup].each_with_index do |t, i|
281
- tag = MakeTaggable::Tag.new(name: t)
282
- tag.taggings_count = i
283
- tag.save!
126
+ def sanitize_sql_for_named_any(tag)
127
+ if MakeTaggable.strict_case_match
128
+ sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
129
+ else
130
+ sanitize_sql(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(tag))])
131
+ end
284
132
  end
285
133
  end
286
-
287
- it "should find the most popular tags" do
288
- expect(MakeTaggable::Tag.most_used(3).first.name).to eq("golden_syrup")
289
- expect(MakeTaggable::Tag.most_used(3).length).to eq(3)
290
- end
291
-
292
- it "should find the least popular tags" do
293
- expect(MakeTaggable::Tag.least_used(3).first.name).to eq("sports")
294
- expect(MakeTaggable::Tag.least_used(3).length).to eq(3)
295
- end
296
134
  end
297
135
  end