bitmask_enum 1.0.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: 8b142602ee1da79ca17074c2fe1edcabeb98d5a053c2486f98ef95410a6f0b15
4
+ data.tar.gz: fa1d5994dd353bd811ed560f1fbbb204d7d8fe2ff824280c8b4dc689592fb72e
5
+ SHA512:
6
+ metadata.gz: 0bf71ad17b882b72b65f90902c54c8ecf2e2c6589acb4fd90b8b4f586d02b5caa20d2500a0a0b880ea10045048dae6062d5346801a6b0d89c6048dca3908f9e4
7
+ data.tar.gz: 4fc29a681c368a0808d4554fd74e448a28cf24486a81d0488d765add4124e38ce1e40ffeddcf38bedad81579813103bf58fc52e3d3feb38dc63c25aacb8c812b
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rspec_status
10
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ SuggestExtensions: false
6
+ NewCops: enable
7
+ TargetRubyVersion: 2.4
8
+
9
+ Metrics/MethodLength:
10
+ Max: 20
11
+ RSpec/ExampleLength:
12
+ Max: 10
13
+ RSpec/NestedGroups:
14
+ Max: 4
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.10
7
+ before_install: gem install bundler -v 1.17.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # BitmaskEnum changelog
2
+
3
+ ## 0.1.0 : 2022-08-28
4
+
5
+ Initial build. Pre-release.
6
+
7
+ Checking and setting of flags on model instance.
8
+ Scopes for querying.
9
+
10
+ ## 0.2.0 : 2022-08-28
11
+
12
+ Pre-release.
13
+
14
+ Refactor of underlying logic.
15
+ Enforce symbols in method output.
16
+
17
+ ## 0.3.0 : 2022-08-28
18
+
19
+ Pre-release.
20
+
21
+ Refactor logic and tests.
22
+ Switch bitmask_enum params to a single hash with multiple keys.
23
+
24
+ ## 0.4.0 : 2022-08-28
25
+
26
+ Pre-release.
27
+
28
+ Add nil handling.
29
+ Refactor options and add testing for them.
30
+
31
+ ## 1.0.0 : 2022-08-29
32
+
33
+ Release.
34
+
35
+ Add setter override to write flags as flag values.
36
+ Add YARD docs.
37
+ Standardize output to symbols.
38
+ Add validation of the attribute - less_than: 1 << flags.size
39
+ Add max ActiveRecord version to protect against future breaking releases
@@ -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 lucygilbert01@gmail.com. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,82 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bitmask_enum (1.0.0)
5
+ activerecord (>= 4.2, <= 7.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.1.6.1)
11
+ activesupport (= 6.1.6.1)
12
+ activerecord (6.1.6.1)
13
+ activemodel (= 6.1.6.1)
14
+ activesupport (= 6.1.6.1)
15
+ activesupport (6.1.6.1)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
20
+ zeitwerk (~> 2.3)
21
+ ast (2.4.2)
22
+ concurrent-ruby (1.1.10)
23
+ diff-lcs (1.5.0)
24
+ i18n (1.12.0)
25
+ concurrent-ruby (~> 1.0)
26
+ json (2.6.2)
27
+ minitest (5.16.3)
28
+ parallel (1.22.1)
29
+ parser (3.1.2.1)
30
+ ast (~> 2.4.1)
31
+ rainbow (3.1.1)
32
+ rake (10.5.0)
33
+ regexp_parser (2.5.0)
34
+ rexml (3.2.5)
35
+ rspec (3.11.0)
36
+ rspec-core (~> 3.11.0)
37
+ rspec-expectations (~> 3.11.0)
38
+ rspec-mocks (~> 3.11.0)
39
+ rspec-core (3.11.0)
40
+ rspec-support (~> 3.11.0)
41
+ rspec-expectations (3.11.0)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.11.0)
44
+ rspec-mocks (3.11.1)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.11.0)
47
+ rspec-support (3.11.0)
48
+ rubocop (1.35.1)
49
+ json (~> 2.3)
50
+ parallel (~> 1.10)
51
+ parser (>= 3.1.2.1)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 1.8, < 3.0)
54
+ rexml (>= 3.2.5, < 4.0)
55
+ rubocop-ast (>= 1.20.1, < 2.0)
56
+ ruby-progressbar (~> 1.7)
57
+ unicode-display_width (>= 1.4.0, < 3.0)
58
+ rubocop-ast (1.21.0)
59
+ parser (>= 3.1.1.0)
60
+ rubocop-rspec (2.12.1)
61
+ rubocop (~> 1.31)
62
+ ruby-progressbar (1.11.0)
63
+ sqlite3 (1.4.4)
64
+ tzinfo (2.0.5)
65
+ concurrent-ruby (~> 1.0)
66
+ unicode-display_width (2.2.0)
67
+ zeitwerk (2.6.0)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ bitmask_enum!
74
+ bundler (~> 1.17)
75
+ rake (~> 10.0)
76
+ rspec (~> 3.0)
77
+ rubocop (~> 1.35)
78
+ rubocop-rspec (~> 2.12)
79
+ sqlite3 (~> 1.4)
80
+
81
+ BUNDLED WITH
82
+ 1.17.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Lucy Gilbert
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.
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # BitmaskEnum
2
+
3
+ A bitmask enum attribute for ActiveRecord.
4
+
5
+ Aiming to be lightweight and performant, providing the core functionality for the use of a bitmask enum model attribute.
6
+
7
+ It adds checking, getting, setting and toggling of the flags to the instance; scopes for flags to the class; and allows creating/updating the attribute with a flag/an array of flags rather than an integer.
8
+
9
+ Supporting Ruby 2.4+ and Rails 4.2+.
10
+
11
+ Credit is due to Joel Moss' gem [bitmask_attributes](https://github.com/joelmoss/bitmask_attributes). I came across it while considering if I should write a gem for this. It's great work and some elements of it inspired this gem, I just had my own thoughts about how I'd like the gem to operate, and wanted some more end-to-end experience on gem production so I decided to create this rather than pick up the torch on that repo.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'bitmask_enum'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install bitmask_enum
28
+
29
+ ## Usage
30
+
31
+ In the model, the bitmask enum is added in a similar way to enums. Given an integer attribute called `attribs`, flags of `flag` and `flag2`, adding the `flag_prefix` option with the value `type`, the following line would be used:
32
+
33
+ ```ruby
34
+ bitmask_enum attribs: [:flag, :flag2], flag_prefix: 'type'
35
+ ```
36
+
37
+ The `bitmask_enum` class method is used to add the bitmask enum, which then goes on to add numerous helper methods to the model.
38
+
39
+ It also enables setting the attribute with a flag or array of flags when creating or updating. More info is in the `{attribute}=` method section.
40
+ ```ruby
41
+ Model.create!(attribs: :flag)
42
+ Model.update!(attribs: [:flag, :flag2])
43
+ ```
44
+
45
+ ### `bitmask_enum`
46
+
47
+ `bitmask_enum params`
48
+
49
+ #### params
50
+
51
+ **Type:** Hash
52
+
53
+ The first key of this hash should be the name of the integer attribute to be modeled as a bitmask enum. The value of that key should be an array of symbols or strings representing the flags that will be part of the bitmask.
54
+
55
+ Any following keys are optional and should define options for the enum. The current accepted keys are:
56
+ - `flag_prefix` - A symbol or string that will prefix all the created method names for individual flags
57
+ - The gem will prepend the provided value to the flag with an underscore, e.g. `pre` would become `pre_flag`
58
+ - `flag_suffix` - A symbol or string that will suffix all the created method names for individual flags
59
+ - The gem will append the provided value to the flag with an underscore, e.g. `post` would become `flag_post`
60
+ - `nil_handling` - A symbol or string that signifies which behaviour to use when handling nil attribute values
61
+ - The default value, used if the option is not supplied, is `:include`. This includes nil attribute rows as if they were 0.
62
+ - There are currently no other options but more are planned.
63
+ - Providing an unrecognized option will raise an error.
64
+ - `validate` - A boolean signaling whether you want to apply attribute validation. Attributes will validate that they are less than the number of flags squared (number of flags squared - 1 is the highest valid bitmask value). Defaults to `true`.
65
+
66
+ ---
67
+
68
+ ### The following methods will be created on the model instance:
69
+
70
+ ### `{flag}?`
71
+
72
+ **No params**
73
+
74
+ For each flag, this method will be created.
75
+
76
+ The method checks whether a flag is enabled or not.
77
+
78
+ **Return value:** `boolean` - reflects whether the flag is enabled for the instance.
79
+
80
+ ### `{flag}!`
81
+
82
+ **No params**
83
+
84
+ For each flag, this method will be created.
85
+
86
+ The method toggles the current setting of the flag.
87
+
88
+ **Return value:** `boolean` - true if the update of the attribute was successful. Raises error if update was unsuccessful.
89
+
90
+ ### `enable_{flag}!`
91
+
92
+ **No params**
93
+
94
+ For each flag, this method will be created.
95
+
96
+ The method enables the the flag is it is disabled, otherwise it takes no action.
97
+
98
+ **Return value:** `boolean` - true if the update of the attribute was successful. Raises error if update was unsuccessful.
99
+
100
+ ### `disable_{flag}!`
101
+
102
+ **No params**
103
+
104
+ For each flag, this method will be created.
105
+
106
+ The method disables the the flag is it is enabled, otherwise it takes no action.
107
+
108
+ **Return value:** `boolean` - true if the update of the attribute was successful. Raises error if update was unsuccessful.
109
+
110
+ ### `{attribute}_settings`
111
+
112
+ **No params**
113
+
114
+ This method will be created once on the instance.
115
+
116
+ The method returns a hash with the flags as keys and their current settings as values. The keys will be symbols.
117
+
118
+ **Return value:** `hash` - hash with flags as keys and their current settings as values. E.g. `{ flag_one: true, flag_two: false }`
119
+
120
+ ### `{attribute}` (_Override_)
121
+
122
+ **No params**
123
+
124
+ This method will be created once on the instance.
125
+
126
+ The method returns an array of all enabled flags on the instance. The items will be symbols. This is the attribute getter.
127
+
128
+ **Return value:** `array` - array of enabled flags. E.g. `[:flag_one, :flag_two]`
129
+
130
+ ### `{attribute}=` (_Override_)
131
+
132
+ **No params**
133
+
134
+ This method will be created once on the instance.
135
+
136
+ The method sets the attribute to the provided value - either an integer, a symbol or string representing a flag or an array of symbols or strings. This is the attribute setter.
137
+
138
+ This method will raise an ArgumentError if one of the flag values passed is not one that was defined.
139
+
140
+ **Return value:** `array` - array of enabled flags. E.g. `[:flag_one, :flag_two]`
141
+
142
+ ---
143
+
144
+ ### The following methods will be created on the model class:
145
+
146
+ ### `{flag}_enabled`
147
+
148
+ **No params**
149
+
150
+ For each flag, this method will be created on the class.
151
+
152
+ The method is a scope of all records for which the flag is enabled.
153
+
154
+ **Return value:** `ActiveRecord::Relation` - a collection of all records for which the flag is enabled.
155
+
156
+ ### `{flag}_disabled`
157
+
158
+ **No params**
159
+
160
+ For each flag, this method will be created on the class.
161
+
162
+ The method is a scope of all records for which the flag is disabled.
163
+
164
+ **Return value:** `ActiveRecord::Relation` - a collection of all records for which the flag is disabled.
165
+
166
+ ### `{attribute}`
167
+
168
+ **No params**
169
+
170
+ This method will be created once on the class.
171
+
172
+ The method returns an array of all the defined flags. The items will be symbols.
173
+
174
+ **Return value:** `array` - array of defined flags. E.g. `[:flag_one, :flag_two]`
175
+
176
+ ## Manual testing
177
+
178
+ This gem has been tested and found to be generally functional with the following combinations: (but it should work with any combination of Ruby 2.4+ and Rails 4.2+, theoretically it could go lower but those are already 8 years old so I felt it was sufficient.)
179
+
180
+ - Ruby 2.4.10 & Rails 4.2.11.3
181
+ - Ruby 2.6.10 & Rails 4.2.11.3
182
+ - Ruby 2.6.10 & Rails 5.2.8.1
183
+ - Ruby 3.1.2 & Rails 7.0.3.1
184
+
185
+ ## Contributing
186
+
187
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lucygilbert/bitmask_enum. 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.
188
+
189
+ 1. Fork the repo.
190
+ 2. Run `bin/setup` to install the bundle and set up the pre-commit hook.
191
+ - If the output ends with `SUCCESS.`, the pre-commit hook has been applied correctly.
192
+ - If the output ends with `ERROR!`, applying the pre-commit hook has failed. Please check the error and install manually.
193
+ 3. Create a branch, prefixed with `feature/` if this addition is a new feature, or `bugfix/` if the addition is a bug fix.
194
+ 4. Add your code with ample testing and ensure that tests and linting pass for your commit.
195
+ 5. Push the branch to your fork and raise a PR against the main repo.
196
+
197
+ ## License
198
+
199
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
200
+
201
+ ## Code of Conduct
202
+
203
+ Everyone interacting in the BitmaskEnum project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/bitmask_enum/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ bundle install
4
+ if cp -f pre-commit .git/hooks/; then
5
+ echo '\033[0;32mPre-commit copied to .git/hooks!\nSUCCESS.\033[0;0m'
6
+ else
7
+ echo '\033[0;31mPre-commit copy failed! Ensure the pre-commit is enabled before continuing.\nERROR!\033[0;0m'
8
+ fi
@@ -0,0 +1,39 @@
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 'bitmask_enum/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'bitmask_enum'
9
+ spec.version = BitmaskEnum::VERSION
10
+ spec.authors = ['Lucy Gilbert']
11
+ spec.email = ['lucygilbert01@gmail.com']
12
+
13
+ spec.summary = 'A bitmask enum attribute for ActiveRecord'
14
+ spec.homepage = 'https://github.com/lucygilbert/bitmask_enum'
15
+ spec.license = 'MIT'
16
+
17
+ raise 'RubyGems 2+ required to guard against public gem pushes' unless spec.respond_to?(:metadata)
18
+
19
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = spec.homepage
22
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
27
+ end
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.required_ruby_version = '>= 2.4'
31
+
32
+ spec.add_dependency 'activerecord', '>= 4.2', '<=7.0'
33
+ spec.add_development_dependency 'bundler', '~> 1.17'
34
+ spec.add_development_dependency 'rake', '~> 10.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
+ spec.add_development_dependency 'rubocop', '~> 1.35'
37
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.12'
38
+ spec.add_development_dependency 'sqlite3', '~> 1.4'
39
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitmask_enum/conflict_checker'
4
+ require 'bitmask_enum/options'
5
+ require 'bitmask_enum/nil_handler'
6
+ require 'bitmask_enum/eval_scripts'
7
+
8
+ module BitmaskEnum
9
+ # Constructs the magic methods and overrides getters and setters for a bitmask enum attribute
10
+ # @api private
11
+ class Attribute
12
+ def initialize(model, attribute, flags, options, defined_enum_methods)
13
+ @attribute = attribute
14
+ @flags = flags
15
+ @options = Options.new(options)
16
+ @nil_handler = NilHandler.new(@options.nil_handling)
17
+ @model = model
18
+ @conflict_checker = ConflictChecker.new(model, attribute, defined_enum_methods)
19
+ end
20
+
21
+ # Defines the methods for the attribute
22
+ def construct!
23
+ attribute_validation if @options.validate
24
+
25
+ @flags.each_with_index do |flag, flag_index|
26
+ per_flag_methods("#{@options.flag_prefix}#{flag}#{@options.flag_suffix}", flag_index)
27
+ end
28
+
29
+ flag_settings_hash_method
30
+ flag_getter_method
31
+ flag_setter_method
32
+
33
+ class_flag_values_method
34
+ end
35
+
36
+ private
37
+
38
+ def attribute_validation
39
+ @model.class_eval EvalScripts.attribute_validation(@attribute, @flags.size), __FILE__, __LINE__
40
+ end
41
+
42
+ def per_flag_methods(flag_label, flag_index)
43
+ flag_check_method(flag_label, flag_index)
44
+ flag_toggle_method(flag_label, flag_index)
45
+ flag_on_method(flag_label, flag_index)
46
+ flag_off_method(flag_label, flag_index)
47
+
48
+ class_flag_enabled_scope(flag_label, flag_index)
49
+ class_flag_disabled_scope(flag_label, flag_index)
50
+ end
51
+
52
+ def flag_check_method(flag_label, flag_index)
53
+ flag_method("#{flag_label}?", "(#{@nil_handler.in_attribute_eval(@attribute)} & #{1 << flag_index}).positive?")
54
+ end
55
+
56
+ def flag_toggle_method(flag_label, flag_index)
57
+ flag_method(
58
+ "#{flag_label}!",
59
+ "update!('#{@attribute}' => #{@nil_handler.in_attribute_eval(@attribute)} ^ #{1 << flag_index})"
60
+ )
61
+ end
62
+
63
+ def flag_on_method(flag_label, flag_index)
64
+ flag_method(
65
+ "enable_#{flag_label}!",
66
+ "update!('#{@attribute}' => #{@nil_handler.in_attribute_eval(@attribute)} | #{1 << flag_index})"
67
+ )
68
+ end
69
+
70
+ def flag_off_method(flag_label, flag_index)
71
+ flag_method(
72
+ "disable_#{flag_label}!",
73
+ "update!('#{@attribute}' => #{@nil_handler.in_attribute_eval(@attribute)} & #{~(1 << flag_index)})"
74
+ )
75
+ end
76
+
77
+ def flag_method(method_name, method_code)
78
+ @conflict_checker.check_instance_method!(method_name)
79
+
80
+ @model.class_eval EvalScripts.flag_method(method_name, method_code), __FILE__, __LINE__
81
+ end
82
+
83
+ def class_flag_enabled_scope(flag_label, flag_index)
84
+ class_flag_scope("#{flag_label}_enabled", :on, flag_index)
85
+ end
86
+
87
+ def class_flag_disabled_scope(flag_label, flag_index)
88
+ class_flag_scope("#{flag_label}_disabled", :off, flag_index)
89
+ end
90
+
91
+ def class_flag_scope(scope_name, setting, flag_index)
92
+ comparator = setting == :on ? :> : :==
93
+ values_for_bitmask = (0...(1 << @flags.size)).select { |x| (x & (1 << flag_index)).send(comparator, 0) }
94
+
95
+ values_for_bitmask = @nil_handler.in_array(values_for_bitmask) if setting == :off
96
+
97
+ @conflict_checker.check_class_method!(scope_name)
98
+
99
+ @model.class_eval EvalScripts.flag_scope(scope_name, @attribute, values_for_bitmask), __FILE__, __LINE__
100
+ end
101
+
102
+ def flag_settings_hash_method
103
+ method_name = "#{@attribute}_settings"
104
+
105
+ @conflict_checker.check_instance_method!(method_name)
106
+
107
+ flag_hash_contents = @flags.each_with_index.map do |flag, flag_index|
108
+ "#{flag}: (#{@nil_handler.in_attribute_eval(@attribute)} & #{1 << flag_index}).positive?"
109
+ end.join(', ')
110
+ @model.class_eval EvalScripts.flag_settings(method_name, flag_hash_contents), __FILE__, __LINE__
111
+ end
112
+
113
+ def flag_getter_method
114
+ @conflict_checker.check_instance_method!(@attribute)
115
+
116
+ flag_array_contents = @flags.each_with_index.map do |flag, flag_index|
117
+ "(#{@nil_handler.in_attribute_eval(@attribute)} & #{1 << flag_index}).positive? ? :#{flag} : nil"
118
+ end.join(', ')
119
+ @model.class_eval EvalScripts.flag_getter(@attribute, flag_array_contents), __FILE__, __LINE__
120
+ end
121
+
122
+ def flag_setter_method
123
+ method_name = "#{@attribute}="
124
+
125
+ @conflict_checker.check_instance_method!(method_name)
126
+
127
+ @model.class_eval EvalScripts.flag_setter(method_name, @attribute), __FILE__, __LINE__
128
+ end
129
+
130
+ def class_flag_values_method
131
+ @conflict_checker.check_class_method!(@attribute)
132
+
133
+ @model.class_eval EvalScripts.class_flag_values(@attribute, @flags), __FILE__, __LINE__
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitmask_enum/errors'
4
+
5
+ module BitmaskEnum
6
+ # Checks for method conflicts on the model
7
+ # @api private
8
+ class ConflictChecker
9
+ def initialize(model, attribute, defined_enum_methods)
10
+ @model = model
11
+ @attribute = attribute
12
+ @defined_enum_methods = defined_enum_methods
13
+ end
14
+
15
+ # Check if the method name is dangerous or already defined on the class
16
+ # @param method_name [String] Name of the method
17
+ def check_class_method!(method_name)
18
+ if @model.dangerous_class_method?(method_name)
19
+ raise_bitmask_conflict_error!(ActiveRecord.name, method_name, klass_method: true)
20
+ elsif @model.method_defined_within?(method_name, ActiveRecord::Relation)
21
+ raise_bitmask_conflict_error!(ActiveRecord::Relation.name, method_name, klass_method: true)
22
+ end
23
+ end
24
+
25
+ # Check if the method name is dangerous or already defined on the instance, or defined by another bitmask enum
26
+ # @param method_name [String] Name of the method
27
+ def check_instance_method!(method_name)
28
+ if @model.dangerous_attribute_method?(method_name)
29
+ raise_bitmask_conflict_error!(ActiveRecord.name, method_name)
30
+ elsif @defined_enum_methods.include?(method_name)
31
+ raise_bitmask_conflict_error!(@defined_enum_methods[method_name], method_name)
32
+ end
33
+
34
+ @defined_enum_methods[method_name] = @attribute
35
+ end
36
+
37
+ private
38
+
39
+ def raise_bitmask_conflict_error!(source, method_name, klass_method: false)
40
+ raise BitmaskEnumMethodConflictError.new(source, @model.name, @attribute, method_name, klass_method)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitmaskEnum
4
+ # Error for an invalid definition of the bitmask enum
5
+ # @api private
6
+ class BitmaskEnumInvalidError < ArgumentError
7
+ def initialize(detail)
8
+ super("BitmaskEnum definition is invalid: #{detail}")
9
+ end
10
+ end
11
+
12
+ # Error for a conflicting method that would be generated by the enum
13
+ # @api private
14
+ class BitmaskEnumMethodConflictError < ArgumentError
15
+ def initialize(source, klass, attribute, method_name, klass_method)
16
+ super(
17
+ 'BitmaskEnum method definition is conflicting: ' \
18
+ "#{klass_method ? 'class ' : ''}method: #{method_name} " \
19
+ "for enum: #{attribute} in class: #{klass} is already defined by: #{source}"
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitmaskEnum
4
+ # Code strings to be templated and evaled to create methods
5
+ # @api private
6
+ module EvalScripts
7
+ class << self
8
+ # Code for validation method checking attribute is within valid values
9
+ # @param attribute [String] Name of the attribute
10
+ # @param flags_size [Integer] Number of defined flags
11
+ # @return [String] Code string to be evaled
12
+ def attribute_validation(attribute, flags_size)
13
+ %(
14
+ validates( # validates(
15
+ :#{attribute}, # :attribs,
16
+ numericality: { less_than: (1 << #{flags_size}) } # numericality: { less_than: (1 << 3) }
17
+ ) # )
18
+ )
19
+ end
20
+
21
+ # Code for methods checking and setting flags: `#flag?`, `#flag!`, `#enable_flag!`, `#disable_flag!`
22
+ # @param method_name [String] Name of the method
23
+ # @param method_code [String] Code contents of the method
24
+ # @return [String] Code string to be evaled
25
+ def flag_method(method_name, method_code)
26
+ %(
27
+ def #{method_name} # def flag!
28
+ #{method_code} # update!('attribs' => self['attribs'] ^ 1)
29
+ end # end
30
+ )
31
+ end
32
+
33
+ # Code for methods scoping by flag: `.flag_enabled`, `.flag_disabled`
34
+ # @param scope_name [String] Name of the scope
35
+ # @param attribute [String] Name of the attribute
36
+ # @param values_for_bitmask [Array] Array of integers for which the flag would be disabled
37
+ # @return [String] Code string to be evaled
38
+ def flag_scope(scope_name, attribute, values_for_bitmask)
39
+ %(
40
+ scope :#{scope_name}, -> do # scope :flag_disabled, -> do
41
+ where('#{attribute}' => #{values_for_bitmask}) # where('attribs' => [0, 2, 4])
42
+ end # end
43
+ )
44
+ end
45
+
46
+ # Code for method returning hash of flags with their boolean setting: `#attribs_settings`
47
+ # @param method_name [String] Name of the method
48
+ # @param flag_hash_contents [String] Contents of the hash which provides the flag settings
49
+ # @return [String] The code string to be evaled
50
+ def flag_settings(method_name, flag_hash_contents)
51
+ %(
52
+ def #{method_name} # def attribs_settings
53
+ { #{flag_hash_contents} } # { flag: (self['attribs'] & 1).positive? }
54
+ end # end
55
+ )
56
+ end
57
+
58
+ # Code for attribute getter method: `#attribs`
59
+ # The return value of the method will be an array of symbols representing the enabled flags
60
+ # @param attribute [String] Name of the attribute
61
+ # @param flag_array_contents [String] Contents of the array which provides the enabled flags
62
+ # @return [String] The code string to be evaled
63
+ def flag_getter(attribute, flag_array_contents)
64
+ %(
65
+ def #{attribute} # def attribs
66
+ [#{flag_array_contents}].compact # [((self['attribs'] || 0) & 1).positive? ? :flag : nil].compact
67
+ end # end
68
+ )
69
+ end
70
+
71
+ # Code for attribute setter method: `#attribs=`
72
+ # The method will take an integer, a symbol representing a flag or an array of symbols representing flags
73
+ # @param method_name [String] Name of the method (the attribute name with an =)
74
+ # @param attribute [String] Name of the attribute
75
+ # @return [String] The code string to be evaled
76
+ def flag_setter(method_name, attribute)
77
+ %(
78
+ def #{method_name}(value) # def attribs=(value)
79
+ if value.is_a?(Integer) # if value.is_a?(Integer)
80
+ super # super
81
+ else # else
82
+ super(Array(value).reduce(0) do |acc, flag| # super(Array(value).reduce(0) do |acc, x|
83
+ flag_index = self.class.#{attribute}.index(flag.to_sym) # flag_index = self.class.attribs.index(flag.to_sym)
84
+ if flag_index.nil? # if flag_index.nil?
85
+ raise( # raise(
86
+ ArgumentError, # ArgumentError,
87
+ "Invalid flag \#{flag} for #{attribute}" # "Invalid flag \#{flag} for attribs"
88
+ ) # )
89
+ end # end
90
+ acc | (1 << flag_index) # acc | (1 << flag_index)
91
+ end) # end)
92
+ end # end
93
+ end # end
94
+ )
95
+ end
96
+
97
+ # Code for class attribute values method: `#attribs=`
98
+ # The return value of the method will be an array of symbols representing all defined flags
99
+ # @param attribute [String] Name of the attribute
100
+ # @param flags [String] Array of symbols representing all defined flags
101
+ # @return [String] The code string to be evaled
102
+ def class_flag_values(attribute, flags)
103
+ %(
104
+ def self.#{attribute} # def self.attribs
105
+ #{flags} # [:flag]
106
+ end # end
107
+ )
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitmaskEnum
4
+ # Handles nil attribute values
5
+ # @api private
6
+ class NilHandler
7
+ def initialize(handling_option)
8
+ @handling_option = handling_option
9
+ end
10
+
11
+ # Handles nil when evaling the attribute
12
+ # @param attribute [String] Name of the attribute
13
+ # @return [String] Code string to handle a nil attribute according to the handling option
14
+ def in_attribute_eval(attribute)
15
+ select_handling(
16
+ attribute,
17
+ include: ->(attrib) { "(self['#{attrib}'] || 0)" }
18
+ )
19
+ end
20
+
21
+ # Handles nil for an array of values for the attribute
22
+ # @param array [Array] Array of integers representing values of the attribute
23
+ # @return [Array] Array of integers representing values of the attribute, now corrected for nil values
24
+ def in_array(array)
25
+ select_handling(
26
+ array,
27
+ include: ->(arr) { arr << nil }
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def select_handling(value, handling_actions)
34
+ action = handling_actions[@handling_option] || handling_actions[:include]
35
+
36
+ action.call(value)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitmaskEnum
4
+ NIL_HANDLING_OPTIONS = [:include].freeze
5
+
6
+ # Handles the bitmask enum's user-provided options
7
+ # @api private
8
+ class Options
9
+ attr_reader :flag_prefix, :flag_suffix, :nil_handling, :validate
10
+
11
+ def initialize(options)
12
+ @nil_handling = options[:nil_handling].to_sym
13
+ unless NIL_HANDLING_OPTIONS.include?(@nil_handling)
14
+ raise BitmaskEnumInvalidError, "#{@nil_handling} is not a valid nil handling option"
15
+ end
16
+
17
+ @flag_prefix = options[:flag_prefix].nil? ? '' : "#{options[:flag_prefix]}_"
18
+ @flag_suffix = options[:flag_suffix].nil? ? '' : "_#{options[:flag_suffix]}"
19
+ @validate = options[:validate]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitmaskEnum
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'bitmask_enum/attribute'
5
+ require 'bitmask_enum/errors'
6
+
7
+ # Adds support for bitmask enum attributes to ActiveRecord models.
8
+ module BitmaskEnum
9
+ DEFAULT_BITMASK_ENUM_OPTIONS = {
10
+ flag_prefix: nil,
11
+ flag_suffix: nil,
12
+ nil_handling: :include,
13
+ validate: true
14
+ }.freeze
15
+
16
+ # Defines a bitmask enum and constructs the magic methods and method overrides for handling it.
17
+ # @param params [Hash] Hash with first key/value being the attribute name and an array of flags,
18
+ # the remaining keys being options.
19
+ # - `flag_prefix`: Symbol or string that prefixes all the created method names for flags joined with an underscore
20
+ # - `flag_suffix`: Symbol or string that suffixes all the created method names for flags joined with an underscore
21
+ # - `nil_handling`: Symbol or string signaling behaviour when handling nil attribute values. Options are:
22
+ # - `include`: Treat nil as 0 and include in queries, this is the default.
23
+ # - `validate`: Boolean to apply attribute validation. Attributes will validate that they are
24
+ # less than the number of flags squared (number of flags squared - 1 is the highest valid bitmask value).
25
+ # Defaults to `true`.
26
+ def bitmask_enum(params)
27
+ validation_error = validate_params(params)
28
+ raise BitmaskEnumInvalidError, validation_error if validation_error.present?
29
+
30
+ attribute, flags = params.shift
31
+ flags = flags.map(&:to_sym)
32
+ options = params
33
+ merged_options = DEFAULT_BITMASK_ENUM_OPTIONS.merge(options.symbolize_keys)
34
+
35
+ Attribute.new(self, attribute, flags, merged_options, defined_bitmask_enum_methods).construct!
36
+ end
37
+
38
+ private
39
+
40
+ def validate_params(params)
41
+ return 'must be a hash' unless params.is_a?(Hash)
42
+ return 'attribute must be a symbol or string and cannot be empty' unless text?(params.first.first)
43
+
44
+ flags = params.first[1]
45
+ return if flags.is_a?(Array) && flags.all? { |f| text?(f) }
46
+
47
+ 'must provide a symbol or string array of flags'
48
+ end
49
+
50
+ def text?(value)
51
+ (value.is_a?(Symbol) || value.is_a?(String)) && value.size.positive?
52
+ end
53
+
54
+ def defined_bitmask_enum_methods
55
+ @defined_bitmask_enum_methods ||= {}
56
+ end
57
+ end
58
+
59
+ ActiveRecord::Base.extend(BitmaskEnum)
data/pre-commit ADDED
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+
3
+
4
+ rake spec
5
+
6
+ valid=$?
7
+
8
+ if [[ $valid -ne 0 ]]; then
9
+ exit $valid
10
+ fi
11
+
12
+ for file in $(git diff-index --name-only --diff-filter AM --cached HEAD); do
13
+ if (echo $file | egrep -q '(\.rb|Gemfile|Rakefile|\.gemspec)$') then
14
+ bundle exec rubocop --force-exclusion $file
15
+ valid=$(( $valid == 0 ? $? : $valid ))
16
+ fi
17
+ done
18
+
19
+ exit $valid
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bitmask_enum
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Lucy Gilbert
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-08-29 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: '4.2'
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.2'
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.17'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.17'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.35'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.35'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rubocop-rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.12'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.12'
103
+ - !ruby/object:Gem::Dependency
104
+ name: sqlite3
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.4'
117
+ description:
118
+ email:
119
+ - lucygilbert01@gmail.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".rubocop.yml"
127
+ - ".travis.yml"
128
+ - CHANGELOG.md
129
+ - CODE_OF_CONDUCT.md
130
+ - Gemfile
131
+ - Gemfile.lock
132
+ - LICENSE.txt
133
+ - README.md
134
+ - Rakefile
135
+ - bin/setup
136
+ - bitmask_enum.gemspec
137
+ - lib/bitmask_enum.rb
138
+ - lib/bitmask_enum/attribute.rb
139
+ - lib/bitmask_enum/conflict_checker.rb
140
+ - lib/bitmask_enum/errors.rb
141
+ - lib/bitmask_enum/eval_scripts.rb
142
+ - lib/bitmask_enum/nil_handler.rb
143
+ - lib/bitmask_enum/options.rb
144
+ - lib/bitmask_enum/version.rb
145
+ - pre-commit
146
+ homepage: https://github.com/lucygilbert/bitmask_enum
147
+ licenses:
148
+ - MIT
149
+ metadata:
150
+ allowed_push_host: https://rubygems.org/
151
+ homepage_uri: https://github.com/lucygilbert/bitmask_enum
152
+ source_code_uri: https://github.com/lucygilbert/bitmask_enum
153
+ changelog_uri: https://github.com/lucygilbert/bitmask_enum/blob/master/CHANGELOG.md
154
+ rubygems_mfa_required: 'true'
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '2.4'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.0.3.1
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: A bitmask enum attribute for ActiveRecord
174
+ test_files: []