bruce-bitmask-attribute 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2009 Bruce Williams
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,47 @@
1
+ # bitmask-attribute
2
+
3
+ Transparent manipulation of bitmask attributes.
4
+
5
+ ## Example
6
+
7
+ Simply declare an existing integer column as a bitmask with its possible
8
+ values.
9
+
10
+ class User < ActiveRecord::Base
11
+ bitmask :roles, :as => [:writer, :publisher, :editor]
12
+ end
13
+
14
+ You can then modify the column using the declared values without resorting
15
+ to manual bitmasks.
16
+
17
+ user = User.create(:name => "Bruce", :roles => [:publisher, :editor])
18
+ user.roles
19
+ # => [:publisher, :editor]
20
+ user.roles << :writer
21
+ user.roles
22
+ # => [:publisher, :editor, :writer]
23
+
24
+ For the moment, querying for bitmasks is left as an exercise to the reader,
25
+ but here's how to grab the bitmask for a specific possible value for use in
26
+ your SQL query:
27
+
28
+ bitmask = User.bitmasks[:roles][:editor]
29
+ # Use `bitmask` as needed
30
+
31
+ ## Modifying possible values
32
+
33
+ Once you have data using a bitmask, don't change the order of the values,
34
+ remove any values, or insert any new values in the array anywhere except at
35
+ the end.
36
+
37
+ ## Contributing and reporting issues
38
+
39
+ Please feel free to fork & contribute fixes via GitHub pull requests.
40
+ The official repository for this project is
41
+ http://github.com/bruce/bitmask-attribute
42
+
43
+ Issues can be reported at http://github.com/bruce/bitmask-attribute/issues
44
+
45
+ ## Copyright
46
+
47
+ Copyright (c) 2007-2009 Bruce Williams. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "bitmask-attribute"
8
+ gem.summary = %Q{Simple bitmask attribute support for ActiveRecord}
9
+ gem.email = "bruce@codefluency.com"
10
+ gem.homepage = "http://github.com/bruce/bitmask-attribute"
11
+ gem.authors = ["Bruce Williams"]
12
+ gem.add_dependency 'activerecord'
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "bitmask-attribute #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 0
4
+ :patch: 0
@@ -0,0 +1,2 @@
1
+ # Stub for dash-style requires
2
+ require File.dirname(__FILE__) << "/bitmask_attribute"
@@ -0,0 +1,100 @@
1
+ require 'bitmask_attribute/value_proxy'
2
+
3
+ module BitmaskAttribute
4
+
5
+ class Definition
6
+
7
+ attr_reader :attribute, :values, :extension
8
+ def initialize(attribute, values=[], &extension)
9
+ @attribute = attribute
10
+ @values = values
11
+ @extension = extension
12
+ end
13
+
14
+ def install_on(model)
15
+ validate_for model
16
+ generate_bitmasks_on model
17
+ override model
18
+ create_convenience_method_on model
19
+ end
20
+
21
+ #######
22
+ private
23
+ #######
24
+
25
+ def validate_for(model)
26
+ unless model.columns.detect { |col| col.name == attribute.to_s && col.type == :integer }
27
+ raise ArgumentError, "`#{attribute}' is not an integer column of `#{model}'"
28
+ end
29
+ end
30
+
31
+ def generate_bitmasks_on(model)
32
+ model.bitmasks[attribute] = returning HashWithIndifferentAccess.new do |mapping|
33
+ values.each_with_index do |value, index|
34
+ mapping[value] = 0b1 << index
35
+ end
36
+ end
37
+ end
38
+
39
+ def override(model)
40
+ override_getter_on(model)
41
+ override_setter_on(model)
42
+ end
43
+
44
+ def override_getter_on(model)
45
+ model.class_eval(<<-EVAL)
46
+ def #{attribute}
47
+ @#{attribute} ||= BitmaskAttribute::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension)
48
+ end
49
+ EVAL
50
+ end
51
+
52
+ def override_setter_on(model)
53
+ model.class_eval(<<-EVAL)
54
+ def #{attribute}=(raw_value)
55
+ values = raw_value.kind_of?(Array) ? raw_value : [raw_value]
56
+ #{attribute}.replace(values)
57
+ end
58
+ EVAL
59
+ end
60
+
61
+ def create_convenience_method_on(model)
62
+ model.class_eval(<<-EVAL)
63
+ def self.bitmask_for_#{attribute}(*values)
64
+ values.inject(0) do |bitmask, value|
65
+ unless (bit = bitmasks[:#{attribute}][value])
66
+ raise ArgumentError, "Unsupported value for #{attribute}: \#{value.inspect}"
67
+ end
68
+ bitmask | bit
69
+ end
70
+ end
71
+ EVAL
72
+ end
73
+
74
+ end
75
+
76
+ def self.included(model)
77
+ model.extend ClassMethods
78
+ end
79
+
80
+ module ClassMethods
81
+
82
+ def bitmask(attribute, options={}, &extension)
83
+ unless options[:as] && options[:as].kind_of?(Array)
84
+ raise ArgumentError, "Must provide an Array :as option"
85
+ end
86
+ bitmask_definitions[attribute] = BitmaskAttribute::Definition.new(attribute, options[:as].to_a, &extension)
87
+ bitmask_definitions[attribute].install_on(self)
88
+ end
89
+
90
+ def bitmask_definitions
91
+ @bitmask_definitions ||= {}
92
+ end
93
+
94
+ def bitmasks
95
+ @bitmasks ||= {}
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,72 @@
1
+ module BitmaskAttribute
2
+
3
+ class ValueProxy < Array
4
+
5
+ def initialize(record, attribute, &extension)
6
+ @record = record
7
+ @attribute = attribute
8
+ find_mapping
9
+ instance_eval(&extension) if extension
10
+ super(extract_values)
11
+ end
12
+
13
+ # =========================
14
+ # = OVERRIDE TO SERIALIZE =
15
+ # =========================
16
+
17
+ %w(push << delete replace reject! select!).each do |override|
18
+ class_eval(<<-EOEVAL)
19
+ def #{override}(*args)
20
+ returning(super) do
21
+ updated!
22
+ end
23
+ end
24
+ EOEVAL
25
+ end
26
+
27
+ def to_i
28
+ inject(0) { |memo, value| memo | @mapping[value] }
29
+ end
30
+
31
+ #######
32
+ private
33
+ #######
34
+
35
+ def validate!
36
+ each do |value|
37
+ if @mapping.key? value
38
+ true
39
+ else
40
+ raise ArgumentError, "Unsupported value for `#{@attribute}': #{value.inspect}"
41
+ end
42
+ end
43
+ end
44
+
45
+ def updated!
46
+ validate!
47
+ uniq!
48
+ serialize!
49
+ end
50
+
51
+ def serialize!
52
+ @record.send(:write_attribute, @attribute, to_i)
53
+ end
54
+
55
+ def extract_values
56
+ stored = @record.send(:read_attribute, @attribute) || 0
57
+ @mapping.inject([]) do |values, (value, bitmask)|
58
+ returning values do
59
+ values << value.to_sym if (stored & bitmask > 0)
60
+ end
61
+ end
62
+ end
63
+
64
+ def find_mapping
65
+ unless (@mapping = @record.class.bitmasks[@attribute])
66
+ raise ArgumentError, "Could not find mapping for bitmask attribute :#{@attribute}"
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Base.instance_eval do
2
+ include BitmaskAttribute
3
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+
3
+ class BitmaskAttributeTest < Test::Unit::TestCase
4
+
5
+ context "Campaign" do
6
+
7
+ should "can assign single value to bitmask" do
8
+ assert_stored Campaign.new(:medium => :web), :web
9
+ end
10
+
11
+ should "can assign multiple values to bitmask" do
12
+ assert_stored Campaign.new(:medium => [:web, :print]), :web, :print
13
+ end
14
+
15
+ should "can add single value to bitmask" do
16
+ campaign = Campaign.new(:medium => [:web, :print])
17
+ assert_stored campaign, :web, :print
18
+ campaign.medium << :phone
19
+ assert_stored campaign, :web, :print, :phone
20
+ end
21
+
22
+ should "ignores duplicate values added to bitmask" do
23
+ campaign = Campaign.new(:medium => [:web, :print])
24
+ assert_stored campaign, :web, :print
25
+ campaign.medium << :phone
26
+ assert_stored campaign, :web, :print, :phone
27
+ campaign.medium << :phone
28
+ assert_stored campaign, :web, :print, :phone
29
+ assert_equal 1, campaign.medium.select { |value| value == :phone }.size
30
+ end
31
+
32
+ should "can assign new values at once to bitmask" do
33
+ campaign = Campaign.new(:medium => [:web, :print])
34
+ assert_stored campaign, :web, :print
35
+ campaign.medium = [:phone, :email]
36
+ assert_stored campaign, :phone, :email
37
+ end
38
+
39
+ should "can save bitmask to db and retrieve values transparently" do
40
+ campaign = Campaign.new(:medium => [:web, :print])
41
+ assert_stored campaign, :web, :print
42
+ assert campaign.save
43
+ assert_stored Campaign.find(campaign.id), :web, :print
44
+ end
45
+
46
+ should "can add custom behavor to value proxies during bitmask definition" do
47
+ campaign = Campaign.new(:medium => [:web, :print])
48
+ assert_raises NoMethodError do
49
+ campaign.medium.worked?
50
+ end
51
+ assert_nothing_raised do
52
+ campaign.misc.worked?
53
+ end
54
+ assert campaign.misc.worked?
55
+ end
56
+
57
+ should "cannot use unsupported values" do
58
+ assert_unsupported { Campaign.new(:medium => [:web, :print, :this_will_fail]) }
59
+ campaign = Campaign.new(:medium => :web)
60
+ assert_unsupported { campaign.medium << :this_will_fail_also }
61
+ assert_unsupported { campaign.medium = [:so_will_this] }
62
+ end
63
+
64
+ should "can determine bitmasks using convenience method" do
65
+ assert Campaign.bitmask_for_medium(:web, :print)
66
+ assert_equal(
67
+ Campaign.bitmasks[:medium][:web] | Campaign.bitmasks[:medium][:print],
68
+ Campaign.bitmask_for_medium(:web, :print)
69
+ )
70
+ end
71
+
72
+ should "assert use of unknown value in convenience method will result in exception" do
73
+ assert_unsupported { Campaign.bitmask_for_medium(:web, :and_this_isnt_valid) }
74
+ end
75
+
76
+ should "hash of values is with indifferent access" do
77
+ string_bit = nil
78
+ assert_nothing_raised do
79
+ assert (string_bit = Campaign.bitmask_for_medium('web', 'print'))
80
+ end
81
+ assert_equal Campaign.bitmask_for_medium(:web, :print), string_bit
82
+ end
83
+
84
+ #######
85
+ private
86
+ #######
87
+
88
+ def assert_unsupported(&block)
89
+ assert_raises(ArgumentError, &block)
90
+ end
91
+
92
+ def assert_stored(record, *values)
93
+ values.each do |value|
94
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
95
+ end
96
+ full_mask = values.inject(0) do |mask, value|
97
+ mask | Campaign.bitmasks[:medium][value]
98
+ end
99
+ assert_equal full_mask, record.medium.to_i
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ begin
5
+ require 'redgreen'
6
+ rescue LoadError
7
+ end
8
+
9
+ require 'active_support'
10
+ require 'active_record'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'bitmask-attribute'
15
+ require File.dirname(__FILE__) + '/../rails/init'
16
+
17
+ ActiveRecord::Base.establish_connection(
18
+ :adapter => 'sqlite3',
19
+ :database => ':memory:'
20
+ )
21
+
22
+ ActiveRecord::Schema.define do
23
+ create_table :campaigns do |table|
24
+ table.column :medium, :integer
25
+ table.column :misc, :integer
26
+ end
27
+ end
28
+
29
+ # Pseudo model for testing purposes
30
+ class Campaign < ActiveRecord::Base
31
+ bitmask :medium, :as => [:web, :print, :email, :phone]
32
+ bitmask :misc, :as => %w(some useless values) do
33
+ def worked?
34
+ true
35
+ end
36
+ end
37
+ end
38
+
39
+ class Test::Unit::TestCase
40
+
41
+ def assert_unsupported(&block)
42
+ assert_raises(ArgumentError, &block)
43
+ end
44
+
45
+ def assert_stored(record, *values)
46
+ values.each do |value|
47
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
48
+ end
49
+ full_mask = values.inject(0) do |mask, value|
50
+ mask | Campaign.bitmasks[:medium][value]
51
+ end
52
+ assert_equal full_mask, record.medium.to_i
53
+ end
54
+
55
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bruce-bitmask-attribute
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bruce Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-18 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: bruce@codefluency.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.markdown
34
+ files:
35
+ - LICENSE
36
+ - README.markdown
37
+ - Rakefile
38
+ - VERSION.yml
39
+ - lib/bitmask-attribute.rb
40
+ - lib/bitmask_attribute.rb
41
+ - lib/bitmask_attribute/value_proxy.rb
42
+ - rails/init.rb
43
+ - test/bitmask_attribute_test.rb
44
+ - test/test_helper.rb
45
+ has_rdoc: true
46
+ homepage: http://github.com/bruce/bitmask-attribute
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --charset=UTF-8
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.2.0
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Simple bitmask attribute support for ActiveRecord
71
+ test_files:
72
+ - test/bitmask_attribute_test.rb
73
+ - test/test_helper.rb