has_attached_tags 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b963c216d2f4441257005083da80e8d39d7eb4403ca5582e59845d3f573ead36
4
+ data.tar.gz: 6cf7ac60aa5a60f40650343dc5b019cb34e962c2bc7edacb634e64c1ced7ad42
5
+ SHA512:
6
+ metadata.gz: 799a39fe762d20004310b0dc6bf3da80e84c3bc6f06e527ab59c755ebcc6a3b04431a171b0fef70a6cfd1fe5305b6984aa667f9f79c751c8ca39ea7b80f4deb9
7
+ data.tar.gz: f55c759afd4e770c6ba5000b8a954b239c05be81f0bb8a8ec0fd08d3697bfaad26aac6e9e1fe7394e8323fab5dfdad4027d31dbfac23701d67367099f3f695a5
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,152 @@
1
+ require:
2
+ - rubocop-performance
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ DefaultFormatter: progress
7
+ DisplayCopNames: true
8
+ ExtraDetails: true
9
+ TargetRubyVersion: 2.6
10
+ Exclude:
11
+ - 'bin/*'
12
+
13
+ Layout/AccessModifierIndentation:
14
+ EnforcedStyle: outdent
15
+
16
+ Layout/EmptyLinesAroundAttributeAccessor:
17
+ Enabled: true
18
+
19
+ Layout/ExtraSpacing:
20
+ AllowForAlignment: true
21
+
22
+ Layout/HashAlignment:
23
+ EnforcedColonStyle: table
24
+ EnforcedHashRocketStyle: table
25
+
26
+ Layout/LineLength:
27
+ Max: 120
28
+
29
+ Layout/MultilineMethodCallIndentation:
30
+ EnforcedStyle: indented
31
+
32
+ Layout/SpaceAroundMethodCallOperator:
33
+ Enabled: true
34
+
35
+ Layout/SpaceInLambdaLiteral:
36
+ EnforcedStyle: require_space
37
+
38
+ Lint/AmbiguousBlockAssociation:
39
+ Enabled: false
40
+
41
+ Lint/DeprecatedOpenSSLConstant:
42
+ Enabled: true
43
+
44
+ Lint/DuplicateElsifCondition:
45
+ Enabled: true
46
+
47
+ Lint/MixedRegexpCaptureTypes:
48
+ Enabled: true
49
+
50
+ Lint/RaiseException:
51
+ Enabled: true
52
+
53
+ Lint/StructNewOverride:
54
+ Enabled: true
55
+
56
+ Metrics/BlockLength:
57
+ Exclude:
58
+ - 'spec/**/*'
59
+
60
+ Naming/RescuedExceptionsVariableName:
61
+ PreferredName: error
62
+
63
+ Performance/AncestorsInclude:
64
+ Enabled: true
65
+
66
+ Performance/BigDecimalWithNumericArgument:
67
+ Enabled: true
68
+
69
+ Performance/RedundantSortBlock:
70
+ Enabled: true
71
+
72
+ Performance/RedundantStringChars:
73
+ Enabled: true
74
+
75
+ Performance/ReverseFirst:
76
+ Enabled: true
77
+
78
+ Performance/SortReverse:
79
+ Enabled: true
80
+
81
+ Performance/Squeeze:
82
+ Enabled: true
83
+
84
+ Performance/StringInclude:
85
+ Enabled: true
86
+
87
+ RSpec/ExpectChange:
88
+ EnforcedStyle: block
89
+
90
+ RSpec/HookArgument:
91
+ EnforcedStyle: each
92
+
93
+ RSpec/NestedGroups:
94
+ Max: 5
95
+
96
+ Style/AccessorGrouping:
97
+ Enabled: true
98
+
99
+ Style/ArrayCoercion:
100
+ Enabled: true
101
+
102
+ Style/BisectedAttrAccessor:
103
+ Enabled: true
104
+
105
+ Style/CaseLikeIf:
106
+ Enabled: true
107
+
108
+ Style/ClassAndModuleChildren:
109
+ SafeAutoCorrect: true
110
+
111
+ Style/Documentation:
112
+ Enabled: false
113
+
114
+ Style/ExponentialNotation:
115
+ Enabled: true
116
+
117
+ Style/FrozenStringLiteralComment:
118
+ Safe: true
119
+
120
+ Style/HashAsLastArrayItem:
121
+ Enabled: true
122
+
123
+ Style/HashEachMethods:
124
+ Enabled: true
125
+
126
+ Style/HashLikeCase:
127
+ Enabled: true
128
+
129
+ Style/HashTransformKeys:
130
+ Enabled: true
131
+
132
+ Style/HashTransformValues:
133
+ Enabled: true
134
+
135
+ Style/RedundantAssignment:
136
+ Enabled: true
137
+
138
+ Style/RedundantFetchBlock:
139
+ Enabled: true
140
+ Safe: true
141
+
142
+ Style/RedundantFileExtensionInRequire:
143
+ Enabled: true
144
+
145
+ Style/RedundantRegexpCharacterClass:
146
+ Enabled: true
147
+
148
+ Style/RedundantRegexpEscape:
149
+ Enabled: true
150
+
151
+ Style/SlicingWithRange:
152
+ Enabled: true
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.5
7
+ before_install: gem install bundler -v 2.0.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at mihail@platterz.ca. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in has_attached_tags.gemspec
6
+ gemspec
@@ -0,0 +1,90 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ has_attached_tags (0.1.0)
5
+ activerecord (>= 5.1, < 7)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.0.3.2)
11
+ activesupport (= 6.0.3.2)
12
+ activerecord (6.0.3.2)
13
+ activemodel (= 6.0.3.2)
14
+ activesupport (= 6.0.3.2)
15
+ activesupport (6.0.3.2)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 0.7, < 2)
18
+ minitest (~> 5.1)
19
+ tzinfo (~> 1.1)
20
+ zeitwerk (~> 2.2, >= 2.2.2)
21
+ ast (2.4.1)
22
+ concurrent-ruby (1.1.6)
23
+ diff-lcs (1.4.4)
24
+ factory_bot (6.1.0)
25
+ activesupport (>= 5.0.0)
26
+ faker (2.13.0)
27
+ i18n (>= 1.6, < 2)
28
+ i18n (1.8.4)
29
+ concurrent-ruby (~> 1.0)
30
+ minitest (5.14.1)
31
+ parallel (1.19.2)
32
+ parser (2.7.1.4)
33
+ ast (~> 2.4.1)
34
+ rainbow (3.0.0)
35
+ rake (13.0.1)
36
+ regexp_parser (1.7.1)
37
+ rexml (3.2.4)
38
+ rspec (3.9.0)
39
+ rspec-core (~> 3.9.0)
40
+ rspec-expectations (~> 3.9.0)
41
+ rspec-mocks (~> 3.9.0)
42
+ rspec-core (3.9.2)
43
+ rspec-support (~> 3.9.3)
44
+ rspec-expectations (3.9.2)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.9.0)
47
+ rspec-mocks (3.9.1)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.9.0)
50
+ rspec-support (3.9.3)
51
+ rubocop (0.88.0)
52
+ parallel (~> 1.10)
53
+ parser (>= 2.7.1.1)
54
+ rainbow (>= 2.2.2, < 4.0)
55
+ regexp_parser (>= 1.7)
56
+ rexml
57
+ rubocop-ast (>= 0.1.0, < 1.0)
58
+ ruby-progressbar (~> 1.7)
59
+ unicode-display_width (>= 1.4.0, < 2.0)
60
+ rubocop-ast (0.2.0)
61
+ parser (>= 2.7.0.1)
62
+ rubocop-performance (1.7.1)
63
+ rubocop (>= 0.82.0)
64
+ rubocop-rspec (1.42.0)
65
+ rubocop (>= 0.87.0)
66
+ ruby-progressbar (1.10.1)
67
+ sqlite3 (1.4.2)
68
+ thread_safe (0.3.6)
69
+ tzinfo (1.2.7)
70
+ thread_safe (~> 0.1)
71
+ unicode-display_width (1.7.0)
72
+ zeitwerk (2.4.0)
73
+
74
+ PLATFORMS
75
+ ruby
76
+
77
+ DEPENDENCIES
78
+ bundler (~> 2.0)
79
+ factory_bot (~> 6.1)
80
+ faker (~> 2, >= 2.13)
81
+ has_attached_tags!
82
+ rake (~> 13.0)
83
+ rspec (~> 3.0)
84
+ rubocop (~> 0.88)
85
+ rubocop-performance (~> 1.7)
86
+ rubocop-rspec (~> 1.42)
87
+ sqlite3 (~> 1.4)
88
+
89
+ BUNDLED WITH
90
+ 2.0.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Mihail K
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,186 @@
1
+ # HasAttachedTags
2
+
3
+ Provides a general-purpose, typed tagging mechanism for ActiveRecord models.
4
+
5
+ HasAttachedTags does:
6
+ * Use tag types (e.g. Allowing a "Jane" character tag to be separate from a "Jane" artist tag.)
7
+ * Allow a model to have multiple, separate lists of tags
8
+ * Allow a model to specify only a single tag, rather than a list
9
+ * Allow a model to have multiple lists of the same kind of tag (e.g. A user preferences object with an allow-list and a block-list of tags.)
10
+ * Use only run-of-the-mill ActiveRecord models and associations, without introducing any new infrastructural types.
11
+
12
+ HasAttachedTags does not:
13
+ * Parse tags from comma-separate lists
14
+ * Embed tag information directly onto a model
15
+ * Add any specialized methods for interacting with tags
16
+ * Monkey-patch ActiveRecord or any other library
17
+
18
+ In general, HasAttachedTags handles only the storage and association of tags, and leaves other concerns to the application.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'has_attached_tags'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install has_attached_tags
35
+
36
+ To generate configuration and locale files:
37
+
38
+ $ rails g has_attached_tags:install
39
+
40
+ To generate the database migration:
41
+
42
+ $ rails g has_attached_tags:migration
43
+
44
+ ### Namespacing
45
+
46
+ By default, HasAttachedTags with map its `Tag` and `Tagging` models into the local application's top-level scope for easy access.
47
+ This simplifies operations like:
48
+ ```ruby
49
+ # Fully-qualified access:
50
+ tag = HasAttachedTags::Tag.of_type('place').find_or_create_by(name: 'Home')
51
+
52
+ # Simplified access:
53
+ tag = Tag.of_type('place').find_or_create_by(name: 'Home')
54
+ ```
55
+
56
+ If this naming conflicts with existing types in your application, or you simply do not want this behaviour, remove these lines from `config/initializers/has_attached_tags.rb`:
57
+ ```ruby
58
+ Tag = HasAttachedTags::Tag
59
+ Tagging = HasAttachedTags::Tagging
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ In most cases, it's simplest to install HasAttachedTags globally by adding it to the `ApplicationRecord` base class:
65
+
66
+ ```ruby
67
+ class ApplicationRecord < ActiveRecord::Base
68
+ extend HasAttachedTags
69
+
70
+ # ...
71
+ end
72
+ ```
73
+
74
+ Alternatively, it's possible to install the tag DSL only for specific models:
75
+
76
+ ```ruby
77
+ class Image < ActiveRecord::Base
78
+ extend HasAttachedTags
79
+
80
+ # ...
81
+ end
82
+ ```
83
+
84
+ ### Attaching Tags to Models
85
+
86
+ To define a model that accepts tags, the `has_many_tags` macro helper can be used:
87
+
88
+ ```ruby
89
+ class Image < ApplicationRecord
90
+ has_many_tags :characters
91
+ end
92
+ ```
93
+
94
+ We refer to this association between the model and its tags as an "attachment".
95
+ Tags attachments can be used like any other `has_many` association, without any special methods or magic:
96
+
97
+ ```ruby
98
+ image = Image.new
99
+ image.characters # => #<ActiveRecord::Associations::CollectionProxy [...]>
100
+
101
+ image.characters << Tag.find_by(name: 'J', type: 'character')
102
+ image.save
103
+ ```
104
+
105
+ Validations will prevent assigning unsupported tag types to an attachment:
106
+
107
+ ```ruby
108
+ image.characters << Tag.find_by(name: 'J', type: 'character')
109
+ image.valid? # => true
110
+
111
+ image.characters << Tag.find_by(name: 'safe', type: 'rating')
112
+ image.valid? # => false
113
+ ```
114
+
115
+ ### Singular Tag Attachments
116
+
117
+ If only a single tag should be attached to a model, use `has_one_tag` macro helper:
118
+
119
+ ```ruby
120
+ class Image < ApplicationRecord
121
+ has_one_tag :rating
122
+ end
123
+ ```
124
+
125
+ Similarly, the `has_one_tag` attachment uses `has_one` associations under the hood.
126
+
127
+ ```ruby
128
+ image = Image.new
129
+ image.rating # => nil
130
+
131
+ image.rating = Tag.new(name: 'safe', type: 'rating')
132
+ image.rating # => #<Tag id: nil, name: "safe", type: "rating", ...>
133
+ ```
134
+
135
+ ### Marking Tags as Required
136
+
137
+ Sometimes, it might be necessary to validate that as tag is attached to a model. To do this, a `presence: true` validation can be used, however, a `required` option is provided for convenience:
138
+
139
+ ```ruby
140
+ class Image < ApplicationRecord
141
+ has_many_tags :artists, required: true
142
+ end
143
+ ```
144
+
145
+ The `Image` model will now validate that at least 1 artist tag is attached.
146
+
147
+ ```ruby
148
+ image = Image.new
149
+ image.valid? # => false
150
+
151
+ image.artists << Tag.new(name: 'J', type: 'artist')
152
+ image.valid? # => true
153
+ ```
154
+
155
+ This option is available for both `has_one_tag` and `has_many_tags` attachments.
156
+
157
+ ### Specifying a Tag Type
158
+
159
+ By default, the name of the tags attachment will be used to infer the tag type. (For example `has_one_tag :rating` will assume "rating" or `has_many_tags :characters` will assume "character".)
160
+ This value can be customized using the `type` option:
161
+
162
+ ```ruby
163
+ class Image < ApplicationRecord
164
+ has_one_tag :source, type: 'website'
165
+ end
166
+ ```
167
+
168
+ This option is available for both `has_one_tag` and `has_many_tags` attachments.
169
+
170
+ ## Development
171
+
172
+ 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.
173
+
174
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
175
+
176
+ ## Contributing
177
+
178
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Mihail-K/has_attached_tags. 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.
179
+
180
+ ## License
181
+
182
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
183
+
184
+ ## Code of Conduct
185
+
186
+ Everyone interacting in the HasAttachedTags project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Mihail-K/has_attached_tags/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "has_attached_tags"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'has_attached_tags/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'has_attached_tags'
9
+ spec.version = HasAttachedTags::VERSION
10
+ spec.authors = ['Mihail K']
11
+ spec.email = ['mihail@platterz.ca']
12
+
13
+ spec.summary = 'Pluggable Tagging for ActiveRecord'
14
+ spec.description = 'A general purpose, typed tagging system for ActiveRecord'
15
+ spec.homepage = 'https://github.com/Mihail-K/has_attached_tags'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'activerecord', '>= 5.1', '< 7'
32
+
33
+ spec.add_development_dependency 'bundler', '~> 2.0'
34
+ spec.add_development_dependency 'factory_bot', '~> 6.1'
35
+ spec.add_development_dependency 'faker', '~> 2', '>= 2.13'
36
+ spec.add_development_dependency 'rake', '~> 13.0'
37
+ spec.add_development_dependency 'rspec', '~> 3.0'
38
+ spec.add_development_dependency 'rubocop', '~> 0.88'
39
+ spec.add_development_dependency 'rubocop-performance', '~> 1.7'
40
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.42'
41
+ spec.add_development_dependency 'sqlite3', '~> 1.4'
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module HasAttachedTags
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ desc 'Creates the has_attached_tags configuration and locale files.'
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def install
11
+ copy_file('en.yml', 'config/locales/has_attached_tags.en.yml')
12
+ copy_file('has_attached_tags.rb', 'config/initializers/has_attached_tags.rb')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module HasAttachedTags
7
+ class MigrationGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+
10
+ INPUT_FILE = 'migration.rb.erb'
11
+ OUTPUT_FILE = 'db/migrate/create_tags_and_taggings.rb'
12
+
13
+ desc 'Creates the has_attached_tags migration file.'
14
+ source_root File.expand_path('templates', __dir__)
15
+
16
+ def migration
17
+ migration_template(INPUT_FILE, OUTPUT_FILE, migration_version: migration_version)
18
+ end
19
+
20
+ def self.next_migration_number(dirname)
21
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
22
+ end
23
+
24
+ private
25
+
26
+ # @return [String, nil]
27
+ def migration_version
28
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord::VERSION::MAJOR >= 5
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ ---
2
+ en:
3
+ activerecord:
4
+ errors:
5
+ models:
6
+ has_attached_tags/tagging:
7
+ attributes:
8
+ tag:
9
+ unsupported: is unsupported (should be %{type})
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Maps the `Tag` and `Tagging` models into the local application namespace.
4
+ # Remove these lines if you have a conflict, or do not want this behaviour.
5
+ Tag = HasAttachedTags::Tag
6
+ Tagging = HasAttachedTags::Tagging
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateTagsAndTaggings < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :tags do |t|
6
+ t.string :name, null: false
7
+ t.string :type, null: false
8
+ t.index %i[name type], unique: true
9
+ t.integer :taggings_count, null: false
10
+ t.timestamps
11
+ end
12
+
13
+ create_table :taggings do |t|
14
+ t.belongs_to :tag, foreign_key: true, null: false
15
+ t.belongs_to :taggable, polymorphic: true, null: false
16
+ t.string :attachment, null: false
17
+ t.index %i[tag_id attachment taggable_id taggable_type],
18
+ name: 'index_taggings_on_unique_tag_attachment', unique: true
19
+ t.timestamps
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ require 'has_attached_tags/version'
6
+ require 'has_attached_tags/tag'
7
+ require 'has_attached_tags/tagging'
8
+
9
+ module HasAttachedTags
10
+ TAGGINGS_EXISTS = lambda { |tag, attachment, taggable|
11
+ Tagging
12
+ .where(tag: tag, attachment: attachment, taggable_type: taggable.polymorphic_name)
13
+ .where(Tagging.arel_table[:taggable_id].eq(taggable.arel_table[:id]))
14
+ .arel.exists
15
+ }
16
+
17
+ # @param attachment [Symbol]
18
+ # @param type [String]
19
+ # @param required [Boolean]
20
+ # @return [void]
21
+ def has_one_tag(attachment, type: default_type_for_attachment(attachment), required: false) # rubocop:disable Naming/PredicateName
22
+ klass = define_attachment_tagging_class(attachment, type)
23
+
24
+ has_one(:"#{attachment}_tagging", -> { where(attachment: attachment) },
25
+ as: :taggable, autosave: true, class_name: klass.name, dependent: :destroy, inverse_of: :taggable)
26
+ has_one(attachment, -> { of_type(type) }, source: :tag, through: :"#{attachment}_tagging")
27
+
28
+ validates(attachment, presence: true) if required
29
+
30
+ define_attachment_scopes(attachment)
31
+ end
32
+
33
+ # @param attachment [Symbol]
34
+ # @param type [String]
35
+ # @param required [Boolean]
36
+ # @return [void]
37
+ def has_many_tags(attachment, type: default_type_for_attachment(attachment), required: false) # rubocop:disable Naming/PredicateName
38
+ klass = define_attachment_tagging_class(attachment, type)
39
+
40
+ has_many(:"#{attachment}_taggings", -> { where(attachment: attachment) },
41
+ as: :taggable, autosave: true, class_name: klass.name, dependent: :destroy, inverse_of: :taggable)
42
+ has_many(attachment, -> { of_type(type) }, source: :tag, through: :"#{attachment}_taggings")
43
+
44
+ validates(attachment, presence: true) if required
45
+
46
+ define_attachment_scopes(attachment)
47
+ end
48
+
49
+ private
50
+
51
+ # @param attachment [Symbol]
52
+ # @return [String]
53
+ def default_type_for_attachment(attachent)
54
+ attachent.to_s.singularize
55
+ end
56
+
57
+ # @param attachment [Symbol]
58
+ # @param type [String]
59
+ # @return [Class<Tagging>]
60
+ def define_attachment_tagging_class(attachment, type)
61
+ const_set("#{attachment}_tagging".camelize, Class.new(Tagging) do
62
+ validate do
63
+ errors.add(:tag, :unsupported, type: type) if tag && tag.type != type.to_s
64
+ end
65
+ end)
66
+ end
67
+
68
+ # @param attachment [Symbol]
69
+ # @return [void]
70
+ def define_attachment_scopes(attachment)
71
+ # WHERE EXISTS (...) queries are used to prevent duplication of rows.
72
+ scope(:"with_#{attachment}", -> (tag) { where(TAGGINGS_EXISTS.call(tag, attachment, self)) })
73
+ scope(:"without_#{attachment}", -> (tag) { where.not(TAGGINGS_EXISTS.call(tag, attachment, self)) })
74
+ end
75
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasAttachedTags
4
+ class Tag < ActiveRecord::Base
5
+ self.inheritance_column = nil
6
+
7
+ attr_readonly :type
8
+
9
+ has_many :taggings, class_name: 'HasAttachedTags::Tagging', dependent: :destroy, inverse_of: :tag
10
+
11
+ validates :name, :type, presence: true
12
+ validates :name, uniqueness: { scope: :type, if: :name_changed? }
13
+
14
+ scope :of_type, -> (type) { where(type: type) }
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasAttachedTags
4
+ class Tagging < ActiveRecord::Base
5
+ attr_readonly :tag_id, :attachment, :taggable_id, :taggable_type
6
+
7
+ belongs_to :tag, class_name: 'HasAttachedTags::Tag', counter_cache: true, inverse_of: :taggings, optional: false
8
+ belongs_to :taggable, inverse_of: false, optional: false, polymorphic: true
9
+
10
+ validates :attachment, presence: true
11
+ validates :tag, uniqueness: { scope: %i[attachment taggable], on: :create }
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasAttachedTags
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,221 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_attached_tags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mihail K
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: factory_bot
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '6.1'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '6.1'
61
+ - !ruby/object:Gem::Dependency
62
+ name: faker
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2'
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '2.13'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '2.13'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rake
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '13.0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '13.0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '3.0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: rubocop
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '0.88'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.88'
123
+ - !ruby/object:Gem::Dependency
124
+ name: rubocop-performance
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '1.7'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '1.7'
137
+ - !ruby/object:Gem::Dependency
138
+ name: rubocop-rspec
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '1.42'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '1.42'
151
+ - !ruby/object:Gem::Dependency
152
+ name: sqlite3
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '1.4'
158
+ type: :development
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '1.4'
165
+ description: A general purpose, typed tagging system for ActiveRecord
166
+ email:
167
+ - mihail@platterz.ca
168
+ executables: []
169
+ extensions: []
170
+ extra_rdoc_files: []
171
+ files:
172
+ - ".gitignore"
173
+ - ".rspec"
174
+ - ".rubocop.yml"
175
+ - ".travis.yml"
176
+ - CODE_OF_CONDUCT.md
177
+ - Gemfile
178
+ - Gemfile.lock
179
+ - LICENSE.txt
180
+ - README.md
181
+ - Rakefile
182
+ - bin/console
183
+ - bin/rspec
184
+ - bin/rubocop
185
+ - bin/setup
186
+ - has_attached_tags.gemspec
187
+ - lib/generators/has_attached_tags/install_generator.rb
188
+ - lib/generators/has_attached_tags/migration_generator.rb
189
+ - lib/generators/has_attached_tags/templates/en.yml
190
+ - lib/generators/has_attached_tags/templates/has_attached_tags.rb
191
+ - lib/generators/has_attached_tags/templates/migration.rb.erb
192
+ - lib/has_attached_tags.rb
193
+ - lib/has_attached_tags/tag.rb
194
+ - lib/has_attached_tags/tagging.rb
195
+ - lib/has_attached_tags/version.rb
196
+ homepage: https://github.com/Mihail-K/has_attached_tags
197
+ licenses:
198
+ - MIT
199
+ metadata:
200
+ homepage_uri: https://github.com/Mihail-K/has_attached_tags
201
+ source_code_uri: https://github.com/Mihail-K/has_attached_tags
202
+ post_install_message:
203
+ rdoc_options: []
204
+ require_paths:
205
+ - lib
206
+ required_ruby_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ required_rubygems_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ requirements: []
217
+ rubygems_version: 3.0.3
218
+ signing_key:
219
+ specification_version: 4
220
+ summary: Pluggable Tagging for ActiveRecord
221
+ test_files: []