This repository has been archived by the owner on Jan 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
532 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
## MAC OS | ||
.DS_Store | ||
|
||
## TEXTMATE | ||
*.tmproj | ||
tmtags | ||
|
||
## EMACS | ||
*~ | ||
\#* | ||
.\#* | ||
|
||
## VIM | ||
*.swp | ||
|
||
## PROJECT::GENERAL | ||
coverage | ||
rdoc | ||
doc | ||
pkg | ||
.yardoc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
require 'rubygems' | ||
require 'rake' | ||
|
||
begin | ||
require 'jeweler' | ||
Jeweler::Tasks.new do |gem| | ||
gem.name = "has_price" | ||
gem.summary = %Q{Provides a convenient DSL for organizing a price breakdown in a class.} | ||
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.} | ||
gem.email = "max@bitsonnet.com" | ||
gem.homepage = "http://github.com/maxim/has_price" | ||
gem.authors = ["Maxim Chernyak"] | ||
gem.add_development_dependency "shoulda", ">= 0" | ||
gem.add_development_dependency "rr" | ||
gem.files.include %w(lib/*/**) | ||
end | ||
Jeweler::GemcutterTasks.new | ||
rescue LoadError | ||
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" | ||
end | ||
|
||
require 'rake/testtask' | ||
Rake::TestTask.new(:test) do |test| | ||
test.libs << 'lib' << 'test' | ||
test.pattern = 'test/**/test_*.rb' | ||
test.verbose = true | ||
end | ||
|
||
begin | ||
require 'rcov/rcovtask' | ||
Rcov::RcovTask.new do |test| | ||
test.libs << 'test' | ||
test.pattern = 'test/**/test_*.rb' | ||
test.verbose = true | ||
end | ||
rescue LoadError | ||
task :rcov do | ||
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" | ||
end | ||
end | ||
|
||
task :test => :check_dependencies | ||
|
||
task :default => :test | ||
|
||
require 'rake/rdoctask' | ||
Rake::RDocTask.new do |rdoc| | ||
version = File.exist?('VERSION') ? File.read('VERSION') : "" | ||
|
||
rdoc.rdoc_dir = 'rdoc' | ||
rdoc.title = "has_price #{version}" | ||
rdoc.rdoc_files.include('README*') | ||
rdoc.rdoc_files.include('lib/**/*.rb') | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
require File.dirname(__FILE__) + "/has_price/core_extensions/array.rb" | ||
require File.dirname(__FILE__) + "/has_price/core_extensions/string.rb" | ||
|
||
unless Array.instance_methods.include? "extract_options!" | ||
Array.send :include, HasPrice::CoreExtensions::Array | ||
end | ||
|
||
unless String.instance_methods.include? "underscore" | ||
String.send :include, HasPrice::CoreExtensions::String | ||
end | ||
|
||
require File.dirname(__FILE__) + "/has_price/price.rb" | ||
require File.dirname(__FILE__) + "/has_price/price_builder.rb" | ||
require File.dirname(__FILE__) + "/has_price/has_price.rb" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module HasPrice | ||
module CoreExtensions | ||
module Array | ||
# In case we're not in Rails. | ||
# | ||
# @see http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Array/ExtractOptions.html#M001202 | ||
def extract_options! | ||
last.is_a?(::Hash) ? pop : {} | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module HasPrice | ||
module CoreExtensions | ||
module String | ||
# In case we're not in Rails. | ||
# | ||
# @see http://api.rubyonrails.org/classes/Inflector.html#M001631 | ||
def underscore | ||
gsub(/::/, '/'). | ||
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). | ||
gsub(/([a-z\d])([A-Z])/,'\1_\2'). | ||
tr("-", "_"). | ||
downcase | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
module HasPrice | ||
module HasPrice | ||
|
||
# Provides a simple DSL to defines price instance method on the receiver. | ||
# | ||
# @param [Hash] options the options for creating price method. | ||
# @option options [Symbol] :attribute (:price) Name of the price method. | ||
# @option options [Boolean] :free (false) Set `:free => true` to use null object pattern. | ||
# | ||
# @yield The yielded block provides method `item` for declaring price entries, | ||
# and method `group` for declaring price groups. | ||
# | ||
# @example Normal usage | ||
# class Product < ActiveRecord::Base | ||
# has_price do | ||
# item base_price, "base" | ||
# item discount, "discount" | ||
# | ||
# group "taxes" do | ||
# item federal_tax, "federal tax" | ||
# item state_tax, "state tax" | ||
# end | ||
# | ||
# group "shipment" do | ||
# # Notice that delivery_method is an instance method. | ||
# # You can call instance methods anywhere in has_price block. | ||
# item delivery_price, delivery_method | ||
# end | ||
# end | ||
# end | ||
# | ||
# @example Null object pattern | ||
# class Product < ActiveRecord::Base | ||
# # Creates method #price which returns empty Price. | ||
# has_price :free => true | ||
# end | ||
# | ||
# @see PriceBuilder#item | ||
# @see PriceBuilder#group | ||
# | ||
def has_price(options = {}, &block) | ||
attribute = options[:attribute] || :price | ||
free = !block_given? && options[:free] | ||
|
||
define_method attribute.to_sym do | ||
builder = PriceBuilder.new self | ||
builder.instance_eval &block unless free | ||
builder.price | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
module HasPrice | ||
class Price < Hash | ||
# @return [Fixnum] the recursive sum of all declared prices. | ||
def to_i; recursive_sum end | ||
# @return [String] the output of to_i converted to string. | ||
def to_s; to_i.to_s end | ||
# @return [Hash] the price as a Hash object. | ||
def to_hash; Hash[self] end | ||
alias total to_i | ||
|
||
# Provides access to price items and groups using magic methods and chaining. | ||
# | ||
# @example | ||
# class Product | ||
# has_price do | ||
# item 400, "base" | ||
# group "tax" do | ||
# item 100, "federal" | ||
# item 50, "state" | ||
# end | ||
# end | ||
# end | ||
# | ||
# product = Product.new | ||
# product.price # => Full Price object | ||
# product.price.base # => 400 | ||
# product.price.tax # => Price object on group tax | ||
# product.price.tax.federal # => 100 | ||
# product.price.tax.total # => 150 | ||
# | ||
# @return [Price, Fixnum] Price object if method matches a group, Fixnum if method matches an item. | ||
def method_missing(meth, *args, &blk) | ||
value = select{|k,v| k.underscore == meth.to_s}.first | ||
|
||
if !value | ||
super | ||
elsif value.last.is_a?(Hash) | ||
self.class[value.last] | ||
else | ||
value.last | ||
end | ||
end | ||
|
||
private | ||
def recursive_sum(target = self) | ||
target.inject(0) do |sum, pair| | ||
value = pair.last | ||
sum += value.is_a?(Hash) ? recursive_sum(value) : value | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
module HasPrice | ||
class PriceBuilder | ||
attr_reader :price | ||
|
||
# Creates PriceBuilder on a target object. | ||
# | ||
# @param [Object] object the target object on which price is being built. | ||
def initialize(object) | ||
@price = Price.new | ||
@current_nesting_level = @price | ||
@object = object | ||
end | ||
|
||
# Adds price item to the current nesting level of price definition. | ||
# | ||
# @param [#to_hash, #to_i] price an integer representing amount for this price item. | ||
# Alternatively, anything that responds to #to_hash can be used, | ||
# and will be treated as a group named with item_name. | ||
# @param [#to_s] item_name name for the provided price item or group. | ||
# | ||
# @see #group | ||
def item(price, item_name) | ||
@current_nesting_level[item_name.to_s] = price.respond_to?(:to_hash) ? price.to_hash : price.to_i | ||
end | ||
|
||
# Adds price group to the current nesting level of price definition. | ||
# Groups are useful for price breakdown categorization and easy subtotal values. | ||
# | ||
# @example Using group subtotals | ||
# class Product | ||
# include HasPrice | ||
# | ||
# def base_price; 100 end | ||
# def federal_tax; 15 end | ||
# def state_tax; 10 end | ||
# | ||
# has_price do | ||
# item base_price, "base" | ||
# group "tax" do | ||
# item federal_tax, "federal" | ||
# item state_tax, "state" | ||
# end | ||
# end | ||
# end | ||
# | ||
# @product = Product.new | ||
# @product.price.total # => 125 | ||
# @product.price.tax.total # => 25 | ||
# | ||
# @param [#to_s] group_name a name for the price group | ||
# @yield The yielded block is executed within the group, such that all groups and items | ||
# declared within the block appear nested under this group. This behavior is recursive. | ||
# | ||
# @see #item | ||
def group(group_name, &block) | ||
group_key = group_name.to_s | ||
|
||
@current_nesting_level[group_key] ||= {} | ||
|
||
if block_given? | ||
within_group(group_key) do | ||
instance_eval &block | ||
end | ||
end | ||
end | ||
|
||
# Delegates all missing methods to the target object. | ||
def method_missing(meth, *args, &block) | ||
@object.send(meth, *args, &block) | ||
end | ||
|
||
private | ||
def within_group(group_name) | ||
step_into group_name | ||
yield | ||
step_out | ||
end | ||
|
||
def step_into(group_name) | ||
@original_nesting_level = @current_nesting_level | ||
@current_nesting_level = @current_nesting_level[group_name] | ||
end | ||
|
||
def step_out | ||
@current_nesting_level = @original_nesting_level | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require 'has_price' | ||
|
||
ActiveRecord::Base.extend HasPrice::HasPrice |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
require 'rubygems' | ||
require 'test/unit' | ||
require 'shoulda' | ||
require 'rr' | ||
|
||
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) | ||
$LOAD_PATH.unshift(File.dirname(__FILE__)) | ||
require 'has_price' | ||
|
||
class Test::Unit::TestCase | ||
include RR::Adapters::TestUnit | ||
include HasPrice | ||
end |
Oops, something went wrong.