bit_magic 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ module BitMagic
2
+ module Adapters
3
+ # A container for bit_magic settings and fields
4
+ # Meant to be used internally by the Base adapter (and by extension, all adapters).
5
+ #
6
+ # @attr_reader [Hash] field_options bit_magic options that define fields
7
+ # @attr_reader [Hash] action_options bit_magic options that define settings
8
+ # @attr_reader [Hash] field_list name => bits key pairs based off field_options
9
+ # @attr_reader [Integer] bits_length total number of bits defined in field_options
10
+ # @attr_reader [Integer] max_bit the largest bit defined in field_options
11
+ # @attr_reader [Symbol] bit_magic_name the name given to bit_magic
12
+ class Magician
13
+
14
+ DEFAULT_ACTION_OPTIONS = Bits::DEFAULT_OPTIONS.merge({
15
+ :query_by_value => true, # can be true, false, or a number of bits
16
+ :helpers => true, # TODO: enable deeper access control over injected helper methods
17
+ :bits_wrapper => Bits,
18
+ :bits_generator => BitsGenerator,
19
+ }).freeze
20
+
21
+ # Checks if the key is a a field key definition (Integer or Range, defining
22
+ # bit or bit ranges in question).
23
+ #
24
+ # @param [Integer, Range<Integer>] checkValue an integer bit or bit range
25
+ # to check if it is a valid bit index
26
+ #
27
+ # @return [Integer, Array<Integer>] will return an integer or an array of
28
+ # integers if the checkValue is a valid bit index or bit range
29
+ def self.field_key_value_check(check)
30
+ if check.is_a?(Integer)
31
+ check
32
+ elsif check.is_a?(Range)
33
+ list = check.to_a
34
+ valid = list.reduce(true) {|m, i| m && i.is_a?(Integer) }
35
+ list if valid and list.length > 0
36
+ elsif check.is_a?(Array)
37
+ valid = true
38
+ list = check.collect {|i|
39
+ key = self.field_key_value_check(i)
40
+ key.nil? ? (valid = false; break;) : key
41
+ }.flatten
42
+ list if valid and list.length > 0
43
+ end
44
+ end
45
+
46
+ # Separate field and action options from the options passed to bit_magic
47
+ #
48
+ # Valid keys for field options are:
49
+ # Integer - for a specific bit position
50
+ # Range - for a range of bits
51
+ # Array<Integer> - an array of bit positions
52
+ # The value of the key-pair should be a Symbol.
53
+ #
54
+ # Action options are everything else that's not a field option
55
+ #
56
+ # @param [Hash] options the options passed to bit_magic
57
+ #
58
+ # @return [Hash, Hash] returns an array of two options [field, actions]
59
+ # field options and action options, in that order respectively
60
+ def self.options_splitter(options = {})
61
+ field_opts = {}
62
+ action_opts = DEFAULT_ACTION_OPTIONS.dup
63
+
64
+ options.each_pair do |check_key, value|
65
+ if key = self.field_key_value_check(check_key)
66
+ field_opts[key] = value
67
+ else
68
+ if (!options[:allow_failed_fields]) and (check_key.is_a?(Integer) or check_key.is_a?(Range) or check_key.is_a?(Array))
69
+ raise BitMagic::FieldError.new("key-pair expected to be a valid field option, but it is not: #{check_key.inspect} => #{value.inspect}. If this is an action option, you can disable this error by passing ':allow_failed_fields => true' as an option")
70
+ end
71
+ action_opts[check_key] = value
72
+ end
73
+ end
74
+
75
+ [field_opts.freeze, action_opts.freeze]
76
+ end
77
+
78
+ attr_reader :field_options, :action_options
79
+ attr_reader :field_list
80
+ attr_reader :bits_length, :max_bit
81
+ attr_reader :bit_magic_name
82
+
83
+ # Initialize a new Magician, a container for bit_magic settings and fields
84
+ #
85
+ # @param [Symbol] name the name to be used as a namespace
86
+ # @param [Hash] options the options given to bit_magic. Keys that are
87
+ # Integer, Arrays or Range objects are treated as bit allocations, their
88
+ # value should be the name of the bit field. Keys that are anything else
89
+ # (usually Symbol) are treated as action options or settings.
90
+ #
91
+ # @example Initialize a Magician (usually you would not do this directly)
92
+ # magician = Magician.new(:example, 0 => :is_odd, [1, 2] => :eyes, 3..6 => :fingers, default: 7)
93
+ # # here, field names are :is_odd, :eyes, and :fingers
94
+ # # with bits indices 0, [1, 2], and [3, 4, 5, 6] respectively
95
+ # # default is an action option, set to 7
96
+ #
97
+ # @return a Magician
98
+ def initialize(name, options = {})
99
+ @bit_magic_name = name
100
+ @field_options, @action_options = self.class.options_splitter(options)
101
+ validate_field_options!
102
+ end
103
+
104
+ # Define helper methods on the instance, namespaced to the name given in
105
+ # the bit_magic invocation.
106
+ #
107
+ # This is an internal method, only meant to be used by the Base adapter to
108
+ # during its setup phase. Should not be used directly.
109
+ #
110
+ # Methods that are defined is namespaced to the name during initialization.
111
+ # Referred to here as NAMESPACE. These methods are available on instances.
112
+ #
113
+ # NAMESPACE - defined by Base adapter, returns a Bits wrapper
114
+ # NAMESPACE_enabled?(*field_names) - checks if all the given field names
115
+ # or bit indices are enabled (value > 0)
116
+ # NAMESPACE_disabled?(*field_names) - checks if all the given field names
117
+ # or bit indices are disabled (value == 0)
118
+ #
119
+ # The following are helpers, defined based on field names during initialization
120
+ # Refered to here as 'name'.
121
+ #
122
+ # name - returns the value of the field
123
+ # name=(new_value) - sets the value of the field to the new value
124
+ # name? - available only if the field is a single bit, returns true if
125
+ # the value of the bit is 1, or false if 0
126
+ #
127
+ #
128
+ # @param [Class] klass the class to inject methods into.
129
+ #
130
+ # @return nothing useful.
131
+ def define_bit_magic_methods(klass)
132
+ names = @field_list
133
+ bit_magic_name = @bit_magic_name
134
+
135
+ klass.instance_eval do
136
+
137
+ define_method(:"#{bit_magic_name}_enabled?") do |*fields|
138
+ self.send(bit_magic_name).enabled?(*fields)
139
+ end
140
+
141
+ define_method(:"#{bit_magic_name}_disabled?") do |*fields|
142
+ self.send(bit_magic_name).disabled?(*fields)
143
+ end
144
+ end
145
+
146
+ if @action_options[:helpers]
147
+
148
+ klass.instance_eval do
149
+ names.each_pair do |name, bits|
150
+
151
+ define_method(:"#{name}") do
152
+ self.send(bit_magic_name)[name]
153
+ end
154
+
155
+ define_method(:"#{name}=") do |val|
156
+ self.send(bit_magic_name)[name] = val
157
+ end
158
+
159
+ if bits.is_a?(Integer) or bits.length == 1
160
+ define_method(:"#{name}?") do
161
+ self.send(bit_magic_name)[name] == 1
162
+ end
163
+ end
164
+
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ end
171
+
172
+ # List of bits defined in @field_options
173
+ #
174
+ # @return [Array<Integer>] an array of bits defined in @field_options
175
+ def bits
176
+ @field_list.values.flatten
177
+ end
178
+
179
+ # Used by the Base#bit_magic to create a Bits wrapper around instances
180
+ #
181
+ # @return [Class] a Bits class
182
+ def bits_wrapper
183
+ self.action_options[:bits_wrapper] || Bits
184
+ end
185
+
186
+ # Used by adapters to generate value lists for particular bit operations
187
+ #
188
+ # @return [BitsGenerator] a BitsGenerator object with this magician's field
189
+ # list
190
+ def bits_generator
191
+ @bits_generator ||= (self.action_options[:bits_generator] || BitsGenerator).new self
192
+ end
193
+
194
+ # Inspect this object. This is customized just to shorten the output to
195
+ # actually be readable.
196
+ def inspect
197
+ "#<#{self.class.to_s} name=#{@bit_magic_name} field_list=#{@field_list.inspect}>"
198
+ end
199
+
200
+ protected
201
+ # Internal use only. Sets @field_list @bits_length and @max_bit from @field_options
202
+ def validate_field_options!
203
+ @field_list = {}
204
+
205
+ @field_options.each_pair do |bits, name|
206
+ name = name.to_sym if name.is_a?(String)
207
+
208
+ if name.is_a?(Symbol)
209
+ raise FieldError.new("'#{name}' defined more than once") if @field_list.has_key?(name)
210
+ @field_list[name] = bits
211
+ else
212
+ raise FieldError.new("field name must be a symbol or string, #{name.inspect} is not")
213
+ end
214
+
215
+ end
216
+
217
+ bits = self.bits
218
+ @bits_length = bits.uniq.length
219
+ @max_bit = bits.max
220
+ @field_list.freeze
221
+ end
222
+
223
+ end
224
+
225
+ end
226
+ end
@@ -0,0 +1,233 @@
1
+ require_relative './base'
2
+
3
+ module BitMagic
4
+ module Adapters
5
+ # This is the adapter for Mongoid. It's implemented as a concern to be
6
+ # included inside Mongoid::Document models.
7
+ #
8
+ # It's expected that you have an integer column (default name 'flags',
9
+ # override using the attribute_name option). It's suggested, though not
10
+ # required, that you set the default value same as the bit_magic default.
11
+ #
12
+ # If you have more than one model that you want to use BitMagic in, it's
13
+ # recommended that you just include this adapter globally:
14
+ # require 'bit_magic/adapters/mongoid_adapter'
15
+ # Mongoid::Document.include BitMagic::Adapters::MongoidAdapter
16
+ #
17
+ # Otherwise, you can include it on a per model basis before calling bit_magic
18
+ #
19
+ # class Example
20
+ # include Mongoid::Document
21
+ # # this line below can be excluded if you included the adapter globally
22
+ # include BitMagic::Adapters::MongoidAdapter
23
+ #
24
+ # bit_magic :settings, 0 => :is_odd, [1, 2, 3] => :amount, 4 => :is_cool
25
+ # field :flags, type: Integer, default: 0
26
+ # end
27
+ #
28
+ # After that, you can start using query helpers and instance helpers.
29
+ # Query helpers return a standard Mongoid::Criteria, so you can do everything
30
+ # you normally would in a query (like chaining additional conditions).
31
+ # Instance helpers are wrapped around by a Bits object, in this case, 'settings'
32
+ # but also have helper methods added based on the name of the fields.
33
+ #
34
+ module MongoidAdapter
35
+ VERSION = "0.1.0".freeze
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ self.extend Base
40
+ end
41
+
42
+ module ClassMethods
43
+ # Cast the given value into a Boolean, follows Mongoid::Boolean rules
44
+ BIT_MAGIC_BOOLEAN_CASTER = lambda do |val|
45
+ !!Mongoid::Boolean.mongoize(val)
46
+ end
47
+
48
+ # Adapter options specific to this adapter
49
+ #
50
+ # :named_scopes Enables (true) or disables (false) individual scopes to
51
+ # query fields
52
+ #
53
+ # :query_by_value whether to use bitwise operations or IN (?) when querying
54
+ # by default will use IN (?) if the total bits defined by bit_magic is
55
+ # less than or equal to 8. true to always query by value, false to always
56
+ # query using bitwise operations
57
+ BIT_MAGIC_ADAPTER_DEFAULTS = {
58
+ :bool_caster => BIT_MAGIC_BOOLEAN_CASTER,
59
+ :named_scopes => true,
60
+ :query_by_value => 8
61
+ }.freeze
62
+
63
+ # Method used to set adapter defaults as options to Magician,
64
+ # Used by the bit_magic definition to add custom options to the magician
65
+ #
66
+ # @param [Hash] options some options list
67
+ #
68
+ # @return new options list including our custom defaults
69
+ def bit_magic_adapter_defaults(options)
70
+ BIT_MAGIC_ADAPTER_DEFAULTS.merge(options)
71
+ end
72
+
73
+ # This method is called by Base#bit_magic after setting up the magician
74
+ # Here, we inject query helpers, scopes, and other useful methods
75
+ #
76
+ # Query helpers: (NAMESPACE is the name given to bit_magic)
77
+ # All the methods that generate where queries take an optional options
78
+ # hash as the last value. Can be used to alter options given to bit_magic.
79
+ # eg: passing '{query_by_value: false}' as the last argument will force
80
+ # the query to generate bitwise operations instead of '$in => []' queries
81
+ #
82
+ # NAMESPACE_query_helper(field_names = nil)
83
+ # an internal method used by other query helpers
84
+ # NAMESPACE_where_in(array, column_name = nil)
85
+ # generates a 'column_name => {:$in => [...]}' query for the array numbers
86
+ # column_name defaults to attribute_name in the options
87
+ # NAMESPACE_with_all(*field_names, options = {})
88
+ # takes one or more field names, and queries for values where ALL of
89
+ # them are enabled. For fields with multiple bits, they must be max value
90
+ # This is the equivalent of: field[0] and field[1] and field[2] ...
91
+ # NAMESPACE_with_any(*field_names, options = {})
92
+ # takes one or more field names, and queries for values where any of
93
+ # them are enabled
94
+ # This is the equivalent of: field[0] or field[1] or field[2] ...
95
+ # NAMESPACE_without_any(*field_names, options = {})
96
+ # takes one or more field names, and queries for values where at least
97
+ # one of them is disabled. For fields with multiple bits, any value
98
+ # other than maximum number
99
+ # This is the equivalent of "!field[0] or !field[1] or !field[2] ..."
100
+ # NAMESPACE_without_all(*field_names, options = {})
101
+ # takes one or more field names and queries for values where none of
102
+ # them are enabled (all disabled). For fields with multiple bits,
103
+ # value must be zero.
104
+ # This is the equivalent of: !field[0] and !field[1] and !field[2] ...
105
+ # NAMESPACE_equals(field_value_list, options = {})
106
+ # * this will truncate values to match the number of bits available
107
+ # field_value_list is a Hash with field_name => value key-pairs.
108
+ # generates a query that matches the bits to the value, exactly
109
+ # This is the equivalent of: field[0] = val and field[1] = value ...
110
+ #
111
+ # Additional named scopes
112
+ # These can be disabled by passing 'named_scopes: false' as an option
113
+ # FIELD is the field name for the bit/bit range
114
+ #
115
+ # NAMESPACE_FIELD
116
+ # queries for values where FIELD has been enabled
117
+ # NAMESPACE_not_FIELD
118
+ # queries for values where FIELD has been disabled (not enabled)
119
+ # NAMESPACE_FIELD_equals(value)
120
+ # * only exists for fields with more than one bit
121
+ # queries for values where FIELD is exactly equal to value
122
+ #
123
+ # @param [Symbol] name the namespace (prefix) for our query helpers
124
+ #
125
+ # @return nothing important
126
+ def bit_magic_adapter(name)
127
+ query_prep = :"#{name}_query_helper"
128
+ query_in = :"#{name}_where_in"
129
+
130
+ self.class_eval do
131
+ define_singleton_method(query_prep) do |field_names = nil|
132
+ magician = @bit_magic_fields[name]
133
+ bit_gen = magician.bits_generator
134
+
135
+ options = (field_names.is_a?(Array) and field_names.last.is_a?(Hash)) ? field_names.pop : {}
136
+
137
+ by_value = options.key?(:query_by_value) ? options[:query_by_value] : magician.action_options[:query_by_value]
138
+
139
+ by_value = (magician.bits_length <= by_value) if by_value.is_a?(Integer)
140
+ column_name = options[:column_name] || magician.action_options[:column_name] || magician.action_options[:attribute_name]
141
+
142
+ [magician, bit_gen, by_value, column_name]
143
+ end
144
+
145
+ define_singleton_method(query_in) do |column_name, arr|
146
+ where(column_name => {:$in => arr})
147
+ end
148
+
149
+ define_singleton_method(:"#{name}_with_all") do |*field_names|
150
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
151
+
152
+ if by_value === true
153
+ self.send(query_in, column_name, bit_gen.all_of(*field_names))
154
+ else
155
+ where(column_name => {:$bitsAllSet => bit_gen.all_of_number(*field_names)})
156
+ end
157
+ end
158
+
159
+ define_singleton_method(:"#{name}_without_any") do |*field_names|
160
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
161
+
162
+ if by_value === true
163
+ self.send(query_in, column_name, bit_gen.instead_of(*field_names))
164
+ else
165
+ where(column_name => {:$bitsAnyClear => bit_gen.any_of_number(*field_names)})
166
+ end
167
+ end
168
+
169
+ define_singleton_method(:"#{name}_without_all") do |*field_names|
170
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
171
+
172
+ if by_value === true
173
+ self.send(query_in, column_name, bit_gen.none_of(*field_names))
174
+ else
175
+ where(column_name => {:$bitsAllClear => bit_gen.any_of_number(*field_names)})
176
+ end
177
+ end
178
+
179
+ # Query for if any of these bits are set.
180
+ define_singleton_method(:"#{name}_with_any") do |*field_names|
181
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
182
+
183
+ if by_value === true
184
+ self.send(query_in, column_name, bit_gen.any_of(*field_names))
185
+ else
186
+ where(column_name => {:$bitsAnySet => bit_gen.any_of_number(*field_names)})
187
+ end
188
+ end
189
+
190
+ define_singleton_method(:"#{name}_equals") do |field_value, options = {}|
191
+ magician, bit_gen, by_value, column_name = self.send(query_prep, [options])
192
+
193
+ if by_value === true
194
+ self.send(query_in, column_name, bit_gen.equal_to(field_value))
195
+ else
196
+ all_num, none_num = bit_gen.equal_to_numbers(field_value)
197
+ where(column_name => {:$bitsAllSet => all_num, :$bitsAllClear => none_num})
198
+ end
199
+ end
200
+
201
+ end
202
+
203
+
204
+ if @bit_magic_fields and @bit_magic_fields[name] and @bit_magic_fields[name].action_options[:named_scopes]
205
+ fields = @bit_magic_fields[name].field_list
206
+
207
+ self.class_eval do
208
+ fields.each_pair do |field, value|
209
+ define_singleton_method(:"#{name}_#{field}") do
210
+ self.send(:"#{name}_with_all", field)
211
+ end
212
+
213
+ define_singleton_method(:"#{name}_not_#{field}") do
214
+ self.send(:"#{name}_without_all", field)
215
+ end
216
+
217
+ if value.is_a?(Array) and value.length > 1
218
+ define_singleton_method(:"#{name}_#{field}_equals") do |val|
219
+ self.send(:"#{name}_equals", field => val)
220
+ end
221
+ end
222
+
223
+ end
224
+
225
+ end
226
+ end
227
+
228
+ end
229
+ end
230
+
231
+ end
232
+ end
233
+ end