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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/Rakefile +8 -0
- data/bin/setup +8 -0
- data/bitmask_enum.gemspec +39 -0
- data/lib/bitmask_enum/attribute.rb +136 -0
- data/lib/bitmask_enum/conflict_checker.rb +43 -0
- data/lib/bitmask_enum/errors.rb +23 -0
- data/lib/bitmask_enum/eval_scripts.rb +111 -0
- data/lib/bitmask_enum/nil_handler.rb +39 -0
- data/lib/bitmask_enum/options.rb +22 -0
- data/lib/bitmask_enum/version.rb +5 -0
- data/lib/bitmask_enum.rb +59 -0
- data/pre-commit +19 -0
- metadata +174 -0
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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
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
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
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
data/bin/setup
ADDED
|
@@ -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
|
data/lib/bitmask_enum.rb
ADDED
|
@@ -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: []
|