has_price 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/has_price.gemspec +66 -0
- data/lib/has_price/core_extensions/array.rb +12 -0
- data/lib/has_price/core_extensions/string.rb +16 -0
- data/lib/has_price/has_price.rb +52 -0
- data/lib/has_price/price.rb +52 -0
- data/lib/has_price/price_builder.rb +88 -0
- data/lib/has_price.rb +14 -0
- data/rails/init.rb +3 -0
- data/test/helper.rb +13 -0
- data/test/test_has_price.rb +85 -0
- data/test/test_price.rb +63 -0
- data/test/test_price_builder.rb +59 -0
- metadata +94 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Maxim Chernyak
|
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.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
has_price
|
2
|
+
=========
|
3
|
+
|
4
|
+
Let's just say, it organizes your price breakdowns and allows for easy retrieval of price subgroups and subtotals, as well as simple serialization for your receipts.
|
5
|
+
|
6
|
+
Install
|
7
|
+
-------
|
8
|
+
|
9
|
+
Make sure [gemcutter.org](http://gemcutter.org) is in your sources.
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
sudo gem install has_price
|
13
|
+
</pre>
|
14
|
+
|
15
|
+
In rails environment:
|
16
|
+
<pre>
|
17
|
+
config.gem "has_price"
|
18
|
+
</pre>
|
19
|
+
|
20
|
+
For any generic Ruby class:
|
21
|
+
<pre>
|
22
|
+
require 'has_price'
|
23
|
+
include HasPrice::HasPrice
|
24
|
+
</pre>
|
25
|
+
|
26
|
+
P.S. Usage as Rails plugin is supported too, but gem is preferred.
|
27
|
+
|
28
|
+
Organize
|
29
|
+
--------
|
30
|
+
|
31
|
+
Say you have a Product class with some attributes which price depends on. For this example assume that base_price, federal_tax, and state_tax are integer attributes existing on Product model.
|
32
|
+
|
33
|
+
<pre lang="ruby">
|
34
|
+
class Product < ActiveRecord::Base
|
35
|
+
has_many :discounts
|
36
|
+
end
|
37
|
+
</pre>
|
38
|
+
|
39
|
+
has_price provides a small DSL with two methods, `item` and `group`, to help you organize this.
|
40
|
+
|
41
|
+
<pre lang="ruby">
|
42
|
+
class Product < ActiveRecord::Base
|
43
|
+
has_many :discounts
|
44
|
+
|
45
|
+
has_price do
|
46
|
+
item base_price, "base"
|
47
|
+
group "taxes" do
|
48
|
+
item federal_tax, "federal"
|
49
|
+
item state_tax, "state"
|
50
|
+
end
|
51
|
+
group "discounts" do
|
52
|
+
discounts.each do |discount|
|
53
|
+
item discount.amount, discount.title
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
</pre>
|
59
|
+
|
60
|
+
What we've done just now is — built instance method `price` on products. Now you can use it as so.
|
61
|
+
|
62
|
+
<pre lang="ruby">
|
63
|
+
# Hypothetically all these numbers are coming from the above declared instance methods.
|
64
|
+
|
65
|
+
product = Product.find(1)
|
66
|
+
product.price # => Price object
|
67
|
+
product.price.total # => 500
|
68
|
+
product.price.base # => 400
|
69
|
+
product.price.taxes # => Price object
|
70
|
+
product.price.taxes.federal # => 100
|
71
|
+
product.price.taxes.total # => 200
|
72
|
+
product.discounts.total # => -100
|
73
|
+
</pre>
|
74
|
+
|
75
|
+
Serialize
|
76
|
+
---------
|
77
|
+
|
78
|
+
Price object actually inherits from a plain old Hash. Therefore, this will work:
|
79
|
+
|
80
|
+
<pre lang="ruby">
|
81
|
+
class Receipt < ActiveRecord::Base
|
82
|
+
serialize :price, Hash
|
83
|
+
end
|
84
|
+
</pre>
|
85
|
+
|
86
|
+
Now passing the whole price breakdown into receipt is as simple as `receipt.price = product.price`.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "has_price"
|
8
|
+
gem.summary = %Q{Provides a convenient DSL for organizing a price breakdown in a class.}
|
9
|
+
gem.description = %Q{A convenient DSL for defining complex price reader/serializer in a class and organizing a price breakdown. Price can be declared with items and groups which depend on other attributes. Price is a very simple subclass of Hash. This provides for easy serialization and flexibility in case of implementation changes. This way you can conveniently store the whole price breakdown in your serialized receipts. It also provides magic methods for convenient access, but can be fully treated as a regular Hash with some sprinkles on top.}
|
10
|
+
gem.email = "max@bitsonnet.com"
|
11
|
+
gem.homepage = "http://github.com/maxim/has_price"
|
12
|
+
gem.authors = ["Maxim Chernyak"]
|
13
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
14
|
+
gem.add_development_dependency "rr"
|
15
|
+
gem.files.include %w(lib/*/**)
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.libs << 'lib' << 'test'
|
25
|
+
test.pattern = 'test/**/test_*.rb'
|
26
|
+
test.verbose = true
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'rcov/rcovtask'
|
31
|
+
Rcov::RcovTask.new do |test|
|
32
|
+
test.libs << 'test'
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
task :rcov do
|
38
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test => :check_dependencies
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "has_price #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/has_price.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{has_price}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Maxim Chernyak"]
|
12
|
+
s.date = %q{2009-12-02}
|
13
|
+
s.description = %q{A convenient DSL for defining complex price reader/serializer in a class and organizing a price breakdown. Price can be declared with items and groups which depend on other attributes. Price is a very simple subclass of Hash. This provides for easy serialization and flexibility in case of implementation changes. This way you can conveniently store the whole price breakdown in your serialized receipts. It also provides magic methods for convenient access, but can be fully treated as a regular Hash with some sprinkles on top.}
|
14
|
+
s.email = %q{max@bitsonnet.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README.md",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"has_price.gemspec",
|
26
|
+
"lib/has_price.rb",
|
27
|
+
"lib/has_price/core_extensions/array.rb",
|
28
|
+
"lib/has_price/core_extensions/string.rb",
|
29
|
+
"lib/has_price/has_price.rb",
|
30
|
+
"lib/has_price/price.rb",
|
31
|
+
"lib/has_price/price_builder.rb",
|
32
|
+
"rails/init.rb",
|
33
|
+
"test/helper.rb",
|
34
|
+
"test/test_has_price.rb",
|
35
|
+
"test/test_price.rb",
|
36
|
+
"test/test_price_builder.rb"
|
37
|
+
]
|
38
|
+
s.homepage = %q{http://github.com/maxim/has_price}
|
39
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.5}
|
42
|
+
s.summary = %q{Provides a convenient DSL for organizing a price breakdown in a class.}
|
43
|
+
s.test_files = [
|
44
|
+
"test/helper.rb",
|
45
|
+
"test/test_has_price.rb",
|
46
|
+
"test/test_price.rb",
|
47
|
+
"test/test_price_builder.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<rr>, [">= 0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
59
|
+
s.add_dependency(%q<rr>, [">= 0"])
|
60
|
+
end
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
63
|
+
s.add_dependency(%q<rr>, [">= 0"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module HasPrice
|
2
|
+
module CoreExtensions
|
3
|
+
module Array
|
4
|
+
# In case we're not in Rails.
|
5
|
+
#
|
6
|
+
# @see http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Array/ExtractOptions.html#M001202
|
7
|
+
def extract_options!
|
8
|
+
last.is_a?(::Hash) ? pop : {}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module HasPrice
|
2
|
+
module CoreExtensions
|
3
|
+
module String
|
4
|
+
# In case we're not in Rails.
|
5
|
+
#
|
6
|
+
# @see http://api.rubyonrails.org/classes/Inflector.html#M001631
|
7
|
+
def underscore
|
8
|
+
gsub(/::/, '/').
|
9
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
10
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
11
|
+
tr("-", "_").
|
12
|
+
downcase
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module HasPrice
|
2
|
+
module HasPrice
|
3
|
+
|
4
|
+
# Provides a simple DSL to defines price instance method on the receiver.
|
5
|
+
#
|
6
|
+
# @param [Hash] options the options for creating price method.
|
7
|
+
# @option options [Symbol] :attribute (:price) Name of the price method.
|
8
|
+
# @option options [Boolean] :free (false) Set `:free => true` to use null object pattern.
|
9
|
+
#
|
10
|
+
# @yield The yielded block provides method `item` for declaring price entries,
|
11
|
+
# and method `group` for declaring price groups.
|
12
|
+
#
|
13
|
+
# @example Normal usage
|
14
|
+
# class Product < ActiveRecord::Base
|
15
|
+
# has_price do
|
16
|
+
# item base_price, "base"
|
17
|
+
# item discount, "discount"
|
18
|
+
#
|
19
|
+
# group "taxes" do
|
20
|
+
# item federal_tax, "federal tax"
|
21
|
+
# item state_tax, "state tax"
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# group "shipment" do
|
25
|
+
# # Notice that delivery_method is an instance method.
|
26
|
+
# # You can call instance methods anywhere in has_price block.
|
27
|
+
# item delivery_price, delivery_method
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @example Null object pattern
|
33
|
+
# class Product < ActiveRecord::Base
|
34
|
+
# # Creates method #price which returns empty Price.
|
35
|
+
# has_price :free => true
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# @see PriceBuilder#item
|
39
|
+
# @see PriceBuilder#group
|
40
|
+
#
|
41
|
+
def has_price(options = {}, &block)
|
42
|
+
attribute = options[:attribute] || :price
|
43
|
+
free = !block_given? && options[:free]
|
44
|
+
|
45
|
+
define_method attribute.to_sym do
|
46
|
+
builder = PriceBuilder.new self
|
47
|
+
builder.instance_eval &block unless free
|
48
|
+
builder.price
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module HasPrice
|
2
|
+
class Price < Hash
|
3
|
+
# @return [Fixnum] the recursive sum of all declared prices.
|
4
|
+
def to_i; recursive_sum end
|
5
|
+
# @return [String] the output of to_i converted to string.
|
6
|
+
def to_s; to_i.to_s end
|
7
|
+
# @return [Hash] the price as a Hash object.
|
8
|
+
def to_hash; Hash[self] end
|
9
|
+
alias total to_i
|
10
|
+
|
11
|
+
# Provides access to price items and groups using magic methods and chaining.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class Product
|
15
|
+
# has_price do
|
16
|
+
# item 400, "base"
|
17
|
+
# group "tax" do
|
18
|
+
# item 100, "federal"
|
19
|
+
# item 50, "state"
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# product = Product.new
|
25
|
+
# product.price # => Full Price object
|
26
|
+
# product.price.base # => 400
|
27
|
+
# product.price.tax # => Price object on group tax
|
28
|
+
# product.price.tax.federal # => 100
|
29
|
+
# product.price.tax.total # => 150
|
30
|
+
#
|
31
|
+
# @return [Price, Fixnum] Price object if method matches a group, Fixnum if method matches an item.
|
32
|
+
def method_missing(meth, *args, &blk)
|
33
|
+
value = select{|k,v| k.underscore == meth.to_s}.first
|
34
|
+
|
35
|
+
if !value
|
36
|
+
super
|
37
|
+
elsif value.last.is_a?(Hash)
|
38
|
+
self.class[value.last]
|
39
|
+
else
|
40
|
+
value.last
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def recursive_sum(target = self)
|
46
|
+
target.inject(0) do |sum, pair|
|
47
|
+
value = pair.last
|
48
|
+
sum += value.is_a?(Hash) ? recursive_sum(value) : value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module HasPrice
|
2
|
+
class PriceBuilder
|
3
|
+
attr_reader :price
|
4
|
+
|
5
|
+
# Creates PriceBuilder on a target object.
|
6
|
+
#
|
7
|
+
# @param [Object] object the target object on which price is being built.
|
8
|
+
def initialize(object)
|
9
|
+
@price = Price.new
|
10
|
+
@current_nesting_level = @price
|
11
|
+
@object = object
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds price item to the current nesting level of price definition.
|
15
|
+
#
|
16
|
+
# @param [#to_hash, #to_i] price an integer representing amount for this price item.
|
17
|
+
# Alternatively, anything that responds to #to_hash can be used,
|
18
|
+
# and will be treated as a group named with item_name.
|
19
|
+
# @param [#to_s] item_name name for the provided price item or group.
|
20
|
+
#
|
21
|
+
# @see #group
|
22
|
+
def item(price, item_name)
|
23
|
+
@current_nesting_level[item_name.to_s] = price.respond_to?(:to_hash) ? price.to_hash : price.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds price group to the current nesting level of price definition.
|
27
|
+
# Groups are useful for price breakdown categorization and easy subtotal values.
|
28
|
+
#
|
29
|
+
# @example Using group subtotals
|
30
|
+
# class Product
|
31
|
+
# include HasPrice
|
32
|
+
#
|
33
|
+
# def base_price; 100 end
|
34
|
+
# def federal_tax; 15 end
|
35
|
+
# def state_tax; 10 end
|
36
|
+
#
|
37
|
+
# has_price do
|
38
|
+
# item base_price, "base"
|
39
|
+
# group "tax" do
|
40
|
+
# item federal_tax, "federal"
|
41
|
+
# item state_tax, "state"
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# @product = Product.new
|
47
|
+
# @product.price.total # => 125
|
48
|
+
# @product.price.tax.total # => 25
|
49
|
+
#
|
50
|
+
# @param [#to_s] group_name a name for the price group
|
51
|
+
# @yield The yielded block is executed within the group, such that all groups and items
|
52
|
+
# declared within the block appear nested under this group. This behavior is recursive.
|
53
|
+
#
|
54
|
+
# @see #item
|
55
|
+
def group(group_name, &block)
|
56
|
+
group_key = group_name.to_s
|
57
|
+
|
58
|
+
@current_nesting_level[group_key] ||= {}
|
59
|
+
|
60
|
+
if block_given?
|
61
|
+
within_group(group_key) do
|
62
|
+
instance_eval &block
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Delegates all missing methods to the target object.
|
68
|
+
def method_missing(meth, *args, &block)
|
69
|
+
@object.send(meth, *args, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def within_group(group_name)
|
74
|
+
step_into group_name
|
75
|
+
yield
|
76
|
+
step_out
|
77
|
+
end
|
78
|
+
|
79
|
+
def step_into(group_name)
|
80
|
+
@original_nesting_level = @current_nesting_level
|
81
|
+
@current_nesting_level = @current_nesting_level[group_name]
|
82
|
+
end
|
83
|
+
|
84
|
+
def step_out
|
85
|
+
@current_nesting_level = @original_nesting_level
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/has_price.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/has_price/core_extensions/array.rb"
|
2
|
+
require File.dirname(__FILE__) + "/has_price/core_extensions/string.rb"
|
3
|
+
|
4
|
+
unless Array.instance_methods.include? "extract_options!"
|
5
|
+
Array.send :include, HasPrice::CoreExtensions::Array
|
6
|
+
end
|
7
|
+
|
8
|
+
unless String.instance_methods.include? "underscore"
|
9
|
+
String.send :include, HasPrice::CoreExtensions::String
|
10
|
+
end
|
11
|
+
|
12
|
+
require File.dirname(__FILE__) + "/has_price/price.rb"
|
13
|
+
require File.dirname(__FILE__) + "/has_price/price_builder.rb"
|
14
|
+
require File.dirname(__FILE__) + "/has_price/has_price.rb"
|
data/rails/init.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'rr'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
require 'has_price'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
include RR::Adapters::TestUnit
|
12
|
+
include HasPrice
|
13
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestHasPrice < Test::Unit::TestCase
|
4
|
+
context "A Product with HasPrice module" do
|
5
|
+
setup do
|
6
|
+
Product = Class.new
|
7
|
+
Product.extend(HasPrice)
|
8
|
+
end
|
9
|
+
|
10
|
+
context "having price defined as :free" do
|
11
|
+
setup do
|
12
|
+
Product.has_price :free => true
|
13
|
+
@product = Product.new
|
14
|
+
end
|
15
|
+
|
16
|
+
should "gain #price method" do
|
17
|
+
assert Product.instance_methods.include? "price"
|
18
|
+
end
|
19
|
+
|
20
|
+
should "return empty Price object" do
|
21
|
+
assert_equal Price.new, @product.price
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "having price defined using builder with hardcoded values" do
|
26
|
+
setup do
|
27
|
+
Product.has_price do
|
28
|
+
item 100, "base"
|
29
|
+
group "tax" do
|
30
|
+
item 20, "federal"
|
31
|
+
item 10, "state"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@product = Product.new
|
36
|
+
end
|
37
|
+
|
38
|
+
should "gain #price method" do
|
39
|
+
assert Product.instance_methods.include? "price"
|
40
|
+
end
|
41
|
+
|
42
|
+
should "return price object" do
|
43
|
+
assert_equal Price, @product.price.class
|
44
|
+
end
|
45
|
+
|
46
|
+
should "return price equivalent to corresponding hash" do
|
47
|
+
assert_equal({"base" => 100, "tax" => {"federal" => 20, "state" => 10}}, @product.price)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "having price defined using builder with values referencing product methods" do
|
52
|
+
setup do
|
53
|
+
Product.has_price do
|
54
|
+
item base_price, "base"
|
55
|
+
group "tax" do
|
56
|
+
item federal_tax, "federal"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@product = Product.new
|
61
|
+
|
62
|
+
mock(@product).base_price { 100 }
|
63
|
+
mock(@product).federal_tax { 20 }
|
64
|
+
end
|
65
|
+
|
66
|
+
should "return Price object with values correctly set from instance methods" do
|
67
|
+
assert_equal({"base" => 100, "tax" => {"federal" => 20}}, @product.price)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "having price defined on attribute total_price" do
|
72
|
+
setup do
|
73
|
+
Product.has_price :attribute => "total_price", :free => true
|
74
|
+
end
|
75
|
+
|
76
|
+
should "gain #total_price method" do
|
77
|
+
assert Product.instance_methods.include? "total_price"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def teardown
|
83
|
+
self.class.instance_eval { remove_const :Product }
|
84
|
+
end
|
85
|
+
end
|
data/test/test_price.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestPrice < Test::Unit::TestCase
|
4
|
+
context "Price instance" do
|
5
|
+
setup do
|
6
|
+
@price = Price[{ "base" => 100,
|
7
|
+
"tax" => 10,
|
8
|
+
"delivery" => { "shipping" => 20,
|
9
|
+
"expedite" => 5,
|
10
|
+
"discounts" => { "thanksgiving" => -20 }}}]
|
11
|
+
end
|
12
|
+
|
13
|
+
should "calculate total recursively" do
|
14
|
+
assert_equal 115, @price.total
|
15
|
+
end
|
16
|
+
|
17
|
+
should "return total on to_i" do
|
18
|
+
assert_equal 115, @price.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
should "return Hash on to_hash" do
|
22
|
+
assert_equal Hash, @price.to_hash.class
|
23
|
+
end
|
24
|
+
|
25
|
+
should "support equality with the equivalent hash" do
|
26
|
+
assert_equal @price, @price.to_hash
|
27
|
+
end
|
28
|
+
|
29
|
+
should "access hash values with missing methods" do
|
30
|
+
assert_equal 10, @price.tax
|
31
|
+
end
|
32
|
+
|
33
|
+
should "return Price object upon accessing nested group" do
|
34
|
+
assert_equal Price, @price.delivery.class
|
35
|
+
end
|
36
|
+
|
37
|
+
should "return equivalent of sub-hash as nested group" do
|
38
|
+
assert_equal({"shipping" => 20, "expedite" => 5, "discounts" => { "thanksgiving" => -20 }}, @price.delivery)
|
39
|
+
end
|
40
|
+
|
41
|
+
should "return subtotal of a nested group" do
|
42
|
+
assert_equal 5, @price.delivery.total
|
43
|
+
end
|
44
|
+
|
45
|
+
should "support deep chaining for accessing groups" do
|
46
|
+
assert_equal({"thanksgiving" => -20}, @price.delivery.discounts)
|
47
|
+
end
|
48
|
+
|
49
|
+
should "provide subtotal on deep nested group with single element" do
|
50
|
+
assert_equal -20, @price.delivery.discounts.total
|
51
|
+
end
|
52
|
+
|
53
|
+
should "access individual elements in deep nested groups" do
|
54
|
+
assert_equal -20, @price.delivery.discounts.thanksgiving
|
55
|
+
end
|
56
|
+
|
57
|
+
should "raise NoMethodError in case called non-existent item/group" do
|
58
|
+
assert_raise NoMethodError do
|
59
|
+
@price.foo
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestPriceBuilder < Test::Unit::TestCase
|
4
|
+
context "PriceBuilder" do
|
5
|
+
context "instance" do
|
6
|
+
setup do
|
7
|
+
@instance = Object.new
|
8
|
+
@price_builder = PriceBuilder.new(@instance)
|
9
|
+
end
|
10
|
+
|
11
|
+
should "create an item in price hash" do
|
12
|
+
@price_builder.item 500, "base"
|
13
|
+
assert_equal({"base" => 500}, @price_builder.price)
|
14
|
+
end
|
15
|
+
|
16
|
+
should "create an item based on another price" do
|
17
|
+
tax_price = Price[{"federal" => 100, "state" => 200}]
|
18
|
+
@price_builder.item tax_price, "tax"
|
19
|
+
assert_equal({"tax" => {"federal" => 100, "state" => 200}}, @price_builder.price)
|
20
|
+
end
|
21
|
+
|
22
|
+
should "create a group in price hash" do
|
23
|
+
@price_builder.group "taxes"
|
24
|
+
assert_equal({"taxes" => {}}, @price_builder.price)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "create an item in a group in a price hash" do
|
28
|
+
@price_builder.group "taxes" do
|
29
|
+
item 500, "federal tax"
|
30
|
+
end
|
31
|
+
|
32
|
+
assert_equal({"taxes" => {"federal tax" => 500}}, @price_builder.price)
|
33
|
+
end
|
34
|
+
|
35
|
+
should "support complex group structure" do
|
36
|
+
@price_builder.item 500, "base"
|
37
|
+
@price_builder.group "delivery" do
|
38
|
+
item 200, "standard shipping"
|
39
|
+
|
40
|
+
group "discounts" do
|
41
|
+
item -100, "shipping discount"
|
42
|
+
end
|
43
|
+
|
44
|
+
item 100, "expedite"
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal({"base" => 500, "delivery" => { "standard shipping" => 200,
|
48
|
+
"expedite" => 100,
|
49
|
+
"discounts" => { "shipping discount" => -100 }
|
50
|
+
}}, @price_builder.price)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "send all missing methods to the object" do
|
54
|
+
stub(@instance).base_price { 200 }
|
55
|
+
assert_equal 200, @price_builder.base_price
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_price
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maxim Chernyak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-02 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rr
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A convenient DSL for defining complex price reader/serializer in a class and organizing a price breakdown. Price can be declared with items and groups which depend on other attributes. Price is a very simple subclass of Hash. This provides for easy serialization and flexibility in case of implementation changes. This way you can conveniently store the whole price breakdown in your serialized receipts. It also provides magic methods for convenient access, but can be fully treated as a regular Hash with some sprinkles on top.
|
36
|
+
email: max@bitsonnet.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.md
|
44
|
+
files:
|
45
|
+
- .gitignore
|
46
|
+
- LICENSE
|
47
|
+
- README.md
|
48
|
+
- Rakefile
|
49
|
+
- VERSION
|
50
|
+
- has_price.gemspec
|
51
|
+
- lib/has_price.rb
|
52
|
+
- lib/has_price/core_extensions/array.rb
|
53
|
+
- lib/has_price/core_extensions/string.rb
|
54
|
+
- lib/has_price/has_price.rb
|
55
|
+
- lib/has_price/price.rb
|
56
|
+
- lib/has_price/price_builder.rb
|
57
|
+
- rails/init.rb
|
58
|
+
- test/helper.rb
|
59
|
+
- test/test_has_price.rb
|
60
|
+
- test/test_price.rb
|
61
|
+
- test/test_price_builder.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/maxim/has_price
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --charset=UTF-8
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: "0"
|
82
|
+
version:
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.3.5
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: Provides a convenient DSL for organizing a price breakdown in a class.
|
90
|
+
test_files:
|
91
|
+
- test/helper.rb
|
92
|
+
- test/test_has_price.rb
|
93
|
+
- test/test_price.rb
|
94
|
+
- test/test_price_builder.rb
|