bitwise_attributes 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +33 -1
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +6 -2
- data/Gemfile.lock +1 -1
- data/README.md +20 -3
- data/bitwise_attributes.gemspec +3 -1
- data/lib/bitwise_attributes/active_record_extension.rb +98 -60
- data/lib/bitwise_attributes/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 838a31f15821813cb22ce30c7830c6b0d7d77f106346a0902fe7e2dc97d40ed0
|
|
4
|
+
data.tar.gz: 1e2c6222ab5c571dbccdde150eba4587b60c70da76ebecc4dd111e0e2457224d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d360c4318ebca0adc53dd75a9081670f0362171a98385c79e5eaf221056d7074d75009a11022aaea1e5a1bf0e6ed7f4cfc962ef4db0bdeabcce672c4b5cf01a9
|
|
7
|
+
data.tar.gz: d3410f2652d74877c51b97c12cffeaff314de1abcba061f0d0c9644f88953bc6328e045b84a271057d8c5c37dfc527c4fd0e7b2871819b9e2dd7d5f57d5e3f0b
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: disable
|
|
4
|
+
SuggestExtensions: false
|
|
3
5
|
|
|
4
6
|
Style/StringLiterals:
|
|
5
7
|
Enabled: true
|
|
@@ -11,3 +13,33 @@ Style/StringLiteralsInInterpolation:
|
|
|
11
13
|
|
|
12
14
|
Layout/LineLength:
|
|
13
15
|
Max: 120
|
|
16
|
+
|
|
17
|
+
Style/Documentation:
|
|
18
|
+
Exclude:
|
|
19
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
20
|
+
- "spec/**/*"
|
|
21
|
+
|
|
22
|
+
Metrics/ModuleLength:
|
|
23
|
+
Exclude:
|
|
24
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
25
|
+
|
|
26
|
+
Metrics/BlockLength:
|
|
27
|
+
Exclude:
|
|
28
|
+
- "spec/**/*"
|
|
29
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
30
|
+
|
|
31
|
+
Metrics/MethodLength:
|
|
32
|
+
Exclude:
|
|
33
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
34
|
+
|
|
35
|
+
Metrics/AbcSize:
|
|
36
|
+
Exclude:
|
|
37
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
38
|
+
|
|
39
|
+
Metrics/CyclomaticComplexity:
|
|
40
|
+
Exclude:
|
|
41
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
|
42
|
+
|
|
43
|
+
Metrics/PerceivedComplexity:
|
|
44
|
+
Exclude:
|
|
45
|
+
- "lib/bitwise_attributes/active_record_extension.rb"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-07-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `toggle_key_bit` instance method (XOR) for each defined key
|
|
7
|
+
- `with_all_attr(keys)` scope — "all of these bits are set" (any additional bits are also OK)
|
|
8
|
+
- `with_exactly_attr(keys)` scope — true exact equality (`WHERE column = bitmask`)
|
|
9
|
+
- Array-based setter: `record.attr = [:key_a, :key_b]` computes and assigns the bitmask; `nil` maps to `0`; raw integers pass through unchanged
|
|
10
|
+
- `validates_bitwise_attribute(attr, **options)` class macro — adds a numericality validation ensuring the stored integer is between `0` and the maximum possible bitmask for that attribute
|
|
11
|
+
- Overflow guard at definition time: raises `ArgumentError` when key count exceeds 62; warns to stderr when it exceeds 30 (recommend BIGINT column)
|
|
12
|
+
- Validates that at least one key is provided and that no duplicate keys are present when calling `bitwise_attribute`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `with_exact_attr` renamed to `with_all_attr` — the old name implied exact column equality but the SQL performs a "has all" check; `with_exactly_attr` is the new true-exact scope
|
|
16
|
+
- Column names are now quoted via `connection.quote_column_name` in all three SQL scopes for cross-adapter correctness
|
|
17
|
+
- Scope lambdas capture the model class at definition time instead of using `ActiveRecord::Relation#model` at query time
|
|
18
|
+
- Error messages from invalid key lookups now report only the unrecognised keys, not the full input array
|
|
19
|
+
- `normalize_and_fetch_values` is now a public class method (no longer bypassed with `send` from instance context)
|
|
20
|
+
- `update_bitwise_attribute` is now a private instance method
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Nil safety: all bitwise operations now call `.to_i` on the raw attribute value, preventing `NoMethodError` when the column is `NULL` or the record is unsaved
|
|
24
|
+
|
|
3
25
|
## [0.1.0] - 2026-07-05
|
|
4
26
|
|
|
5
27
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -52,12 +52,16 @@ A single integer column stores multiple boolean flags packed as bits. Each flag
|
|
|
52
52
|
|---|---|
|
|
53
53
|
| `key_bit?` | Check if a specific bit is set |
|
|
54
54
|
| `set_key_bit` / `unset_key_bit` | Set or clear a single bit |
|
|
55
|
+
| `toggle_key_bit` | XOR-flip a single bit |
|
|
55
56
|
| `attr_name_key` / `attr_name_key=` | Boolean getter/setter (accepts truthy values) |
|
|
56
57
|
| `was_previously_key_bit?` | Dirty-tracking: was bit set before last save |
|
|
58
|
+
| `attr_name=` | Assign full flag set from an `Array`, `Integer`, or `nil` |
|
|
57
59
|
| `associated_attr_name` | Returns array of all currently-set key names |
|
|
58
60
|
| `set_attr_name(*keys)` / `unset_attr_name(*keys)` | Bulk OR-in / AND-NOT-out |
|
|
61
|
+
| `validates_bitwise_attribute(attr, **opts)` | Class macro: numericality validation (0..max_bitmask) |
|
|
59
62
|
| `with_attr_name(keys)` | Scope: any of the given bits set |
|
|
60
|
-
| `
|
|
63
|
+
| `with_all_attr_name(keys)` | Scope: all of the given bits set (other bits may also be set) |
|
|
64
|
+
| `with_exactly_attr_name(keys)` | Scope: column equals the bitmask exactly |
|
|
61
65
|
| `without_attr_name(keys)` | Scope: none of the given bits set |
|
|
62
66
|
|
|
63
67
|
### Aliases
|
|
@@ -76,4 +80,4 @@ Aliases are resolved transparently in scopes and bulk set/unset operations.
|
|
|
76
80
|
|
|
77
81
|
## RuboCop
|
|
78
82
|
|
|
79
|
-
Configured in `.rubocop.yml`: Ruby
|
|
83
|
+
Configured in `.rubocop.yml`: Ruby 3.1 target, double-quoted strings enforced, max line length 120.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -51,6 +51,7 @@ For each key the following methods are generated (shown for `read`):
|
|
|
51
51
|
user.read_bit? # => true / false
|
|
52
52
|
user.set_read_bit # sets the bit in memory
|
|
53
53
|
user.unset_read_bit # clears the bit in memory
|
|
54
|
+
user.toggle_read_bit # XOR — flips the bit
|
|
54
55
|
|
|
55
56
|
user.permissions_read # alias for read_bit?
|
|
56
57
|
user.permissions_read = true # accepts any truthy/falsy value
|
|
@@ -65,19 +66,35 @@ user.was_previously_read_bit? # dirty-tracking: state before last save
|
|
|
65
66
|
user.set_permissions(:read, :admin) # OR-in multiple bits
|
|
66
67
|
user.unset_permissions(:write) # AND-NOT-out multiple bits
|
|
67
68
|
|
|
69
|
+
user.permissions = [:read, :admin] # assign the full set from an array
|
|
70
|
+
user.permissions = 5 # raw integer passthrough also accepted
|
|
71
|
+
|
|
68
72
|
user.associated_permissions # => ["read", "admin"]
|
|
69
73
|
user.permissions_values # => {"read"=>1, "write"=>2, "admin"=>4}
|
|
70
74
|
```
|
|
71
75
|
|
|
76
|
+
### Validation
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
class User < ApplicationRecord
|
|
80
|
+
include BitwiseAttributes
|
|
81
|
+
bitwise_attribute :permissions, :read, :write, :admin
|
|
82
|
+
validates_bitwise_attribute :permissions # 0..7, integer only
|
|
83
|
+
validates_bitwise_attribute :flags, allow_nil: true # passes Rails validator options through
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
72
87
|
### Scopes
|
|
73
88
|
|
|
74
89
|
```ruby
|
|
75
|
-
User.with_permissions(:read)
|
|
90
|
+
User.with_permissions(:read) # any of the given bits set
|
|
76
91
|
User.with_permissions([:read, :write])
|
|
77
92
|
|
|
78
|
-
User.
|
|
93
|
+
User.with_all_permissions([:read, :write]) # ALL given bits set (other bits may also be set)
|
|
94
|
+
|
|
95
|
+
User.with_exactly_permissions([:read, :write]) # column = bitmask exactly (no other bits)
|
|
79
96
|
|
|
80
|
-
User.without_permissions(:admin)
|
|
97
|
+
User.without_permissions(:admin) # none of the given bits set
|
|
81
98
|
```
|
|
82
99
|
|
|
83
100
|
### Aliases
|
data/bitwise_attributes.gemspec
CHANGED
|
@@ -9,7 +9,9 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["mehboob.ali@7vals.com"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "Store multiple boolean flags in a single integer column using bitwise operations."
|
|
12
|
-
spec.description = "BitwiseAttributes extends ActiveRecord models with a bitwise_attribute DSL that packs
|
|
12
|
+
spec.description = "BitwiseAttributes extends ActiveRecord models with a bitwise_attribute DSL that packs " \
|
|
13
|
+
"multiple boolean flags into a single integer column, generating per-flag getter/setter " \
|
|
14
|
+
"methods and query scopes automatically."
|
|
13
15
|
spec.homepage = "https://github.com/mehboobali98/bitwise_attributes"
|
|
14
16
|
spec.license = "MIT"
|
|
15
17
|
spec.required_ruby_version = ">= 3.1.4"
|
|
@@ -7,31 +7,39 @@ module BitwiseAttributes
|
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
9
|
class_methods do
|
|
10
|
-
# Define a bitwise attribute and dynamically generate methods and scopes
|
|
11
10
|
def bitwise_attribute(attribute_name, *keys, aliases: {})
|
|
11
|
+
# C-03: reject empty or duplicate key lists up front
|
|
12
|
+
raise ArgumentError, "#{attribute_name}: at least one key is required" if keys.empty?
|
|
13
|
+
|
|
14
|
+
dupes = keys.tally.select { |_, n| n > 1 }.keys
|
|
15
|
+
raise ArgumentError, "#{attribute_name}: duplicate keys #{dupes.inspect}" if dupes.any?
|
|
16
|
+
|
|
17
|
+
# C-02: overflow guard — 32-bit INT holds 30 usable bits; BIGINT holds 62
|
|
18
|
+
if keys.size > 62
|
|
19
|
+
raise ArgumentError, "#{attribute_name}: #{keys.size} keys exceeds the 62-bit BIGINT limit"
|
|
20
|
+
elsif keys.size > 30
|
|
21
|
+
warn "[BitwiseAttributes] #{attribute_name} has #{keys.size} keys — use a BIGINT column to avoid overflow"
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
invalid_aliases = aliases.values - keys
|
|
13
25
|
raise ArgumentError, "Invalid aliases for #{attribute_name}: #{invalid_aliases}" if invalid_aliases.any?
|
|
14
26
|
|
|
15
|
-
bitwise_aliases[attribute_name]
|
|
16
|
-
bitwise_attributes[attribute_name]
|
|
27
|
+
bitwise_aliases[attribute_name] = aliases.with_indifferent_access.freeze
|
|
28
|
+
bitwise_attributes[attribute_name] = keys.map.with_index { |key, index| [key, 1 << index] }
|
|
29
|
+
.to_h.with_indifferent_access.freeze
|
|
17
30
|
define_bitwise_methods(attribute_name, keys)
|
|
18
31
|
end
|
|
19
32
|
|
|
20
|
-
# Retrieve all defined bitwise aliases
|
|
21
33
|
def bitwise_aliases
|
|
22
34
|
@bitwise_aliases ||= Hash.new { |h, k| h[k] = {} }
|
|
23
35
|
end
|
|
24
36
|
|
|
25
|
-
# Retrieve all defined bitwise attributes
|
|
26
37
|
def bitwise_attributes
|
|
27
38
|
@bitwise_attributes ||= {}
|
|
28
39
|
end
|
|
29
40
|
|
|
30
|
-
# Ensure inheritance propagates bitwise attributes to subclasses
|
|
31
41
|
def inherited(subclass)
|
|
32
42
|
super
|
|
33
|
-
|
|
34
|
-
# Copy bitwise attributes to the subclass
|
|
35
43
|
subclass.instance_variable_set(:@bitwise_aliases, bitwise_aliases.dup)
|
|
36
44
|
subclass.instance_variable_set(:@bitwise_attributes, bitwise_attributes.dup)
|
|
37
45
|
end
|
|
@@ -48,108 +56,138 @@ module BitwiseAttributes
|
|
|
48
56
|
end
|
|
49
57
|
end
|
|
50
58
|
|
|
59
|
+
# G-01: validation macro — ensures the stored integer cannot exceed the declared key space
|
|
60
|
+
def validates_bitwise_attribute(attribute_name, **options)
|
|
61
|
+
max_value = bitwise_attributes.fetch(attribute_name).values.sum
|
|
62
|
+
validates attribute_name,
|
|
63
|
+
numericality: {
|
|
64
|
+
only_integer: true,
|
|
65
|
+
greater_than_or_equal_to: 0,
|
|
66
|
+
less_than_or_equal_to: max_value
|
|
67
|
+
},
|
|
68
|
+
**options
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# G-03: public so instance methods can call it without send
|
|
72
|
+
def normalize_and_fetch_values(attribute_name, keys)
|
|
73
|
+
alias_mappings = bitwise_aliases[attribute_name]
|
|
74
|
+
attribute_values = bitwise_attributes[attribute_name]
|
|
75
|
+
Array(keys).uniq.map { |key| attribute_values[alias_mappings.fetch(key, key)] }
|
|
76
|
+
end
|
|
77
|
+
|
|
51
78
|
private
|
|
52
79
|
|
|
53
|
-
# Dynamically define methods and scopes for a bitwise attribute
|
|
54
80
|
def define_bitwise_methods(attribute_name, keys)
|
|
55
|
-
#
|
|
56
|
-
define_method(:"#{attribute_name}_values") do
|
|
57
|
-
self.class.bitwise_attributes[attribute_name]
|
|
58
|
-
end
|
|
81
|
+
model_class = self # Q-03: capture at definition time; avoids using AR::Relation#model inside lambdas
|
|
59
82
|
|
|
60
|
-
define_method(:"#{attribute_name}
|
|
61
|
-
|
|
62
|
-
end
|
|
83
|
+
define_method(:"#{attribute_name}_values") { self.class.bitwise_attributes[attribute_name] }
|
|
84
|
+
define_method(:"#{attribute_name}_aliases") { self.class.bitwise_aliases[attribute_name] }
|
|
63
85
|
|
|
64
|
-
# Define methods for individual keys
|
|
65
86
|
keys.each do |key|
|
|
66
87
|
define_method(:"#{key}_bit?") do
|
|
67
88
|
bit_value = send(:"#{attribute_name}_values")[key]
|
|
68
|
-
(self[attribute_name] & bit_value) != 0
|
|
89
|
+
(self[attribute_name].to_i & bit_value) != 0 # C-01: .to_i guards against NULL
|
|
69
90
|
end
|
|
70
91
|
|
|
71
92
|
define_method(:"set_#{key}_bit") do
|
|
72
93
|
bit_value = send(:"#{attribute_name}_values")[key]
|
|
73
|
-
self[attribute_name]
|
|
94
|
+
self[attribute_name] = self[attribute_name].to_i | bit_value # C-01
|
|
74
95
|
end
|
|
75
96
|
|
|
76
97
|
define_method(:"unset_#{key}_bit") do
|
|
77
98
|
bit_value = send(:"#{attribute_name}_values")[key]
|
|
78
|
-
self[attribute_name]
|
|
99
|
+
self[attribute_name] = self[attribute_name].to_i & ~bit_value # C-01
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A-02: XOR toggle — the most natural bitwise operation
|
|
103
|
+
define_method(:"toggle_#{key}_bit") do
|
|
104
|
+
bit_value = send(:"#{attribute_name}_values")[key]
|
|
105
|
+
self[attribute_name] = self[attribute_name].to_i ^ bit_value
|
|
79
106
|
end
|
|
80
107
|
|
|
81
108
|
define_method(:"#{attribute_name}_#{key}=") do |val|
|
|
82
109
|
send(ActiveModel::Type::Boolean.new.cast(val) ? :"set_#{key}_bit" : :"unset_#{key}_bit")
|
|
83
110
|
end
|
|
84
111
|
|
|
85
|
-
define_method(:"#{attribute_name}_#{key}")
|
|
86
|
-
send(:"#{key}_bit?")
|
|
87
|
-
end
|
|
112
|
+
define_method(:"#{attribute_name}_#{key}") { send(:"#{key}_bit?") }
|
|
88
113
|
|
|
89
114
|
define_method(:"was_previously_#{key}_bit?") do
|
|
90
115
|
bit_value = send(:"#{attribute_name}_values")[key]
|
|
91
|
-
(attribute_previously_was(attribute_name) & bit_value) != 0
|
|
116
|
+
(attribute_previously_was(attribute_name).to_i & bit_value) != 0 # C-01
|
|
92
117
|
end
|
|
93
118
|
end
|
|
94
119
|
|
|
95
|
-
#
|
|
96
|
-
define_method(:"
|
|
97
|
-
update_bitwise_attribute(attribute_name, keys_to_add, :add)
|
|
98
|
-
end
|
|
120
|
+
define_method(:"set_#{attribute_name}") { |*ks| update_bitwise_attribute(attribute_name, ks, :add) }
|
|
121
|
+
define_method(:"unset_#{attribute_name}") { |*ks| update_bitwise_attribute(attribute_name, ks, :remove) }
|
|
99
122
|
|
|
100
|
-
define_method(:"
|
|
101
|
-
|
|
123
|
+
define_method(:"associated_#{attribute_name}") do
|
|
124
|
+
send(:"#{attribute_name}_values").reject { |_key, bit| (self[attribute_name].to_i & bit).zero? }.keys # C-01
|
|
102
125
|
end
|
|
103
126
|
|
|
104
|
-
#
|
|
105
|
-
define_method(:"
|
|
106
|
-
|
|
127
|
+
# A-04: assign the full flag set from an array or pass an integer through directly
|
|
128
|
+
define_method(:"#{attribute_name}=") do |value|
|
|
129
|
+
self[attribute_name] =
|
|
130
|
+
case value
|
|
131
|
+
when Integer then value
|
|
132
|
+
when Array then self.class.send(:calculate_bitmask, attribute_name, value.map(&:to_s))
|
|
133
|
+
when NilClass then 0
|
|
134
|
+
else raise ArgumentError, "Expected Integer or Array for #{attribute_name}, got #{value.class}"
|
|
135
|
+
end
|
|
107
136
|
end
|
|
108
137
|
|
|
109
|
-
#
|
|
138
|
+
# Q-01: quote column name via the adapter; Q-03: use captured model_class, not AR::Relation#model
|
|
110
139
|
scope :"with_#{attribute_name}", lambda { |bit_keys|
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
col = model_class.connection.quote_column_name(attribute_name)
|
|
141
|
+
bitmask = model_class.send(:calculate_bitmask, attribute_name, bit_keys)
|
|
142
|
+
where("#{col} & ? != 0", bitmask)
|
|
113
143
|
}
|
|
114
144
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
# A-01: renamed from with_exact_ — semantics are "has ALL of these bits" (other bits may also be set)
|
|
146
|
+
scope :"with_all_#{attribute_name}", lambda { |bit_keys|
|
|
147
|
+
col = model_class.connection.quote_column_name(attribute_name)
|
|
148
|
+
bitmask = model_class.send(:calculate_bitmask, attribute_name, bit_keys)
|
|
149
|
+
where("#{col} & :bitmask = :bitmask", { bitmask: bitmask })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# A-01: true exact equality — column value must equal the bitmask precisely
|
|
153
|
+
scope :"with_exactly_#{attribute_name}", lambda { |bit_keys|
|
|
154
|
+
bitmask = model_class.send(:calculate_bitmask, attribute_name, bit_keys)
|
|
155
|
+
where(attribute_name => bitmask)
|
|
118
156
|
}
|
|
119
157
|
|
|
120
158
|
scope :"without_#{attribute_name}", lambda { |bit_keys|
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
col = model_class.connection.quote_column_name(attribute_name)
|
|
160
|
+
bitmask = model_class.send(:calculate_bitmask, attribute_name, bit_keys)
|
|
161
|
+
where("#{col} & ? = 0", bitmask)
|
|
123
162
|
}
|
|
124
163
|
end
|
|
125
164
|
|
|
126
|
-
# Calculate the bitmask for given keys (now supports aliases)
|
|
127
165
|
def calculate_bitmask(attribute_name, keys)
|
|
128
|
-
|
|
129
|
-
|
|
166
|
+
# Q-02: zip so we can report only the unrecognised keys, not the whole input
|
|
167
|
+
unique_keys = Array(keys).uniq
|
|
168
|
+
values = normalize_and_fetch_values(attribute_name, unique_keys)
|
|
169
|
+
invalid = unique_keys.zip(values).filter_map { |k, v| k if v.nil? }
|
|
170
|
+
raise ArgumentError, "Unknown #{attribute_name} keys: #{invalid.inspect}" if invalid.any?
|
|
130
171
|
|
|
131
|
-
|
|
172
|
+
values.sum
|
|
132
173
|
end
|
|
174
|
+
end
|
|
133
175
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Array(keys).uniq.map { |key| attribute_values[alias_mappings.fetch(key, key)] }
|
|
139
|
-
end
|
|
176
|
+
# G-03: direct call — no send needed now that normalize_and_fetch_values is public on the class
|
|
177
|
+
def normalize_and_fetch_values(attribute_name, keys)
|
|
178
|
+
self.class.normalize_and_fetch_values(attribute_name, keys)
|
|
140
179
|
end
|
|
141
180
|
|
|
142
|
-
# Update bitwise attributes for bulk add/remove operations
|
|
143
181
|
def update_bitwise_attribute(attribute_name, keys, operation)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
bitmask = self.class.send(:calculate_bitmask, attribute_name, keys)
|
|
183
|
+
self[attribute_name] =
|
|
184
|
+
if operation == :add
|
|
185
|
+
self[attribute_name].to_i | bitmask # C-01
|
|
186
|
+
else
|
|
187
|
+
self[attribute_name].to_i & ~bitmask # C-01
|
|
188
|
+
end
|
|
149
189
|
end
|
|
150
190
|
|
|
151
|
-
|
|
152
|
-
self.class.send(:normalize_and_fetch_values, attribute_name, keys)
|
|
153
|
-
end
|
|
191
|
+
private :update_bitwise_attribute # A-03: internal helper — not part of the public instance API
|
|
154
192
|
end
|
|
155
193
|
end
|