bit_magic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ namespace :test do
12
+
13
+ desc "tests for adapters"
14
+ task :adapters do
15
+ Bundler.with_original_env do
16
+ Dir.chdir File.dirname(__FILE__) do
17
+ tests = FileList["test/**/*_adaptertest.rb"].reduce('') do |m, i|
18
+ m << "require './#{i}';"
19
+ end
20
+ exec 'ruby', '-I"lib:test"', '-e', tests
21
+ end
22
+ end
23
+ end
24
+
25
+ desc "run all tests"
26
+ task :all do
27
+ Bundler.with_original_env do
28
+ Dir.chdir File.dirname(__FILE__) do
29
+ tests = FileList["test/**/*_adaptertest.rb", "test/**/*_test.rb"].reduce('') do |m, i|
30
+ m << "require './#{i}';"
31
+ end
32
+ exec 'ruby', '-I"lib:test"', '-e', tests
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ task :default => :test
data/TODO.md ADDED
@@ -0,0 +1,25 @@
1
+ # TODO
2
+
3
+ * Test against different versions of integration adapters (ActiveRecord and Mongoid). And in the case of ActiveRecord, test against different databases also.
4
+ * Allow value returns on field names, something like:
5
+ ````
6
+ [0, 1] => {:name => :values, 0 => :nothing, 1 => :one, 2 => :two, 3 => :three}
7
+
8
+ values = :three
9
+ values #=> 3
10
+ values_name #=> :four
11
+ `
12
+
13
+ * Strict checking of field attributes (currently they override, we're only checking the flags/fields not the attributes)
14
+ * Better handling and querying of fields
15
+
16
+ # Integrations
17
+
18
+ ## Rails
19
+
20
+ * Form Helpers - something to help with checkbox/select/radio helpers
21
+
22
+ ## ActiveModel (ActiveRecord/Mongoid)
23
+
24
+ * Validators
25
+
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "bit_magic"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
9
+ puts "X"*100
data/bit_magic.gemspec ADDED
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "bit_magic/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bit_magic"
8
+ spec.version = BitMagic::VERSION
9
+ spec.authors = ["Kia Kroas"]
10
+ spec.email = ["rubygems@userhello.net"]
11
+
12
+ spec.summary = %q{Bit field and bit flag utility library with integration for ActiveRecord and Mongoid}
13
+ spec.description = %q{This gem provides basic utility classes for reading and writing specific bits as flags or fields on Integer values. It lets you turn a single integer value into a collection of boolean values (flags) or smaller numbers (fields). Includes integration adapters for ActiveRecord and Mongoid with a simple interface to make your own custom adapter for any other ORM (ActiveModel, ActiveResource, etc).}
14
+ spec.homepage = "https://github.com/userhello/bit_magic"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.16"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ spec.add_development_dependency "coco", "~> 0.15"
30
+ end
data/lib/bit_magic.rb ADDED
@@ -0,0 +1,9 @@
1
+ # Declare the module for namespacing, see other files for code
2
+ module BitMagic
3
+ end
4
+
5
+ require_relative "./bit_magic/version"
6
+ require_relative "./bit_magic/bit_field"
7
+ require_relative "./bit_magic/bits"
8
+ require_relative "./bit_magic/bits_generator"
9
+ require_relative "./bit_magic/adapters/base"
@@ -0,0 +1,250 @@
1
+ require_relative './base'
2
+ require 'active_support/concern'
3
+
4
+ module BitMagic
5
+ module Adapters
6
+ # This is the adapter for ActiveRecord. It's implemented as a concern to be
7
+ # included inside ActiveRecord::Base subclasses.
8
+ #
9
+ # It's expected that you have an integer column (default name 'flags',
10
+ # override using the attribute_name option). It's suggested, though not
11
+ # required, that you set the column as UNSIGNED and NOT NULL.
12
+ #
13
+ # If you have more than one model that you want to use BitMagic in, it's
14
+ # recommended that you just include this adapter globally:
15
+ # require 'bit_magic/adapters/active_record_adapter'
16
+ # ActiveRecord::Base.include BitMagic::Adapters::ActiveRecordAdapter
17
+ #
18
+ # Otherwise, you can include it on a per model basis before calling bit_magic
19
+ #
20
+ # class Example < ActiveRecord::Base
21
+ # include BitMagic::Adapters::ActiveRecordAdapter
22
+ # bit_magic :settings, 0 => :is_odd, [1, 2, 3] => :amount, 4 => :is_cool
23
+ # end
24
+ #
25
+ # After that, you can start using query helpers and instance helpers.
26
+ # Query helpers return a standard ActiveRecord::QueryMethods::WhereChain,
27
+ # so you can do everything you normally can on the query (like chaining conditions).
28
+ # Instance helpers are wrapped around by a Bits object, in this case, 'settings'
29
+ # but also have helper methods added based on the name of the fields.
30
+ #
31
+ module ActiveRecordAdapter
32
+ VERSION = "0.1.0".freeze
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ self.extend Base
37
+ end
38
+
39
+ module ClassMethods
40
+ # A list of values that are falsy. This is only used as a fallback in case
41
+ # the built-in ActiveRecord boolean cast fails.
42
+ # Taken from ActiveModel::Type::Boolean::FALSE_VALUES with the exception
43
+ # that nil is also treated as false.
44
+ BIT_MAGIC_BOOLEAN_FALSE = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', nil].freeze
45
+
46
+ # Cast the given value into a Boolean. Follows ActiveRecord::Type::Boolean
47
+ # casting if available, with the addition that nil is treated as false.
48
+ # Will fall back to using the BIT_MAGIC_BOOLEAN_FALSE list above
49
+ BIT_MAGIC_BOOLEAN_CASTER = lambda do |val|
50
+ if defined?(ActiveRecord::Type::Boolean)
51
+ caster = ActiveRecord::Type::Boolean.new
52
+ # Rails 5 and up
53
+ return !!caster.cast(val) if caster.respond_to?(:cast)
54
+ end
55
+
56
+ # Fallback, values copied from ActiveRecord::Type::Boolean from Rails 5
57
+ # can be found at ActiveRecord::ConnectionAdapters::Column for Rails <=4
58
+ !BIT_MAGIC_BOOLEAN_FALSE.include?(val)
59
+ end
60
+
61
+ # Adapter options specific to this adapter
62
+ #
63
+ # :named_scopes Enables (true) or disables (false) individual scopes to
64
+ # query fields
65
+ #
66
+ # :query_by_value whether to use bitwise operations or IN (?) when querying
67
+ # by default will use IN (?) if the total bits defined by bit_magic is
68
+ # less than or equal to 8. true to always query by value, false to always
69
+ # query using bitwise operations
70
+ BIT_MAGIC_ADAPTER_DEFAULTS = {
71
+ :bool_caster => BIT_MAGIC_BOOLEAN_CASTER,
72
+ :named_scopes => true,
73
+ :query_by_value => 8
74
+ }.freeze
75
+
76
+ # Method used to set adapter defaults as options to Magician,
77
+ # Used by the bit_magic definition to add custom options to the magician
78
+ #
79
+ # @param [Hash] options some options list
80
+ #
81
+ # @return new options list including our custom defaults
82
+ def bit_magic_adapter_defaults(options = {})
83
+ BIT_MAGIC_ADAPTER_DEFAULTS.merge(options)
84
+ end
85
+
86
+ # This method is called by Base#bit_magic after setting up the magician
87
+ # Here, we inject query helpers, scopes, and other useful methods
88
+ #
89
+ # Query helpers: (NAMESPACE is the name given to bit_magic)
90
+ # All the methods that generate where queries take an optional options
91
+ # hash as the last value. Can be used to alter options given to bit_magic.
92
+ # eg: passing '{query_by_value: false}' as the last argument will force
93
+ # the query to generate bitwise operations instead of 'IN (?)' queries
94
+ #
95
+ # NAMESPACE_query_helper(field_names = nil)
96
+ # an internal method used by other query helpers
97
+ # NAMESPACE_where_in(array, column_name = nil)
98
+ # generates a 'WHERE column_name IN (?)' query for the array numbers
99
+ # column_name defaults to attribute_name in the options
100
+ # NAMESPACE_with_all(*field_names, options = {})
101
+ # takes one or more field names, and queries for values where ALL of
102
+ # them are enabled. For fields with multiple bits, they must be max value
103
+ # This is the equivalent of: field[0] and field[1] and field[2] ...
104
+ # NAMESPACE_with_any(*field_names, options = {})
105
+ # takes one or more field names, and queries for values where any of
106
+ # them are enabled.
107
+ # This is the equivalent of: field[0] or field[1] or field[2] ...
108
+ # NAMESPACE_without_any(*field_names, options = {})
109
+ # takes one or more field names, and queries for values where at least
110
+ # one of them is disabled. For fields with multiple bits, any value
111
+ # other than maximum number.
112
+ # This is the equivalent of: !field[0] or !field[1] or !field[2] ...
113
+ # NAMESPACE_without_all(*field_names, options = {})
114
+ # takes one or more field names and queries for values where none of
115
+ # them are enabled (all disabled). For fields with multiple bits,
116
+ # value must be zero.
117
+ # This is the equivalent of: !field[0] and !field[1] and !field[2] ...
118
+ # NAMESPACE_equals(field_value_list, options = {})
119
+ # * this will truncate values to match the number of bits available
120
+ # field_value_list is a Hash with field_name => value key-pairs.
121
+ # generates a query that matches the bits to the value, exactly
122
+ # This is the equivalent of: field[0] = val and field[1] = value ...
123
+ #
124
+ # Additional named scopes
125
+ # These can be disabled by passing 'named_scopes: false' as an option
126
+ # FIELD is the field name for the bit/bit range
127
+ #
128
+ # NAMESPACE_FIELD
129
+ # queries for values where FIELD has been enabled
130
+ # NAMESPACE_not_FIELD
131
+ # queries for values where FIELD has been disabled (not enabled)
132
+ # NAMESPACE_FIELD_equals(value)
133
+ # * only exists for fields with more than one bit
134
+ # queries for values where FIELD is exactly equal to value
135
+ #
136
+ # @param [Symbol] name the namespace (prefix) for our query helpers
137
+ #
138
+ # @return nothing important
139
+ def bit_magic_adapter(name)
140
+ query_prep = :"#{name}_query_helper"
141
+ query_in = :"#{name}_where_in"
142
+
143
+ self.class_eval do
144
+
145
+ # Internal method used by the other queries
146
+ define_singleton_method(query_prep) do |field_names = nil|
147
+ magician = @bit_magic_fields[name]
148
+ bit_gen = magician.bits_generator
149
+
150
+ options = (field_names.is_a?(Array) and field_names.last.is_a?(Hash)) ? field_names.pop : {}
151
+
152
+ by_value = options.key?(:query_by_value) ? options[:query_by_value] : magician.action_options[:query_by_value]
153
+
154
+ by_value = (magician.bits_length <= by_value) if by_value.is_a?(Integer)
155
+ column_name = options[:column_name] || magician.action_options[:column_name] || magician.action_options[:attribute_name]
156
+
157
+ [magician, bit_gen, by_value, column_name]
158
+ end
159
+
160
+ define_singleton_method(query_in) do |arr, column_name = nil|
161
+ where("\"#{self.table_name}\".#{column_name} IN (?)", arr)
162
+ end
163
+
164
+ define_singleton_method(:"#{name}_with_all") do |*field_names|
165
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
166
+
167
+ if by_value === true
168
+ self.send(query_in, bit_gen.all_of(*field_names), column_name)
169
+ else
170
+ all_of_num = bit_gen.all_of_number(*field_names)
171
+ where("(\"#{self.table_name}\".#{column_name} & ?) == ?", all_of_num, all_of_num)
172
+ end
173
+ end
174
+
175
+ define_singleton_method(:"#{name}_without_any") do |*field_names|
176
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
177
+
178
+ if by_value === true
179
+ self.send(query_in, bit_gen.instead_of(*field_names), column_name)
180
+ else
181
+ bit_num = bit_gen.any_of_number(*field_names)
182
+ where("(\"#{self.table_name}\".#{column_name} & ?) != ?", bit_num, bit_num)
183
+ end
184
+ end
185
+
186
+ define_singleton_method(:"#{name}_without_all") do |*field_names|
187
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
188
+
189
+ if by_value === true
190
+ self.send(query_in, bit_gen.none_of(*field_names), column_name)
191
+ else
192
+ where("(\"#{self.table_name}\".#{column_name} & ?) == 0", bit_gen.any_of_number(*field_names))
193
+ end
194
+ end
195
+
196
+ # Query for if any of these bits are set.
197
+ define_singleton_method(:"#{name}_with_any") do |*field_names|
198
+ magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
199
+
200
+ if by_value === true
201
+ self.send(query_in, bit_gen.any_of(*field_names), column_name)
202
+ else
203
+ where("(\"#{self.table_name}\".#{column_name} & ?) > 0", bit_gen.any_of_number(*field_names))
204
+ end
205
+ end
206
+
207
+ define_singleton_method(:"#{name}_equals") do |field_value, options = {}|
208
+ magician, bit_gen, by_value, column_name = self.send(query_prep, [options])
209
+
210
+ if by_value === true
211
+ self.send(query_in, bit_gen.equal_to(field_value), column_name)
212
+ else
213
+ all_num, none_num = bit_gen.equal_to_numbers(field_value)
214
+ where("(\"#{self.table_name}\".#{column_name} & ?) == ? AND (\"#{self.table_name}\".#{column_name} & ?) == 0", all_num, all_num, none_num)
215
+ end
216
+ end
217
+
218
+ end
219
+
220
+ if @bit_magic_fields and @bit_magic_fields[name] and @bit_magic_fields[name].action_options[:named_scopes]
221
+ fields = @bit_magic_fields[name].field_list
222
+
223
+ self.class_eval do
224
+ fields.each_pair do |field, value|
225
+ define_singleton_method(:"#{name}_#{field}") do
226
+ self.send(:"#{name}_with_all", field)
227
+ end
228
+
229
+ define_singleton_method(:"#{name}_not_#{field}") do
230
+ self.send(:"#{name}_without_all", field)
231
+ end
232
+
233
+ if value.is_a?(Array) and value.length > 1
234
+ define_singleton_method(:"#{name}_#{field}_equals") do |val|
235
+ self.send(:"#{name}_equals", field => val)
236
+ end
237
+ end
238
+
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+
245
+ end
246
+
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,61 @@
1
+ require_relative '../../bit_magic'
2
+ require_relative './magician'
3
+ require_relative "../error"
4
+
5
+ module BitMagic
6
+ module Adapters
7
+ # This module is the core to all adapters. It provides the primary functionality
8
+ # that's the starting point of all adapters: the bit_magic method.
9
+ #
10
+ # This module is intended to be extended into the class scope through an adapter.
11
+ #
12
+ # @example
13
+ # class X
14
+ # extend Base
15
+ # end
16
+ # X.bit_magic :name, 0 => :is_odd, 1 => :ok
17
+ #
18
+ module Base
19
+ # This is the bit_magic method that will be injected into the target class
20
+ # once this module is extended.
21
+ #
22
+ # @param [Symbol] name the name to use as a namespace for bit magic methods
23
+ # @param [Hash] options any additional options, individual options are
24
+ # based off the adapter and Magician defaults.
25
+ #
26
+ # @return [Magician] a Magician object for this invocation
27
+ # @return [Hash] if no name is given, will return a Hash with all magicians
28
+ def bit_magic(name = nil, options = {})
29
+ @bit_magic_fields ||= {}
30
+ return @bit_magic_fields if name == nil
31
+
32
+ if self.respond_to?(:bit_magic_adapter_defaults)
33
+ options = self.bit_magic_adapter_defaults(options)
34
+ end
35
+
36
+ name = name.to_sym
37
+
38
+ @bit_magic_fields[name] = magician = Magician.new(name, options)
39
+
40
+ @bit_magic_fields[name].define_bit_magic_methods self
41
+
42
+ self.instance_eval do
43
+ define_method(:"#{name}") do
44
+ ivar = :"@bit_magic_#{name}"
45
+
46
+ if instance_variable_defined?(ivar)
47
+ instance_variable_get(ivar)
48
+ else
49
+ instance_variable_set(ivar, magician.bits_wrapper.new(self, magician))
50
+ end
51
+ end
52
+ end
53
+
54
+ self.bit_magic_adapter(name) if self.respond_to?(:bit_magic_adapter)
55
+
56
+ @bit_magic_fields[name]
57
+ end
58
+ end
59
+
60
+ end
61
+ end