pg_taggable 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec57f99287e3bb16b29e459e42c0f3c5a24d6f967917906d88dadfe491d133e3
4
+ data.tar.gz: 525995c96c17957ba46a5025d250c1703efec6da6841277864befd0278072449
5
+ SHA512:
6
+ metadata.gz: caa213bb0b77a0c5f71922337563d9bf3222cf98a17571e90f700b5fabf5b345c1c4494c71c45889e8e081b1c0c44c200fa4c228638408d39d2039d26b5bca73
7
+ data.tar.gz: b4760ae519a53f6b414e5578ac6ec241b30b3342acdc82057a9a437599c65ce71bd473ef2be8ccd8c6ee99bff393fc775e647fee75ed5333f519b1ddb8617eda
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Yi-Cyuan Chen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # PgTaggable
2
+ A simple tagging gem for Rails using PostgreSQL array.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "pg_taggable"
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ ```
15
+
16
+ Or install it yourself as:
17
+ ```bash
18
+ $ gem install pg_taggable
19
+ ```
20
+
21
+ ## Usage
22
+ ### Setup
23
+ Add array columns and index to your table. For example:
24
+ ```Ruby
25
+ class CreatePosts < ActiveRecord::Migration[8.0]
26
+ def change
27
+ create_table :posts do |t|
28
+ t.string :tags, array: true, default: []
29
+
30
+ t.timestamps
31
+
32
+ t.index :tags, using: 'gin'
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ Indicate that attribute is "taggable" in a Rails model, like this:
39
+ ```Ruby
40
+ class Post < ActiveRecord::Base
41
+ taggable :tags
42
+ end
43
+ ```
44
+
45
+ ### Modify
46
+ You can modify it like normal array
47
+ ```Ruby
48
+ #set
49
+ post.tags = ['food', 'travel']
50
+
51
+ #add
52
+ post.tags += ['food']
53
+ post.tags += ['food', 'travel']
54
+ post.tags << 'food'
55
+
56
+ #remove
57
+ post.tags -= ['food']
58
+ ```
59
+
60
+ ### Queries
61
+ #### any_#{tag_name}
62
+ Find records with any of the tags.
63
+ ```Ruby
64
+ Post.where(any_tags: ['food', 'travel'])
65
+ ```
66
+
67
+ You can use with `not`
68
+ ```Ruby
69
+ Post.where.not(any_tags: ['food', 'travel'])
70
+ ```
71
+
72
+ #### all_#{tag_name}
73
+ Find records with all of the tags
74
+ ```Ruby
75
+ Post.where(all_tags: ['food', 'travel'])
76
+ ```
77
+
78
+ #### #{tag_name}_in
79
+ Find records that have all the tags included in the list
80
+ ```Ruby
81
+ Post.where(tags_in: ['food', 'travel'])
82
+ ```
83
+
84
+ #### #{tag_name}_eq
85
+ Find records that have exact same tags as the list, order is not important
86
+ ```Ruby
87
+ Post.where(tags_eq: ['food', 'travel'])
88
+ ```
89
+
90
+ #### #{tag_name}
91
+ Find records that have exact same tags as the list, order is important. This is the default behavior.
92
+ ```Ruby
93
+ Post.where(tags: ['food', 'travel'])
94
+ ```
95
+
96
+ Assume a post has tags: 'A', 'B'
97
+ |Method|Query|Matched|
98
+ |-|-|-|
99
+ |any_tags|A|True|
100
+ |any_tags|A, B|True|
101
+ |any_tags|B, A|True|
102
+ |any_tags|A, B, C|True|
103
+ |all_tags|A|True|
104
+ |all_tags|A, B|True|
105
+ |all_tags|B, A|True|
106
+ |all_tags|A, B, C|False|
107
+ |tags_in|A|False|
108
+ |tags_in|A, B|True|
109
+ |tags_in|B, A|True|
110
+ |tags_in|A, B, C|True|
111
+ |tags_eq|A|False|
112
+ |tags_eq|A, B|True|
113
+ |tags_eq|B, A|True|
114
+ |tags_eq|A, B, C|False|
115
+ |tags|A|False|
116
+ |tags|A, B|True|
117
+ |tags|B, A|False|
118
+ |tags|A, B, C|False|
119
+
120
+ ### Class Methods
121
+ #### taggable(name, unique: true)
122
+ Indicate that attribute is "taggable".
123
+
124
+ #### unique: true
125
+ You can use `unique` option to ensure that tags are unique. It will be deduplicated before saving. The default is `true`.
126
+
127
+ ```Ruby
128
+ # taggable :tags, unique: true
129
+ post = Post.create(tags: ['food', 'travel', 'food'])
130
+ post.tags
131
+ # => ['food', 'travel']
132
+
133
+ # taggable :tags, unique: false
134
+ post = Post.create(tags: ['food', 'travel', 'food'])
135
+ post.tags
136
+ # => ['food', 'travel', 'food']
137
+ ```
138
+
139
+ #### #{tag_name}
140
+ Return unnested tags. The column name will be `tag`, For example:
141
+ ```Ruby
142
+ Post.tags
143
+ # => #<ActiveRecord::Relation [#<Post tag: "food", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "technology", id: nil>]>
144
+
145
+ Post.tags.size
146
+ # => 4
147
+
148
+ Post.tags.distinct.size
149
+ # => 3
150
+
151
+ Post.tags.distinct.pluck(:tag)
152
+ # => ["food", "travel", "technology"]
153
+
154
+ Post.tags.group(:tag).count
155
+ # => {"food"=>1, "travel"=>2, "technology"=>1}
156
+ ```
157
+
158
+ #### uniq_#{tag_name}
159
+ Return an array of unique tag strings.
160
+ ```Ruby
161
+ Post.uniq_tags
162
+ # => ["food", "travel", "technology"]
163
+
164
+ # equal to
165
+ Post.tags.distinct.pluck(:tag)
166
+ ```
167
+
168
+ #### count_#{tag_name}
169
+ Calculates the number of occurrences of each tag.
170
+ ```Ruby
171
+ Post.count_tags
172
+ # => {"food"=>1, "travel"=>2, "technology"=>1}
173
+
174
+ # equal to
175
+ Post.tags.group(:tag).count
176
+ ```
177
+
178
+ ### Case Insensitive
179
+ If you use `string` type, it is case sensitive.
180
+ ```Ruby
181
+ # tags is string[]
182
+ post = Post.create(tags: ['food', 'travel', 'Food'])
183
+ post.tags
184
+ # => ['food', 'travel', 'Food']
185
+ ```
186
+
187
+ If you want case insensitive, you need to use `citext`
188
+ ```Ruby
189
+ class CreatePosts < ActiveRecord::Migration[8.0]
190
+ enable_extension('citext') unless extensions.include?('citext')
191
+
192
+ def change
193
+ create_table :posts do |t|
194
+ t.citext :tags, array: true, default: []
195
+
196
+ t.timestamps
197
+
198
+ t.index :tags, using: 'gin'
199
+ end
200
+ end
201
+ end
202
+ ```
203
+
204
+ You will get the diffent result
205
+ ```Ruby
206
+ # tags is citext[]
207
+ post = Post.create(tags: ['food', 'travel', 'Food'])
208
+ post.tags
209
+ # => ['food', 'travel']
210
+ ```
211
+
212
+ ## License
213
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
214
+
215
+ ## Contact
216
+ The project's website is located at https://github.com/emn178/pg_taggable
217
+ Author: Chen, Yi-Cyuan (emn178@gmail.com)
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'bundler/gem_tasks'
@@ -0,0 +1,29 @@
1
+ module PgTaggable
2
+ class PredicateHandler < ActiveRecord::PredicateBuilder::ArrayHandler
3
+ attr_reader :taggable_attributes
4
+
5
+ def initialize(predicate_builder, taggable_attributes)
6
+ @taggable_attributes = taggable_attributes
7
+ super(predicate_builder)
8
+ end
9
+
10
+ def call(attribute, query)
11
+ taggable_attribute = taggable_attributes[attribute.name]
12
+ if taggable_attribute
13
+ attribute.name, type, operator = taggable_attribute
14
+ query_attribute = ActiveRecord::Relation::QueryAttribute.new(attribute.name, query, type)
15
+ bind_param = Arel::Nodes::BindParam.new(query_attribute)
16
+ if operator == '='
17
+ operator = '@>'
18
+ Arel::Nodes::NamedFunction.new('ARRAY_LENGTH', [attribute, 1]).eq(query.size).and(
19
+ Arel::Nodes::InfixOperation.new(operator, attribute, bind_param)
20
+ )
21
+ else
22
+ Arel::Nodes::InfixOperation.new(operator, attribute, bind_param)
23
+ end
24
+ else
25
+ super(attribute, query)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ module PgTaggable
2
+ module Taggable
3
+ @@taggable_attributes = nil
4
+
5
+ def predicate_builder
6
+ register_taggable_handler
7
+ super
8
+ end
9
+
10
+ def register_taggable_handler
11
+ return if @register_taggable_handler || !@@taggable_attributes
12
+
13
+ @register_taggable_handler = true
14
+ predicate_builder.register_handler(Array, PgTaggable::PredicateHandler.new(predicate_builder, @@taggable_attributes))
15
+ end
16
+
17
+ def taggable(name, unique: true)
18
+ type = type_for_attribute(name)
19
+ @@taggable_attributes ||= {}
20
+ @@taggable_attributes = @@taggable_attributes.merge(
21
+ "any_#{name}" => [ name, type, '&&' ],
22
+ "all_#{name}" => [ name, type, '@>' ],
23
+ "#{name}_in" => [ name, type, '<@' ],
24
+ "#{name}_eq" => [ name, type, '=' ]
25
+ )
26
+
27
+ if unique
28
+ if type.type == :citext
29
+ before_save { assign_attributes(name => read_attribute(name).uniq { |t| t.downcase }) }
30
+ else
31
+ before_save { assign_attributes(name => read_attribute(name).uniq) }
32
+ end
33
+ end
34
+
35
+ scope name, -> { unscope(:where).from(select("UNNEST(#{table_name}.#{name}) AS tag"), table_name) }
36
+ scope "uniq_#{name}", -> { public_send(name).distinct.pluck(:tag) }
37
+ scope "count_#{name}", -> { public_send(name).group(:tag).count }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module PgTaggable
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'pg_taggable/version'
2
+ require 'pg_taggable/predicate_handler'
3
+ require 'pg_taggable/taggable'
4
+
5
+ module PgTaggable
6
+ end
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ extend PgTaggable::Taggable
10
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_taggable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yi-Cyuan Chen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 8.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 8.0.0
27
+ description: A simple tagging gem for Rails using PostgreSQL array.
28
+ email:
29
+ - emn178@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/pg_taggable.rb
38
+ - lib/pg_taggable/predicate_handler.rb
39
+ - lib/pg_taggable/taggable.rb
40
+ - lib/pg_taggable/version.rb
41
+ homepage: https://github.com/emn178/pg_taggable
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/emn178/pg_taggable
46
+ source_code_uri: https://github.com/emn178/pg_taggable
47
+ changelog_uri: https://github.com/emn178/pg_taggable/blob/master/CHANGELOG.md
48
+ rubygems_mfa_required: 'true'
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.5.22
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: A simple tagging gem for Rails using PostgreSQL array.
68
+ test_files: []