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.
- checksums.yaml +7 -0
- data/.coco.yml +4 -0
- data/.gitignore +10 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +290 -0
- data/Rakefile +39 -0
- data/TODO.md +25 -0
- data/bin/console +14 -0
- data/bin/setup +9 -0
- data/bit_magic.gemspec +30 -0
- data/lib/bit_magic.rb +9 -0
- data/lib/bit_magic/adapters/active_record_adapter.rb +250 -0
- data/lib/bit_magic/adapters/base.rb +61 -0
- data/lib/bit_magic/adapters/magician.rb +226 -0
- data/lib/bit_magic/adapters/mongoid_adapter.rb +233 -0
- data/lib/bit_magic/bit_field.rb +103 -0
- data/lib/bit_magic/bits.rb +285 -0
- data/lib/bit_magic/bits_generator.rb +399 -0
- data/lib/bit_magic/error.rb +7 -0
- data/lib/bit_magic/railtie.rb +28 -0
- data/lib/bit_magic/version.rb +7 -0
- metadata +129 -0
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
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
|