├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── attr_bitwise.gemspec ├── circle.yml ├── lib ├── attr_bitwise.rb └── attr_bitwise │ └── version.rb └── spec ├── attr_bitwise_spec.rb └── attr_bitwise_user_defined_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | # Gemfile.lock 32 | # .ruby-version 33 | # .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.1.1' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | attr_bitwise (0.0.4) 5 | activesupport 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activesupport (4.2.5.1) 11 | i18n (~> 0.7) 12 | json (~> 1.7, >= 1.7.7) 13 | minitest (~> 5.1) 14 | thread_safe (~> 0.3, >= 0.3.4) 15 | tzinfo (~> 1.1) 16 | diff-lcs (1.2.5) 17 | i18n (0.7.0) 18 | json (1.8.3) 19 | minitest (5.8.4) 20 | rake (10.5.0) 21 | rspec (3.4.0) 22 | rspec-core (~> 3.4.0) 23 | rspec-expectations (~> 3.4.0) 24 | rspec-mocks (~> 3.4.0) 25 | rspec-core (3.4.3) 26 | rspec-support (~> 3.4.0) 27 | rspec-expectations (3.4.0) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.4.0) 30 | rspec-mocks (3.4.1) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.4.0) 33 | rspec-support (3.4.1) 34 | thread_safe (0.3.5) 35 | tzinfo (1.2.2) 36 | thread_safe (~> 0.1) 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | attr_bitwise! 43 | bundler (~> 1.6) 44 | rake 45 | rspec 46 | 47 | BUNDLED WITH 48 | 1.10.6 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Charly POLY 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: NOT MAINTAINED :warning: 2 | 3 | # `attr_bitwise` ![https://circleci.com/gh/wittydeveloper/attr_bitwise.png?circle-token=7f58370c3b13faaf1954b9e8fe6c7b1fb329daf2](https://circleci.com/gh/wittydeveloper/attr_bitwise.png?circle-token=7f58370c3b13faaf1954b9e8fe6c7b1fb329daf2) [![Gem Version](https://badge.fury.io/rb/attr_bitwise.svg)](https://badge.fury.io/rb/attr_bitwise) 4 | Bitwise attribute for ruby class and Rails model 5 | 6 | ## Features 7 | 8 | - bitwise attribute + helpers, useful for storing multiple states in one place 9 | - ActiveRecord compatible 10 | 11 | [Please read this article for some concrete examples](https://medium.com/jobteaser-dev-team/rails-bitwise-enum-with-super-powers-5030bda5dbab) 12 | 13 | 14 | ## Install 15 | 16 | 17 | ### Inline 18 | 19 | - `gem install attr_bitwise` 20 | 21 | ### Gemfile 22 | 23 | - `gem 'attr_bitwise'` 24 | 25 | 26 | ## Usage 27 | 28 | ```ruby 29 | attr_bitwise :, mapping: [, column_name: ] 30 | ``` 31 | 32 | Alternatively, you can explicitly specify your states by supplying a hash with the values. 33 | 34 | ```ruby 35 | attr_bitwise :, mapping: {} [, column_name: ] 36 | ``` 37 | 38 | 39 | ## Example 40 | 41 | You have a website with many locales (English, French, German...) with specific content in each locale. You want your users to be able to chose which content they want to see and you want to be able to query the users by the locales they have chosen. 42 | 43 | Start with migration 44 | ```ruby 45 | class CreateUsers < ActiveRecord::Migration 46 | def change 47 | create_table :users do |t| 48 | # [...] 49 | t.integer :locales_value 50 | end 51 | end 52 | end 53 | ``` 54 | 55 | Model 56 | ```ruby 57 | 58 | class User < ActiveRecord::Base 59 | include AttrBitwise 60 | 61 | attr_bitwise :locales, mapping: [:en, :fr, :de] 62 | 63 | scope :with_any_locales, lambda { |*locales_sym| 64 | where(locales_value: bitwise_union(*locales_sym, 'locales')) 65 | } 66 | 67 | scope :with_all_locales, lambda { |*locales_sym| 68 | where(locales_value: bitwise_intersection(*locales_sym, 'locales')) 69 | } 70 | 71 | end 72 | 73 | ### 74 | 75 | # return all users who can see at least english or french content 76 | User.with_any_locales(:en, :fr) 77 | 78 | # return all users who can see english and french content 79 | User.with_all_locales(:en, :fr) 80 | 81 | ``` 82 | 83 | 84 | ## API 85 | 86 | **Examples with name = 'locales'** 87 | 88 | ### High level methods 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 102 | 105 | 106 | 107 | 110 | 113 | 116 | 117 | 118 | 121 | 124 | 127 | 128 | 129 | 132 | 135 | 138 | 139 | 140 | 143 | 146 | 149 | 150 | 151 | 154 | 157 | 160 | 161 | 162 | 165 | 168 | 172 | 173 | 174 | 177 | 180 | 184 | 185 | 186 | 189 | 192 | 195 | 196 | 197 | 198 |
MethodReturnDescription
97 | Class#locales 98 | 100 | [, ...] 101 | 103 | Return values as symbols 104 |
108 | Class#locales=([value_or_sym, ..]) 109 | 111 | [, ...] 112 | 114 | Given an array of values (Fixnum or Symbol) or single value (Fixnum or Symbol) add them to value. 115 |
119 | Class#locale == fixnum_or_sym 120 | 122 | Boolean 123 | 125 | Return true if value contains only Fixnum or Symbol 126 |
130 | Class#locale?(fixnum_or_sym) 131 | 133 | Boolean 134 | 136 | Return true if value contains Fixnum or Symbol 137 |
141 | Class#add_locale(value_or_sym) 142 | 144 | Fixnum 145 | 147 | Add Fixnum or Symbol to value 148 |
152 | Class#remove_locale(value_or_sym) 153 | 155 | Fixnum 156 | 158 | Remove Fixnum or Symbol to value 159 |
163 | Class#locales_union([value_or_sym, ..]) 164 | 166 | [Fixnum, ..] 167 | 169 | Given an array of values (Fixnum or Symbol), return bitwise union computation
170 | Return all possible values (mask) for an union of given values 171 |
175 | Class#locales_intersection([value_or_sym, ..]) 176 | 178 | [Fixnum, ..] 179 | 181 | Given an array of values (Fixnum or Symbol), return bitwise intersection computation
182 | Return all possible values (mask) for an intersection of given values 183 |
187 | Class::LOCALES_MAPPING 188 | 190 | Hash 191 | 193 | Return Symbol -> Fixnum mapping 194 |
199 | 200 | 201 | ### Low level methods 202 | 203 | *Theses methods are static, so a name parameters is mandatory in order to fetch mapping* 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 216 | 219 | 222 | 223 | 224 | 227 | 230 | 234 | 235 | 236 | 239 | 242 | 246 | 247 | 248 |
MethodReturnDescription
214 | Class.to_bitwise_values(object, name) 215 | 217 | [Fixnum, ...] 218 | 220 | Given an Object and a attribute name, return Fixnum value depending on mapping 221 |
225 | Class.bitwise_union([Fixnum, ..], name) 226 | 228 | [Fixnum, ..] 229 | 231 | Given an array of values (Fixnum or Symbol) and a attribute name, return bitwise union computation
232 | Return all possible values (mask) for an union of given values 233 |
237 | Class.bitwise_intersection([Fixnum, ..], name) 238 | 240 | [Fixnum, ..] 241 | 243 | Given an array of values (Fixnum or Symbol) and a attribute name, return bitwise intersection computation
244 | Return all possible values (mask) for an intersection of given values 245 |
249 | 250 | 251 | ---------------------------------------- 252 | Maintainers : [@wittydeveloper](https://github.com/wittydeveloper) and [@FSevaistre](https://github.com/FSevaistre) 253 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | -------------------------------------------------------------------------------- /attr_bitwise.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "attr_bitwise/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "attr_bitwise" 8 | spec.version = AttrBitwise::VERSION 9 | spec.authors = ["Charly POLY"] 10 | spec.email = ["cpoly55@gmail.com"] 11 | spec.summary = %q{Bitwise attribute for ruby class and Rails model} 12 | spec.description = %q{Bitwise attribute for ruby class and Rails model} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_runtime_dependency "activesupport" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.6" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | end 27 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | ruby: 3 | version: 2.1.1 -------------------------------------------------------------------------------- /lib/attr_bitwise.rb: -------------------------------------------------------------------------------- 1 | require "active_support" 2 | require "active_support/concern" 3 | require "active_support/core_ext" 4 | require "attr_bitwise/version" 5 | 6 | # Helper to define a bits based value on a Rails model attribute 7 | # this helper expose a set of methods to make bitwise operations 8 | # 9 | # 10 | # Usage : 11 | # attr_bitwise :, [column_name: ,] mapping: 12 | # 13 | # Example 14 | # class MyModel < ActiveRecord::Base 15 | # include AttrBitwise 16 | # 17 | # attr_bitwise :payment_types, mapping: [:slots, :credits] 18 | # end 19 | # 20 | # Will define the following high-level methods : 21 | # - Class#payment_types => [, ...] 22 | # - Class#payment_type?(value_or_sym) => Boolean 23 | # - Class#add_payment_type(value_or_sym) => Fixnum 24 | # - Class#remove_payment_type(value_or_sym) => Fixnum 25 | # 26 | # Will define the following low-level methods : 27 | # - Class.to_bitwise_values(object, name) => [, ...] 28 | # - Class#payment_types_union([Fixnum, ..]) => [Fixnum, ..] 29 | # - Class.bitwise_union([Fixnum, ..], name) => [Fixnum, ..] 30 | # - Class#payment_types_intersection([Fixnum, ..]) => [Fixnum, ..] 31 | # - Class.bitwise_intersection([Fixnum, ..], name) => [Fixnum, ..] 32 | # - Class#payment_types_mapping => Hash 33 | # 34 | # More details in methods definition 35 | module AttrBitwise 36 | extend ActiveSupport::Concern 37 | 38 | 39 | # Custom class that allow to use shortcut : 40 | # my_column == :banana 41 | # instead of 42 | # my_column == [:banana] 43 | class ComparableSymbolsArray < Array 44 | def ==(other_object) 45 | if other_object.is_a?(Symbol) 46 | self.size == 1 && self.first == other_object 47 | else 48 | super(other_object) 49 | end 50 | end 51 | end 52 | 53 | # ClassMethods 54 | module ClassMethods 55 | ###################### 56 | # public class methods 57 | ###################### 58 | 59 | # Usage : 60 | # attr_bitwise :payment_types, mapping: [:slots, :credits], 61 | # column_name: 'payment_types_value' 62 | # 63 | def attr_bitwise(name, column_name: nil, mapping:) 64 | column_name = "#{name}_value" unless column_name.present? 65 | 66 | # build mapping 67 | bitwise_mapping = build_mapping(mapping, name) 68 | 69 | # mask to symbols helper 70 | define_method("#{name}") { send(:value_getter, column_name, bitwise_mapping) } 71 | define_method("#{name}=") do |values_or_symbols_array| 72 | send(:value_setter, column_name, Array(values_or_symbols_array), bitwise_mapping) 73 | end 74 | 75 | # masks symbol presence 76 | define_method("#{name.to_s.singularize}?") do |value_or_symbol| 77 | send(:value?, column_name, force_to_bitwise_value(value_or_symbol, bitwise_mapping)) 78 | end 79 | 80 | # add value to mask 81 | define_method("add_#{name.to_s.singularize}") do |value_or_symbol| 82 | send(:add_value, column_name, force_to_bitwise_value(value_or_symbol, bitwise_mapping)) 83 | end 84 | 85 | # remove value from mask 86 | define_method("remove_#{name.to_s.singularize}") do |value_or_symbol| 87 | send(:remove_value, column_name, force_to_bitwise_value(value_or_symbol, bitwise_mapping)) 88 | end 89 | 90 | # compute values union against mask 91 | define_method("#{name}_union") do |*mixed_array| 92 | self.class.bitwise_union(*mixed_array, name) 93 | end 94 | 95 | # compute values intersection against mask 96 | define_method("#{name}_intersection") do |*mixed_array| 97 | self.class.bitwise_intersection(*mixed_array, name) 98 | end 99 | end 100 | 101 | # given a payment_values array, return a possible matches 102 | # for a union 103 | # 104 | # with PAYMENT_TYPES_MAPPING = { credits: 0b001, slots: 0b010, paypal: 0b100 } 105 | # see http://www.calleerlandsson.com/2015/02/16/flags-bitmasks-and-unix-file-system-permissions-in-ruby/ 106 | # 107 | # bitwise_union(:slots, :credits, 'payment_types') => [0b011, 0b111] 108 | def bitwise_union(*mixed_array, name) 109 | values_array = mixed_array.map { |v| to_bitwise_values(v, name) } 110 | mapping = mapping_from_name(name) 111 | mask = [] 112 | 113 | values_array.each do |pv| 114 | mapping.values.each do |pvv| 115 | mask << (pv | pvv) 116 | end 117 | end 118 | 119 | mask.uniq 120 | end 121 | 122 | # given a values_arr ay array, return a possible matches 123 | # for a intersection 124 | # 125 | # with PAYMENT_TYPES_MAPPING = { credits: 0b001, slots: 0b010, paypal: 0b100 } 126 | # see http://www.calleerlandsson.com/2015/02/16/flags-bitmasks-and-unix-file-system-permissions-in-ruby/ 127 | # 128 | # bitwise_intersection(:slots, :credits, 'payment_types') => [0b101, 0b100, 0b011, 0b111] 129 | def bitwise_intersection(*mixed_array, name) 130 | values_array = mixed_array.map { |v| to_bitwise_values(v, name) } 131 | mapping = mapping_from_name(name) 132 | mask = [] 133 | val = values_array.reduce(&:|) 134 | 135 | mapping.values.each do |pv| 136 | mask << (pv | val) 137 | end 138 | 139 | mask.uniq 140 | end 141 | 142 | # given an Object, return proper Fixnum value, depending of mapping 143 | def to_bitwise_values(object, name) 144 | mapping = mapping_from_name(name) 145 | if object.is_a?(Array) 146 | object.map { |v| force_to_bitwise_value(v, mapping) } 147 | elsif object.is_a?(Hash) 148 | object.values.map { |v| force_to_bitwise_value(v, mapping) } 149 | else 150 | force_to_bitwise_value(object, mapping) 151 | end 152 | end 153 | 154 | # Given a raw value (int) or a symbol, return proper raw value (int) 155 | def force_to_bitwise_value(value_or_symbol, mapping) 156 | if value_or_symbol.is_a?(Symbol) 157 | mapping[value_or_symbol] 158 | else 159 | value_or_symbol.to_i 160 | end 161 | end 162 | 163 | ####################### 164 | # Private class methods 165 | ####################### 166 | 167 | private 168 | 169 | 170 | # return mapping given a bitwise name 171 | def mapping_from_name(name) 172 | const_get("#{name}_mapping".upcase) 173 | end 174 | 175 | # build internal bitwise key-value mapping 176 | # it add a zero value, needed for bits operations 177 | # 178 | # each sym get a power of 2 value 179 | def build_mapping(symbols, name) 180 | mapping = {}.tap do |hash| 181 | if symbols.is_a?(Hash) 182 | validate_user_defined_values!(symbols, name) 183 | hash.merge!(symbols.sort_by{|k,v| v}.to_h) 184 | else 185 | symbols.each_with_index do |key, i| 186 | hash[key] = 2**i 187 | end 188 | end 189 | hash[:empty] = 0 190 | end 191 | # put mapping in unique constant 192 | const_mapping_name = "#{name}_mapping".upcase 193 | const_set(const_mapping_name, mapping) 194 | end 195 | 196 | def validate_user_defined_values!(hash, name) 197 | hash.select{|key,value| (Math.log2(value) % 1.0)!=0}.tap do |invalid_options| 198 | if invalid_options.any? 199 | raise(ArgumentError, "#{name} value should be a power of two number (#{invalid_options.to_s})") 200 | end 201 | end 202 | end 203 | end 204 | 205 | 206 | ########################## 207 | # Private instance methods 208 | ########################## 209 | 210 | private 211 | 212 | 213 | def force_to_bitwise_value(value_or_symbol, mapping) 214 | self.class.force_to_bitwise_value(value_or_symbol, mapping) 215 | end 216 | 217 | # Given a raw value (int) return proper raw value (int) 218 | def value_to_sym(value, mapping) 219 | mapping.invert[value] 220 | end 221 | 222 | # Return current value to symbols array 223 | # Ex : 011 => :slots, :credits 224 | def value_getter(name, mapping) 225 | ComparableSymbolsArray.new( 226 | mapping.values.select { |pv| (send(name) & pv) != 0 }. 227 | map { |v| value_to_sym(v, mapping) } 228 | ) 229 | end 230 | 231 | # Set current values from values array 232 | def value_setter(column_name, values_or_symbols_array, mapping) 233 | send("#{column_name}=", 0) 234 | values_or_symbols_array.each { |val| add_value(column_name, force_to_bitwise_value(val, mapping)) } 235 | end 236 | 237 | # Return if value presents in mask (raw value) and mask set 238 | def value?(column_name, val) 239 | ![0, false].include?(send(column_name) & val) 240 | end 241 | 242 | # add `value_or_symbol` to mask 243 | # Ex, with values = `10` 244 | # add_value(1) => 11 245 | def add_value(column_name, val) 246 | send("#{column_name}=", send(column_name) | val) 247 | end 248 | 249 | # remove `value_or_symbol` to mask 250 | # Ex, with values = `11` 251 | # remove_value(1) => 10 252 | def remove_value(column_name, val) 253 | send("#{column_name}=", send(column_name) & ~val) 254 | end 255 | end 256 | -------------------------------------------------------------------------------- /lib/attr_bitwise/version.rb: -------------------------------------------------------------------------------- 1 | module AttrBitwise 2 | VERSION = "0.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /spec/attr_bitwise_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/attr_bitwise.rb' 2 | 3 | describe AttrBitwise do 4 | 5 | # test class in order to test concern 6 | class TestClass 7 | 8 | include AttrBitwise 9 | 10 | attr_accessor :fruits_value 11 | attr_accessor :vegetables_value 12 | 13 | attr_bitwise :fruits, mapping: [:banana, :apple] 14 | attr_bitwise :vegetables, mapping: [:carrot, :pepper] 15 | 16 | def initialize 17 | @fruits_value = 0 18 | @vegetables_value = 0 19 | end 20 | 21 | end 22 | 23 | subject { TestClass.new } 24 | 25 | context '.to_bitwise_values' do 26 | 27 | context 'with Hash argument' do 28 | it do 29 | expect( 30 | TestClass.to_bitwise_values( 31 | { 32 | a: :apple, 33 | b: :banana 34 | }, 35 | 'fruits' 36 | ) 37 | ).to eq [2, 1] 38 | end 39 | end 40 | 41 | context 'with Array argument' do 42 | it do 43 | expect(TestClass.to_bitwise_values( 44 | [:apple, :banana], 45 | 'fruits' 46 | )).to eq [2, 1] 47 | end 48 | end 49 | 50 | context 'with Fixnum argument' do 51 | it do 52 | expect(TestClass.to_bitwise_values(1, 'fruits')).to eq 1 53 | end 54 | end 55 | 56 | end 57 | 58 | context 'with `fruits` attribute_name' do 59 | 60 | context '#fruits=' do 61 | 62 | before { subject.fruits = [:banana, :apple] } 63 | 64 | it 'should set proper value' do 65 | expect(subject.fruits_value).to eq 3 66 | expect(subject.fruits).to eq [:banana, :apple] 67 | end 68 | 69 | it 'shouldn\'t set values in other field' do 70 | expect(subject.vegetables_value).to eq 0 71 | expect(subject.vegetables).to eq [] 72 | end 73 | 74 | end 75 | 76 | context '#fruits==' do 77 | 78 | context 'when value is incorrect' do 79 | before { subject.fruits = [:banana, :apple] } 80 | 81 | it do 82 | expect(subject.fruits == :banana).to eq false 83 | end 84 | end 85 | 86 | context 'when value is correct' do 87 | before { subject.fruits = [:banana] } 88 | 89 | it do 90 | expect(subject.fruits == :banana).to eq true 91 | end 92 | end 93 | 94 | end 95 | 96 | context 'with `fruits_value` = 0' do 97 | context '#fruits' do 98 | 99 | it do 100 | expect(subject.fruits).to eq [] 101 | end 102 | 103 | end 104 | 105 | context '#add_fruit' do 106 | 107 | it do 108 | subject.add_fruit(:banana) 109 | expect(subject.fruits).to eq [:banana] 110 | end 111 | 112 | context 'when called twice, each type remains unique' do 113 | it do 114 | subject.add_fruit(:banana) 115 | subject.add_fruit(:banana) 116 | expect(subject.fruits).to eq [:banana] 117 | end 118 | end 119 | 120 | end 121 | 122 | context '#fruit?(:banana)' do 123 | 124 | it do 125 | expect(subject.fruit?(:banana)).to eq false 126 | end 127 | 128 | end 129 | end 130 | 131 | context 'with `fruits_value` = 3' do 132 | 133 | before { subject.fruits_value = 3 } 134 | 135 | context '#fruits' do 136 | 137 | it do 138 | expect(subject.fruits).to eq [:banana, :apple] 139 | end 140 | 141 | end 142 | 143 | context '#fruit?(:banana)' do 144 | 145 | it do 146 | expect(subject.fruit?(:banana)).to eq true 147 | end 148 | 149 | end 150 | 151 | context '#remove_fruit' do 152 | 153 | it do 154 | subject.remove_fruit(:banana) 155 | expect(subject.fruits).to eq [:apple] 156 | end 157 | 158 | end 159 | 160 | context '#fruits_union' do 161 | 162 | it do 163 | expect(subject.fruits_union(1, 2)).to eq [1, 3, 2] 164 | end 165 | 166 | end 167 | 168 | context '.bitwise_union' do 169 | 170 | it do 171 | expect( 172 | TestClass.bitwise_union(1, 2, 'fruits') 173 | ).to eq [1, 3, 2] 174 | end 175 | 176 | end 177 | 178 | context '#bitwise_intersection' do 179 | 180 | it do 181 | expect( 182 | TestClass.bitwise_intersection(1, 2, 'fruits') 183 | ).to eq [3] 184 | end 185 | 186 | end 187 | 188 | end 189 | 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /spec/attr_bitwise_user_defined_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/attr_bitwise.rb' 2 | 3 | describe AttrBitwise, 'with user defined mapping values' do 4 | 5 | describe 'invalid options' do 6 | 7 | 8 | #wrap to ensure class is not evaluated until we are ready 9 | let(:test_class) do 10 | 11 | class InvalidUserDefinedTestClass 12 | 13 | include AttrBitwise 14 | 15 | attr_accessor :fruits_value 16 | 17 | attr_bitwise :fruits, mapping: {banana: 2, kiwi: 4, apple: 1, dog: 3, fish: 99} 18 | 19 | def initialize 20 | @fruits_value = 0 21 | end 22 | 23 | end 24 | 25 | end 26 | 27 | context 'raise an exception with invalid options' do 28 | it do 29 | expect{test_class}.to raise_error(ArgumentError) 30 | end 31 | end 32 | end 33 | 34 | describe 'valid options' do 35 | 36 | # test class in order to test concern 37 | class UserDefinedTestClass 38 | 39 | include AttrBitwise 40 | 41 | attr_accessor :fruits_value 42 | 43 | attr_bitwise :fruits, mapping: {banana: 2, kiwi: 4, apple: 1} 44 | 45 | def initialize 46 | @fruits_value = 0 47 | end 48 | 49 | end 50 | 51 | 52 | subject { UserDefinedTestClass.new } 53 | 54 | context '.to_bitwise_values' do 55 | 56 | context 'with Hash argument' do 57 | it do 58 | expect( 59 | UserDefinedTestClass.to_bitwise_values( 60 | { 61 | a: :apple, 62 | b: :banana 63 | }, 64 | 'fruits' 65 | ) 66 | ).to eq [1, 2] 67 | end 68 | end 69 | 70 | context 'with Array argument' do 71 | it do 72 | expect(UserDefinedTestClass.to_bitwise_values( 73 | [:apple, :banana], 74 | 'fruits' 75 | )).to eq [1, 2] 76 | end 77 | end 78 | 79 | context 'with Fixnum argument' do 80 | it do 81 | expect(UserDefinedTestClass.to_bitwise_values(1, 'fruits')).to eq 1 82 | end 83 | end 84 | 85 | end 86 | 87 | context 'with `fruits` attribute_name' do 88 | 89 | context '#fruits=' do 90 | 91 | before { subject.fruits = [:banana, :apple] } 92 | 93 | it 'should set proper value' do 94 | expect(subject.fruits_value).to eq 3 95 | expect(subject.fruits).to eq [:apple, :banana] 96 | end 97 | 98 | end 99 | 100 | context '#fruits==' do 101 | 102 | context 'when value is incorrect' do 103 | before { subject.fruits = [:banana, :apple] } 104 | 105 | it do 106 | expect(subject.fruits == :banana).to eq false 107 | end 108 | end 109 | 110 | context 'when value is correct' do 111 | before { subject.fruits = [:banana] } 112 | 113 | it do 114 | expect(subject.fruits == :banana).to eq true 115 | end 116 | end 117 | 118 | end 119 | 120 | context 'with `fruits_value` = 0' do 121 | context '#fruits' do 122 | 123 | it do 124 | expect(subject.fruits).to eq [] 125 | end 126 | 127 | end 128 | 129 | context '#add_fruit' do 130 | 131 | it do 132 | subject.add_fruit(:banana) 133 | expect(subject.fruits).to eq [:banana] 134 | end 135 | 136 | context 'when called twice, each type remains unique' do 137 | it do 138 | subject.add_fruit(:banana) 139 | subject.add_fruit(:banana) 140 | expect(subject.fruits).to eq [:banana] 141 | end 142 | end 143 | 144 | end 145 | 146 | context '#fruit?(:banana)' do 147 | 148 | it do 149 | expect(subject.fruit?(:banana)).to eq false 150 | end 151 | 152 | end 153 | end 154 | 155 | context 'with `fruits_value` = 3' do 156 | 157 | before { subject.fruits_value = 3 } 158 | 159 | context '#fruits' do 160 | 161 | it do 162 | expect(subject.fruits).to eq [:apple, :banana] 163 | end 164 | 165 | end 166 | 167 | context '#fruit?(:banana)' do 168 | 169 | it do 170 | expect(subject.fruit?(:banana)).to eq true 171 | end 172 | 173 | end 174 | 175 | context '#remove_fruit' do 176 | 177 | it do 178 | subject.remove_fruit(:banana) 179 | expect(subject.fruits).to eq [:apple] 180 | end 181 | 182 | end 183 | 184 | context '#fruits_union' do 185 | 186 | it do 187 | expect(subject.fruits_union(1, 2, 4)).to eq [1, 3, 5, 2, 6, 4] 188 | end 189 | 190 | end 191 | 192 | context '.bitwise_union' do 193 | 194 | it do 195 | expect( 196 | TestClass.bitwise_union(1, 2, 'fruits') 197 | ).to eq [1, 3, 2] 198 | end 199 | 200 | end 201 | 202 | context '#bitwise_intersection' do 203 | 204 | it do 205 | expect( 206 | TestClass.bitwise_intersection(1, 2, 'fruits') 207 | ).to eq [3] 208 | end 209 | 210 | end 211 | 212 | end 213 | 214 | end 215 | 216 | end 217 | end 218 | --------------------------------------------------------------------------------