metka 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd6f202972d558afed083a0095530a47e1e2f35028beb4a457c1b13741614b0d
4
- data.tar.gz: e11832ae26b49cdb99aa7b6fd5dddb9f559b862b8460c19b492c05467ea04331
3
+ metadata.gz: 960a0b262fa5e1dc7a4a08baf844f4f12063696133527f0ef201a4414ee498c0
4
+ data.tar.gz: 2792d8b84d0ebf6c9aa197c3bbbe8d6b257f522493a2b770887105cd0d6b539d
5
5
  SHA512:
6
- metadata.gz: ea2f237f88c8a661931db07532243ddd6c7af85741f77422cc6f6fafd3460fbfaacad205064eeecb438a29a25c3aff11157ddf6309ff6bb559d317674bbdc019
7
- data.tar.gz: b9e9f74757a541c6bf8fb123ba27cf619ebafeb7b92b61ea0a71b0482b5e3a3def87bae4c5292cd159e4bc6beeeb9274811e77e7dd461736d16bb15529ec7673
6
+ metadata.gz: 4b47eb9c9b7e64609b0933b02e5704aa8e5dd5cca5ca1a9c796625942fed6b67551574cb6fe3c45190954ce8bfb56ae40bb125659751d55b5ef8f780a35beb0d
7
+ data.tar.gz: fe1b4c69679173d7f8c639f3c060722c617f39f537dba1253ff1f9cd70e26a44028e8ae6676d440c2b9165691162f907a0cf4e546510c8e37194db13c59adb44
data/.gitignore CHANGED
@@ -10,3 +10,7 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+ spec/debug.log
14
+
15
+ spec/dummy/log/test.log
16
+ spec/dummy/log/development.log
@@ -8,17 +8,29 @@ sudo: false
8
8
  notifications:
9
9
  email: false
10
10
 
11
+ services:
12
+ - postgresql
11
13
  addons:
12
- postgresql: '9.6'
13
-
14
- env:
15
- global:
16
- - METKA_DB_USER=postgres
17
- - METKA_DB_NAME=metka
14
+ postgresql: "11.2"
18
15
 
19
16
  before_install:
20
17
  - gem install -v 2.0.2 bundler
18
+ - sudo apt-get update
19
+ - sudo apt-get --yes remove postgresql\*
20
+ - sudo apt-get install -y postgresql-11 postgresql-client-11
21
+ - sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf
22
+ - sudo service postgresql restart 11
21
23
 
22
24
  before_script:
23
25
  - gem update --system
24
- - RAILS_ENV=test bundle exec rake dummy:db:create dummy:db:schema:load
26
+ - psql -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB;' -U postgres
27
+ - ./bin/setup
28
+
29
+ matrix:
30
+ include:
31
+ - rvm: 2.5.1
32
+ gemfile: gemfiles/rails5.gemfile
33
+ - rvm: 2.5.5
34
+ gemfile: gemfiles/rails52.gemfile
35
+ - rvm: 2.6.2
36
+ gemfile: gemfiles/rails6.gemfile
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- metka (0.1.1)
4
+ metka (1.0.0)
5
5
  dry-configurable
6
- rails (>= 4.2)
6
+ rails (~> 5.1)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
@@ -52,27 +52,29 @@ GEM
52
52
  arel (8.0.0)
53
53
  ast (2.4.0)
54
54
  builder (3.2.3)
55
+ coderay (1.1.2)
55
56
  concurrent-ruby (1.1.5)
56
- crass (1.0.4)
57
+ crass (1.0.5)
58
+ database_cleaner (1.7.0)
57
59
  diff-lcs (1.3)
58
- dry-configurable (0.8.3)
60
+ dry-configurable (0.9.0)
59
61
  concurrent-ruby (~> 1.0)
60
62
  dry-core (~> 0.4, >= 0.4.7)
61
63
  dry-core (0.4.9)
62
64
  concurrent-ruby (~> 1.0)
63
- erubi (1.8.0)
64
- faker (2.3.0)
65
- i18n (~> 1.6.0)
65
+ erubi (1.9.0)
66
+ faker (2.8.0)
67
+ i18n (>= 1.6, < 1.8)
66
68
  globalid (0.4.2)
67
69
  activesupport (>= 4.2.0)
68
- i18n (1.6.0)
70
+ i18n (1.7.0)
69
71
  concurrent-ruby (~> 1.0)
70
- jaro_winkler (1.5.3)
71
- jetrockets-standard (1.0.1)
72
+ jaro_winkler (1.5.4)
73
+ jetrockets-standard (1.0.4)
72
74
  rubocop-rails (~> 2.3.2)
73
75
  rubocop-rspec (~> 1.35.0)
74
76
  standard (~> 0.1.4)
75
- loofah (2.2.3)
77
+ loofah (2.4.0)
76
78
  crass (~> 1.0.2)
77
79
  nokogiri (>= 1.5.9)
78
80
  mail (2.7.1)
@@ -80,14 +82,17 @@ GEM
80
82
  method_source (0.9.2)
81
83
  mini_mime (1.0.2)
82
84
  mini_portile2 (2.4.0)
83
- minitest (5.11.3)
84
- nio4r (2.5.1)
85
- nokogiri (1.10.4)
85
+ minitest (5.13.0)
86
+ nio4r (2.5.2)
87
+ nokogiri (1.10.7)
86
88
  mini_portile2 (~> 2.4.0)
87
- parallel (1.17.0)
88
- parser (2.6.4.1)
89
+ parallel (1.19.1)
90
+ parser (2.6.5.0)
89
91
  ast (~> 2.4.0)
90
92
  pg (1.1.4)
93
+ pry (0.12.2)
94
+ coderay (~> 1.1.0)
95
+ method_source (~> 0.9.0)
91
96
  rack (2.0.7)
92
97
  rack-test (1.1.0)
93
98
  rack (>= 1.0, < 3)
@@ -106,8 +111,8 @@ GEM
106
111
  rails-dom-testing (2.0.3)
107
112
  activesupport (>= 4.2.0)
108
113
  nokogiri (>= 1.6)
109
- rails-html-sanitizer (1.2.0)
110
- loofah (~> 2.2, >= 2.2.2)
114
+ rails-html-sanitizer (1.3.0)
115
+ loofah (~> 2.3)
111
116
  railties (5.1.7)
112
117
  actionpack (= 5.1.7)
113
118
  activesupport (= 5.1.7)
@@ -115,36 +120,36 @@ GEM
115
120
  rake (>= 0.8.7)
116
121
  thor (>= 0.18.1, < 2.0)
117
122
  rainbow (3.0.0)
118
- rake (12.3.3)
119
- rspec (3.8.0)
120
- rspec-core (~> 3.8.0)
121
- rspec-expectations (~> 3.8.0)
122
- rspec-mocks (~> 3.8.0)
123
- rspec-core (3.8.2)
124
- rspec-support (~> 3.8.0)
125
- rspec-expectations (3.8.4)
123
+ rake (13.0.1)
124
+ rspec (3.9.0)
125
+ rspec-core (~> 3.9.0)
126
+ rspec-expectations (~> 3.9.0)
127
+ rspec-mocks (~> 3.9.0)
128
+ rspec-core (3.9.0)
129
+ rspec-support (~> 3.9.0)
130
+ rspec-expectations (3.9.0)
126
131
  diff-lcs (>= 1.2.0, < 2.0)
127
- rspec-support (~> 3.8.0)
128
- rspec-mocks (3.8.1)
132
+ rspec-support (~> 3.9.0)
133
+ rspec-mocks (3.9.0)
129
134
  diff-lcs (>= 1.2.0, < 2.0)
130
- rspec-support (~> 3.8.0)
131
- rspec-rails (3.8.2)
135
+ rspec-support (~> 3.9.0)
136
+ rspec-rails (3.9.0)
132
137
  actionpack (>= 3.0)
133
138
  activesupport (>= 3.0)
134
139
  railties (>= 3.0)
135
- rspec-core (~> 3.8.0)
136
- rspec-expectations (~> 3.8.0)
137
- rspec-mocks (~> 3.8.0)
138
- rspec-support (~> 3.8.0)
139
- rspec-support (3.8.2)
140
- rubocop (0.72.0)
140
+ rspec-core (~> 3.9.0)
141
+ rspec-expectations (~> 3.9.0)
142
+ rspec-mocks (~> 3.9.0)
143
+ rspec-support (~> 3.9.0)
144
+ rspec-support (3.9.0)
145
+ rubocop (0.75.1)
141
146
  jaro_winkler (~> 1.5.1)
142
147
  parallel (~> 1.10)
143
148
  parser (>= 2.6)
144
149
  rainbow (>= 2.2.2, < 4.0)
145
150
  ruby-progressbar (~> 1.7)
146
151
  unicode-display_width (>= 1.4.0, < 1.7)
147
- rubocop-performance (1.4.1)
152
+ rubocop-performance (1.5.1)
148
153
  rubocop (>= 0.71.0)
149
154
  rubocop-rails (2.3.2)
150
155
  rack (>= 1.1)
@@ -152,16 +157,16 @@ GEM
152
157
  rubocop-rspec (1.35.0)
153
158
  rubocop (>= 0.60.0)
154
159
  ruby-progressbar (1.10.1)
155
- sprockets (3.7.2)
160
+ sprockets (4.0.0)
156
161
  concurrent-ruby (~> 1.0)
157
162
  rack (> 1, < 3)
158
163
  sprockets-rails (3.2.1)
159
164
  actionpack (>= 4.0)
160
165
  activesupport (>= 4.0)
161
166
  sprockets (>= 3.0.0)
162
- standard (0.1.4)
163
- rubocop (~> 0.72.0)
164
- rubocop-performance (~> 1.4.0)
167
+ standard (0.1.6)
168
+ rubocop (~> 0.75.0)
169
+ rubocop-performance (~> 1.5.0)
165
170
  thor (0.20.3)
166
171
  thread_safe (0.3.6)
167
172
  timecop (0.9.1)
@@ -179,13 +184,15 @@ DEPENDENCIES
179
184
  activerecord (~> 5.1.1)
180
185
  ammeter
181
186
  bundler
187
+ database_cleaner
182
188
  faker
183
189
  jetrockets-standard (~> 1.0.1)
184
190
  metka!
185
191
  pg
192
+ pry (~> 0.12.2)
186
193
  rake
187
- rspec
188
- rspec-rails
194
+ rspec (~> 3.9)
195
+ rspec-rails (~> 3.9)
189
196
  timecop
190
197
 
191
198
  BUNDLED WITH
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # Metka
5
5
 
6
- Rails gem to manage tags with PostgreSQL array columns.
6
+ Rails gem to manage tags with SonggreSQL array columns.
7
7
 
8
8
  ## Installation
9
9
 
@@ -24,33 +24,109 @@ Or install it yourself as:
24
24
  ## Tag objects
25
25
 
26
26
  ```ruby
27
- class Post < ActiveRecord::Base
28
- include Metka::Model
29
-
27
+ class Song < ActiveRecord::Base
28
+ include Metka::Model(column: 'tags')
29
+ include Metka::Model(column: 'genres')
30
30
  end
31
31
 
32
- @post = Post.new(title: 'Migrate tags in Rails to PostgreSQL')
33
- @post.tags = ['ruby', 'postgres', 'rails']
34
- @post.save
32
+ @Song = Song.new(title: 'Migrate tags in Rails to SonggreSQL')
33
+ @Song.tag_list = 'top, chill'
34
+ @Song.genre_list = 'rock, jazz, pop'
35
+ @Song.save
35
36
  ```
36
37
 
37
38
  ## Find tagged objects
38
39
 
40
+ ### .with_all_#{column_name}
41
+ ```ruby
42
+ Song.with_all_tags('top')
43
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
44
+
45
+ Song.with_all_tags('top, 1990')
46
+ => []
47
+
48
+ Song.with_all_tags('')
49
+ => []
50
+
51
+ Song.with_all_genres('rock')
52
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
53
+ ```
54
+
55
+ ### .with_any_#{column_name}
56
+ ```ruby
57
+ Song.with_any_tags('chill')
58
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
59
+
60
+ Song.with_any_tags('chill, 1980')
61
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
62
+
63
+ Song.with_any_tags('')
64
+ => []
65
+
66
+ Song.with_any_genres('rock, rap')
67
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
68
+ ```
69
+ ### .without_all_#{column_name}
70
+ ```ruby
71
+ Song.without_all_tags('top')
72
+ => []
73
+
74
+ Song.without_all_tags('top, 1990')
75
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
76
+
77
+ Song.without_all_tags('')
78
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
79
+
80
+ Song.without_all_genres('rock, pop')
81
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
82
+
83
+ Song.without_all_genres('rock')
84
+ => []
85
+ ```
86
+
87
+ ### .without_any_#{column_name}
39
88
  ```ruby
40
- Post.tagged_with('ruby')
41
- => [#<Post id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['ruby', 'postgres', 'rails']
89
+ Song.without_any_tags('top, 1990')
90
+ => []
91
+
92
+ Song.without_any_tags('1990, 1980')
93
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
42
94
 
43
- Post.tagged_with('ruby, crystal')
44
- => nil
95
+ Song.without_any_genres('rock, pop')
96
+ => []
97
+
98
+ Song.without_any_genres('')
99
+ => [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
45
100
  ```
46
101
 
47
- In example above you will get records that are tagged with `ruby` and `crystal`. To get records that are tagged with any of these tags use `any` option.
102
+ ## Custom delimiter
103
+ By default, a comma is used as a delimiter to create tags from a string.
104
+ You can make your own custom separator:
105
+ ```ruby
106
+ Metka.config.delimiter = [',', ' ', '\|']
107
+ parsed_data = Metka::GenericParser.instance.call('cool, data|I have')
108
+ parsed_data.to_a
109
+ =>['cool', 'data', 'I', 'have']
110
+ ```
48
111
 
112
+ ## Tags with quote
49
113
  ```ruby
50
- Post.tagged_with('ruby, crystal', any: true)
51
- => [#<Post id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['ruby', 'postgres', 'rails']
114
+ parsed_data = Metka::GenericParser.instance.call("'cool, data', code")
115
+ parsed_data.to_a
116
+ => ['cool, data', 'code']
52
117
  ```
53
118
 
119
+ ## Custom parser
120
+ By default we use [generic_parser](lib/metka/generic_parser.rb "generic_parser")
121
+ If you want use your custom parser you can do:
122
+ ```ruby
123
+ class Song < ActiveRecord::Base
124
+ include Metka::Model(column: 'tags', parser: Your::Custom::Parser.instance)
125
+ include Metka::Model(column: 'genres')
126
+ end
127
+ ```
128
+ Custom parser must be a singleton class that has a `.call` method that accepts the tag string
129
+
54
130
  ## Tag Cloud Strategies
55
131
 
56
132
  There are several strategies to get tag statistics
@@ -123,7 +199,31 @@ Now you can create `TaggedNote` model and work with the view like you usually do
123
199
 
124
200
  Similar to the strategy above, but the view will be Materialized and refreshed with the trigger
125
201
 
126
- TBD
202
+ ```bash
203
+ rails g metka:strategies:materialized_view --source-table-name=NAME_OF_TABLE_WITH_TAGS
204
+ ```
205
+
206
+ The code above will generate a migration that creates view to store aggregated data about tag in `NAME_OF_TABLE_WITH_TAGS` table.
207
+
208
+ Lets take a look at real example. We have a `notes` table with `tags` column.
209
+
210
+ | Column | Type | Default |
211
+ |--------|---------------------|-----------------------------------|
212
+ | id | integer | nextval('notes_id_seq'::regclass) |
213
+ | body | text | |
214
+ | tags | character varying[] | '{}'::character varying[] |
215
+
216
+ Now lets generate a migration.
217
+
218
+ ```bash
219
+ rails g metka:strategies:materialized_view --source-table-name=notes
220
+ ```
221
+
222
+ The migration code you can see [here](spec/dummy/db/migrate/05_create_tagged_materialized_view_Songs_materialized_view.rb "here")
223
+
224
+ Now lets take a look at `tagged_notes` materialized view.
225
+
226
+ Now you can create `TaggedNote` model and work with the view like you usually do with Rails models.
127
227
 
128
228
  ### Table Strategy with Triggers
129
229
 
@@ -131,6 +231,11 @@ TBD
131
231
 
132
232
  TBD
133
233
 
234
+ ## Inspired by
235
+ 1. [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on)
236
+ 2. [ActsAsTaggableArrayOn](https://github.com/tmiyamon/acts-as-taggable-array-on)
237
+ 3. [TagColumns](https://github.com/hopsoft/tag_columns)
238
+
134
239
  ## Development
135
240
 
136
241
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -141,6 +246,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
141
246
 
142
247
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/metka. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
143
248
 
249
+ ## Credits
250
+ ![JetRockets](https://jetrockets.pro/jetrockets-icons-black.png)
251
+ Metka is maintained by [JetRockets](http://www.jetrockets.ru).
252
+
144
253
  ## License
145
254
 
146
255
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/setup CHANGED
@@ -5,5 +5,4 @@ set -e
5
5
  gem install bundler --conservative
6
6
  bundle check || bundle install
7
7
 
8
- RAILS_ENV=test bundle exec rake dummy:db:create
9
- RAILS_ENV=test bundle exec rake dummy:db:schema:load
8
+ RAILS_ENV=test bundle exec rake dummy:db:migrate:reset
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~>5.1'
4
+ gem 'activerecord', '~> 5.1'
5
+
6
+ gemspec path: '..'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~>5.2'
4
+ gem 'activerecord', '~> 5.2'
5
+
6
+ gemspec path: '..'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~>6.0'
4
+ gem 'activerecord', '~> 6.0'
5
+
6
+ gemspec path: '..'
@@ -41,7 +41,7 @@ module Metka
41
41
  end
42
42
 
43
43
  def migration_name
44
- "create_#{view_name}_view"
44
+ "create_#{view_name}_materialized_view"
45
45
  end
46
46
 
47
47
  def migration_class_name
@@ -3,38 +3,45 @@
3
3
  class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %>
4
4
  def up
5
5
  execute <<-SQL
6
- CREATE
7
- OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view RETURNS TRIGGER LANGUAGE plpgsql AS $$
6
+ CREATE OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view() RETURNS trigger LANGUAGE plpgsql AS $$
8
7
  BEGIN
9
8
  IF TG_OP = 'INSERT' AND NEW.<%= source_column_name %> IS NOT NULL THEN
10
9
  REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
11
- ELSIF TG_OP = 'UPDATE' AND OLD.<%= source_column_name %> IS NOT NULL AND NEW.<%= source_column_name %> IS NOT NULL THEN
12
- REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
13
- ELSIF TG_OP = 'DELETE' AND OLD.<%= source_column_name %> IS NOT NULL THEN
10
+ END IF;
11
+
12
+ IF TG_OP = 'UPDATE' AND OLD.<%= source_column_name %> != NEW.<%= source_column_name %> THEN
14
13
  REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
15
14
  END IF;
16
15
 
17
- RETURN NULL;
18
- END $$;
16
+ IF TG_OP = 'DELETE' AND OLD.<%= source_column_name %> IS NOT NULL THEN
17
+ REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
18
+ END IF;
19
+ RETURN NEW;
20
+ END $$;
19
21
 
20
- CREATE OR REPLACE MATERIALZIED VIEW <%= view_name %> AS
22
+ DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
23
+ CREATE MATERIALIZED VIEW <%= view_name %> AS
24
+ SELECT UNNEST
25
+ ( <%= source_column_name %> ) AS <%= source_column_name.singularize %>_name,
26
+ COUNT ( * ) AS taggings_count
27
+ FROM
28
+ <%= source_table_name %>
29
+ GROUP BY
30
+ <%= source_column_name.singularize %>_name;
21
31
 
22
- SELECT UNNEST
23
- ( <%= source_column_name %> ) AS <%= source_column_name.singularize %>_name,
24
- COUNT ( * ) AS taggings_count
25
- FROM
26
- <%= source_table_name %>
27
- GROUP BY
28
- <%= source_column_name.singularize %>_name;
32
+ CREATE UNIQUE INDEX idx_<%= source_table_name %>_<%= source_column_name %> ON <%= view_name %>(<%= source_column_name.singularize %>_name);
29
33
 
30
34
  CREATE TRIGGER metka_on_<%= source_table_name %>
31
- BEFORE UPDATE OR INSERT OR DELETE ON <%= source_table_name %> FOR EACH ROW
32
- EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
35
+ AFTER UPDATE OR INSERT OR DELETE ON <%= source_table_name %> FOR EACH ROW
36
+ EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
37
+ SQL
33
38
  end
34
39
 
35
40
  def down
36
41
  execute <<-SQL
37
- DROP VIEW <%= view_name %>;
42
+ DROP TRIGGER IF EXISTS metka_on_<%= source_table_name %> ON <%= source_table_name %>;
43
+ DROP FUNCTION IF EXISTS metka_refresh_<%= view_name %>_materialized_view;
44
+ DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
38
45
  SQL
39
46
  end
40
47
  end
@@ -16,11 +16,37 @@ module Metka
16
16
  TagList.new.tap do |tag_list|
17
17
  case value
18
18
  when String
19
- tag_list.merge value.split(',').map(&:strip).reject(&:empty?)
19
+ value = value.to_s.dup
20
+ gsub_quote_pattern!(tag_list, value, double_quote_pattern)
21
+ gsub_quote_pattern!(tag_list, value, single_quote_pattern)
22
+
23
+ tag_list.merge value.split(Regexp.new joined_delimiter).map(&:strip).reject(&:empty?)
20
24
  when Enumerable
21
25
  tag_list.merge value.reject(&:empty?)
22
26
  end
23
27
  end
24
28
  end
29
+
30
+ private
31
+
32
+ def gsub_quote_pattern!(tag_list, value, pattern)
33
+ value.gsub!(pattern) {
34
+ tag_list.add(Regexp.last_match[2])
35
+ ''
36
+ }
37
+ end
38
+
39
+ def joined_delimiter
40
+ delimeter = Metka.config.delimiter
41
+ delimeter.is_a?(Array) ? delimeter.join('|') : delimeter
42
+ end
43
+
44
+ def single_quote_pattern
45
+ /(\A|#{joined_delimiter})\s*'(.*?)'\s*(?=#{joined_delimiter}\s*|\z)/
46
+ end
47
+
48
+ def double_quote_pattern
49
+ /(\A|#{joined_delimiter})\s*"(.*?)"\s*(?=#{joined_delimiter}\s*|\z)/
50
+ end
25
51
  end
26
52
  end
@@ -1,33 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/concern'
4
-
5
3
  module Metka
6
- # Extends AR model with methods to use tags
7
- module Model
8
- extend ActiveSupport::Concern
9
-
10
- included do
11
- scope :tagged_with, ->(tags, options = {}) do
12
- tag_list = Metka.config.parser.instance.call(tags)
13
- options = options.dup
14
-
15
- return none if tag_list.empty?
4
+ def self.Model(column:, **options)
5
+ Metka::Model.new(column: column, **options)
6
+ end
16
7
 
17
- where(::Metka::QueryBuilder.new.call(self, 'tags', tag_list, options))
18
- end
8
+ class Model < Module
9
+ def initialize(column: , **options)
10
+ @column = column
11
+ @options = options
19
12
  end
20
13
 
21
- def tag_list=(v)
22
- self.tags = Metka.config.parser.instance.call(v).to_a
23
- self.tags = nil if tags.empty?
24
- end
14
+ def included(base)
15
+ column = @column
16
+ parser = ->(tags) {
17
+ @options[:parser] ? @options[:parser].call(tags) : Metka.config.parser.instance.call(tags)
18
+ }
19
+
20
+ search_by_tags = ->(model, tags, column, **options) {
21
+ parsed_tag_list = parser.call(tags)
22
+ if options[:without].present?
23
+ model.where.not(::Metka::QueryBuilder.new.call(model, column, parsed_tag_list, options))
24
+ else
25
+ return model.none if parsed_tag_list.empty?
26
+ model.where(::Metka::QueryBuilder.new.call(model, column, parsed_tag_list, options))
27
+ end
28
+ }
29
+
30
+ base.class_eval do
31
+ scope "with_all_#{column}", ->(tags) { search_by_tags.call(self, tags, column) }
32
+ scope "with_any_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { any: true }) }
33
+ scope "without_all_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { exclude_all: true, without: true }) }
34
+ scope "without_any_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { exclude_any: true, without: true }) }
35
+ end
25
36
 
26
- def tag_list
27
- Metka.config.parser.instance.call(tags)
28
- end
37
+ base.define_method(column.singularize + '_list=') do |v|
38
+ self.write_attribute(column, parser.call(v).to_a)
39
+ self.write_attribute(column, nil) if self.send(column).empty?
40
+ end
29
41
 
30
- module ClassMethods # :nodoc:
42
+ base.define_method(column.singularize + '_list') do
43
+ parser.call(self.send(column))
44
+ end
31
45
  end
32
46
  end
33
- end
47
+ end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'query_builder/exclude_tags_query'
3
+ require_relative 'query_builder/base_query'
4
+ require_relative 'query_builder/exclude_all_tags_query'
5
+ require_relative 'query_builder/exclude_any_tags_query'
4
6
  require_relative 'query_builder/any_tags_query'
5
7
  require_relative 'query_builder/all_tags_query'
6
8
 
7
9
  module Metka
8
10
  class QueryBuilder
9
11
  def call(taggable_model, column, tag_list, options)
10
- if options[:exclude].present?
11
- ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
12
+ if options[:exclude_all].present?
13
+ ExcludeAllTagsQuery.instance.call(taggable_model, column, tag_list)
14
+ elsif options[:exclude_any].present?
15
+ ExcludeAnyTagsQuery.instance.call(taggable_model, column, tag_list)
12
16
  elsif options[:any].present?
13
17
  AnyTagsQuery.instance.call(taggable_model, column, tag_list)
14
18
  else
@@ -1,26 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Metka
4
- class AllTagsQuery
5
- include Singleton
4
+ class AllTagsQuery < BaseQuery
5
+ private
6
6
 
7
- def call(model, column_name, tag_list)
8
- column_cast = Arel::Nodes::NamedFunction.new(
9
- 'CAST',
10
- [model.arel_table[column_name].as('text[]')]
11
- )
12
-
13
- value = Arel::Nodes::SqlLiteral.new(
14
- # In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
15
- ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
16
- )
17
-
18
- value_cast = Arel::Nodes::NamedFunction.new(
19
- 'CAST',
20
- [value.as('text[]')]
21
- )
22
-
23
- Arel::Nodes::InfixOperation.new('@>', column_cast, value_cast)
7
+ def infix_operator
8
+ '@>'
24
9
  end
25
10
  end
26
11
  end
@@ -1,28 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
-
5
3
  module Metka
6
- class AnyTagsQuery
7
- include Singleton
8
-
9
- def call(model, column_name, tag_list)
10
- column_cast = Arel::Nodes::NamedFunction.new(
11
- 'CAST',
12
- [model.arel_table[column_name].as('text[]')]
13
- )
14
-
15
- value = Arel::Nodes::SqlLiteral.new(
16
- # In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
17
- ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
18
- )
19
-
20
- value_cast = Arel::Nodes::NamedFunction.new(
21
- 'CAST',
22
- [value.as('text[]')]
23
- )
4
+ class AnyTagsQuery < BaseQuery
5
+ private
24
6
 
25
- Arel::Nodes::InfixOperation.new('&&', column_cast, value_cast)
7
+ def infix_operator
8
+ '&&'
26
9
  end
27
10
  end
28
11
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Metka
5
+ class BaseQuery
6
+ include Singleton
7
+
8
+ def call(model, column_name, tag_list)
9
+ column_cast = Arel::Nodes::NamedFunction.new(
10
+ 'CAST',
11
+ [model.arel_table[column_name].as('text[]')]
12
+ )
13
+
14
+ value = Arel::Nodes::SqlLiteral.new(
15
+ # In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
16
+ ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
17
+ )
18
+
19
+ value_cast = Arel::Nodes::NamedFunction.new(
20
+ 'CAST',
21
+ [value.as('text[]')]
22
+ )
23
+
24
+ Arel::Nodes::InfixOperation.new(infix_operator, column_cast, value_cast)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metka
4
+ class ExcludeAllTagsQuery< BaseQuery
5
+ private
6
+
7
+ def infix_operator
8
+ '@>'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metka
4
+ class ExcludeAnyTagsQuery< BaseQuery
5
+ private
6
+
7
+ def infix_operator
8
+ '&&'
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Metka
4
- VERSION = '0.1.2'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -7,8 +7,8 @@ require 'metka/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'metka'
9
9
  spec.version = Metka::VERSION
10
- spec.authors = ['Igor Alexandrov']
11
- spec.email = ['igor.alexandrov@gmail.com']
10
+ spec.authors = ['Igor Alexandrov', 'Andrey Morozov']
11
+ spec.email = ['igor.alexandrov@gmail.com', 'andrey.morozov@jetrockets.ru']
12
12
 
13
13
  spec.summary = 'Rails tagging system based on PostgreSQL arrays'
14
14
  spec.description = 'Rails tagging system based on PostgreSQL arrays'
@@ -25,15 +25,17 @@ Gem::Specification.new do |spec|
25
25
  spec.require_paths = ['lib']
26
26
 
27
27
  spec.add_dependency 'dry-configurable'
28
- spec.add_dependency 'rails', '>= 4.2'
28
+ spec.add_dependency 'rails', '~> 5.1'
29
29
 
30
30
  spec.add_development_dependency 'ammeter'
31
+ spec.add_development_dependency 'pry', '~> 0.12.2'
31
32
  spec.add_development_dependency 'bundler'
32
33
  spec.add_development_dependency 'faker'
33
34
  spec.add_development_dependency 'jetrockets-standard', '~> 1.0.1'
34
35
  spec.add_development_dependency 'pg'
35
36
  spec.add_development_dependency 'rake'
36
- spec.add_development_dependency 'rspec'
37
- spec.add_development_dependency 'rspec-rails'
37
+ spec.add_development_dependency 'rspec', '~> 3.9'
38
+ spec.add_development_dependency 'rspec-rails', '~> 3.9'
38
39
  spec.add_development_dependency 'timecop'
40
+ spec.add_development_dependency 'database_cleaner'
39
41
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Alexandrov
8
+ - Andrey Morozov
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2019-10-29 00:00:00.000000000 Z
12
+ date: 2019-12-13 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: dry-configurable
@@ -28,16 +29,16 @@ dependencies:
28
29
  name: rails
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - ">="
32
+ - - "~>"
32
33
  - !ruby/object:Gem::Version
33
- version: '4.2'
34
+ version: '5.1'
34
35
  type: :runtime
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
- - - ">="
39
+ - - "~>"
39
40
  - !ruby/object:Gem::Version
40
- version: '4.2'
41
+ version: '5.1'
41
42
  - !ruby/object:Gem::Dependency
42
43
  name: ammeter
43
44
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +53,20 @@ dependencies:
52
53
  - - ">="
53
54
  - !ruby/object:Gem::Version
54
55
  version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 0.12.2
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 0.12.2
55
70
  - !ruby/object:Gem::Dependency
56
71
  name: bundler
57
72
  requirement: !ruby/object:Gem::Requirement
@@ -126,18 +141,32 @@ dependencies:
126
141
  name: rspec
127
142
  requirement: !ruby/object:Gem::Requirement
128
143
  requirements:
129
- - - ">="
144
+ - - "~>"
130
145
  - !ruby/object:Gem::Version
131
- version: '0'
146
+ version: '3.9'
132
147
  type: :development
133
148
  prerelease: false
134
149
  version_requirements: !ruby/object:Gem::Requirement
135
150
  requirements:
136
- - - ">="
151
+ - - "~>"
137
152
  - !ruby/object:Gem::Version
138
- version: '0'
153
+ version: '3.9'
139
154
  - !ruby/object:Gem::Dependency
140
155
  name: rspec-rails
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '3.9'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '3.9'
168
+ - !ruby/object:Gem::Dependency
169
+ name: timecop
141
170
  requirement: !ruby/object:Gem::Requirement
142
171
  requirements:
143
172
  - - ">="
@@ -151,7 +180,7 @@ dependencies:
151
180
  - !ruby/object:Gem::Version
152
181
  version: '0'
153
182
  - !ruby/object:Gem::Dependency
154
- name: timecop
183
+ name: database_cleaner
155
184
  requirement: !ruby/object:Gem::Requirement
156
185
  requirements:
157
186
  - - ">="
@@ -167,6 +196,7 @@ dependencies:
167
196
  description: Rails tagging system based on PostgreSQL arrays
168
197
  email:
169
198
  - igor.alexandrov@gmail.com
199
+ - andrey.morozov@jetrockets.ru
170
200
  executables: []
171
201
  extensions: []
172
202
  extra_rdoc_files: []
@@ -184,6 +214,9 @@ files:
184
214
  - Rakefile
185
215
  - bin/console
186
216
  - bin/setup
217
+ - gemfiles/rails5.gemfile
218
+ - gemfiles/rails52.gemfile
219
+ - gemfiles/rails6.gemfile
187
220
  - lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb
188
221
  - lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb
189
222
  - lib/generators/metka/strategies/view/templates/migration.rb.erb
@@ -194,7 +227,9 @@ files:
194
227
  - lib/metka/query_builder.rb
195
228
  - lib/metka/query_builder/all_tags_query.rb
196
229
  - lib/metka/query_builder/any_tags_query.rb
197
- - lib/metka/query_builder/exclude_tags_query.rb
230
+ - lib/metka/query_builder/base_query.rb
231
+ - lib/metka/query_builder/exclude_all_tags_query.rb
232
+ - lib/metka/query_builder/exclude_any_tags_query.rb
198
233
  - lib/metka/tag_list.rb
199
234
  - lib/metka/version.rb
200
235
  - metka.gemspec
@@ -217,7 +252,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
252
  - !ruby/object:Gem::Version
218
253
  version: '0'
219
254
  requirements: []
220
- rubygems_version: 3.0.6
255
+ rubyforge_project:
256
+ rubygems_version: 2.7.6
221
257
  signing_key:
222
258
  specification_version: 4
223
259
  summary: Rails tagging system based on PostgreSQL arrays
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Metka
4
- class ExcludeTagsQuery
5
- end
6
- end