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.
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