make_taggable 0.6.6 → 0.7.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 (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