bit_magic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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