bitmask-attribute 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.markdown
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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,57 @@
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
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "bitmask-attribute #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 0
4
+ :patch: 0
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{bitmask-attribute}
5
+ s.version = "1.0.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Bruce Williams"]
9
+ s.date = %q{2009-05-18}
10
+ s.email = %q{bruce@codefluency.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.markdown"
14
+ ]
15
+ s.files = [
16
+ "LICENSE",
17
+ "README.markdown",
18
+ "Rakefile",
19
+ "VERSION.yml",
20
+ "lib/bitmask-attribute.rb",
21
+ "lib/bitmask_attribute.rb",
22
+ "lib/bitmask_attribute/value_proxy.rb",
23
+ "rails/init.rb",
24
+ "test/bitmask_attribute_test.rb",
25
+ "test/test_helper.rb"
26
+ ]
27
+ s.has_rdoc = true
28
+ s.homepage = %q{http://github.com/bruce/bitmask-attribute}
29
+ s.rdoc_options = ["--charset=UTF-8"]
30
+ s.require_paths = ["lib"]
31
+ s.rubygems_version = %q{1.3.2}
32
+ s.summary = %q{Simple bitmask attribute support for ActiveRecord}
33
+ s.test_files = [
34
+ "test/bitmask_attribute_test.rb",
35
+ "test/test_helper.rb"
36
+ ]
37
+
38
+ if s.respond_to? :specification_version then
39
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
+ s.specification_version = 3
41
+
42
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
43
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
44
+ else
45
+ s.add_dependency(%q<activerecord>, [">= 0"])
46
+ end
47
+ else
48
+ s.add_dependency(%q<activerecord>, [">= 0"])
49
+ end
50
+ end
@@ -0,0 +1,2 @@
1
+ # Stub for dash-style requires
2
+ require File.dirname(__FILE__) << "/bitmask_attribute"
@@ -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
@@ -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 %(
46
+ def #{attribute}
47
+ @#{attribute} ||= BitmaskAttribute::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension)
48
+ end
49
+ )
50
+ end
51
+
52
+ def override_setter_on(model)
53
+ model.class_eval %(
54
+ def #{attribute}=(raw_value)
55
+ values = raw_value.kind_of?(Array) ? raw_value : [raw_value]
56
+ #{attribute}.replace(values)
57
+ end
58
+ )
59
+ end
60
+
61
+ def create_convenience_method_on(model)
62
+ model.class_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
+ )
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
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,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: 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-12-14 00:00:00 -08: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
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.markdown
39
+ - Rakefile
40
+ - VERSION.yml
41
+ - bitmask-attribute.gemspec
42
+ - lib/bitmask-attribute.rb
43
+ - lib/bitmask_attribute.rb
44
+ - lib/bitmask_attribute/value_proxy.rb
45
+ - rails/init.rb
46
+ - test/bitmask_attribute_test.rb
47
+ - test/test_helper.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/bruce/bitmask-attribute
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --charset=UTF-8
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.5
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Simple bitmask attribute support for ActiveRecord
76
+ test_files:
77
+ - test/bitmask_attribute_test.rb
78
+ - test/test_helper.rb