dpickett-ramazon_advertising 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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ features/support/ramazon_advertising.yml
7
+ features/support/root_nodes.yml
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Dan Pickett
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.rdoc ADDED
@@ -0,0 +1,44 @@
1
+ = ramazon_advertising
2
+
3
+ Ruby + Amazon Advertising == Car RAmazon_Advertising - Say Car RAmazon_Advertising!
4
+
5
+ Ruh roh' Rorge it's an object oriented approach to Amazon's overly complicated product api that has a new name every 2 weeks.
6
+
7
+ Complete with the ability to use nokogiri selectors for results
8
+
9
+ Currently only supports product search and retrieval. Requests are signed properly. More soon!
10
+
11
+ Ramazon::Configuration.access_key = "Your Access Key"
12
+ Ramazon::Configuration.secret_key = "Your Secret Key"
13
+
14
+ @products = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")
15
+ @products[0].title
16
+ @products[0].asin
17
+ @products[0].upc
18
+ @products[0].large_image.url
19
+ @products[0].url
20
+
21
+ #you can also use a nokogiri search string to get elements that don't have built-in accessors
22
+ @products[0].get("ItemAttributes Actor").collect{|a| a.content}
23
+
24
+ == What's under the hood?
25
+
26
+ * HTTParty
27
+ * HappyMapper
28
+ * will_paginate
29
+ * nokogiri
30
+
31
+ == Note on Patches/Pull Requests
32
+
33
+ * Fork the project.
34
+ * Make your feature addition or bug fix.
35
+ * Add tests for it. This is important so I don't break it in a
36
+ future version unintentionally.
37
+ * Commit, do not mess with rakefile, version, or history.
38
+ (if you want to have your own version, that is fine but
39
+ bump version in a commit by itself I can ignore when I pull)
40
+ * Send me a pull request. Bonus points for topic branches.
41
+
42
+ == Copyright
43
+
44
+ Copyright (c) 2009 Dan Pickett. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,85 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'lib/ramazon_advertising'
5
+ load File.join(File.dirname(__FILE__), 'lib', 'tasks', 'ramazon.rake')
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "ramazon_advertising"
11
+ gem.summary = %Q{TODO: one-line summary of your gem}
12
+ gem.description = %Q{TODO: longer description of your gem}
13
+ gem.email = "dpickett@enlightsolutions.com"
14
+ gem.homepage = "http://github.com/dpickett/ramazon_advertising"
15
+ gem.authors = ["Dan Pickett"]
16
+ gem.add_dependency("jnunemaker-httparty", ">= 0.4.3")
17
+ gem.add_dependency("jnunemaker-happymapper", ">= 0.2.5")
18
+ gem.add_dependency("mislav-will_paginate", ">= 2.3.11")
19
+ gem.add_dependency("nokogiri", ">= 1.3.3")
20
+ gem.add_dependency("configatron", ">= 2.4.1")
21
+ gem.add_dependency("rails", ">= 2.3.3")
22
+ gem.add_dependency("ruby-hmac", ">= 0.3.2")
23
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
24
+ end
25
+
26
+ rescue LoadError
27
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
28
+ end
29
+
30
+ require 'spec/rake/spectask'
31
+ Spec::Rake::SpecTask.new(:spec) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.spec_files = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
37
+ spec.libs << 'lib' << 'spec'
38
+ spec.pattern = 'spec/**/*_spec.rb'
39
+ spec.rcov = true
40
+ end
41
+
42
+ task :default => :spec
43
+
44
+ begin
45
+ require 'cucumber/rake/task'
46
+ Cucumber::Rake::Task.new(:cucumber)
47
+ rescue LoadError
48
+ task :cucumber do
49
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
50
+ end
51
+ end
52
+
53
+ task :default => :test
54
+
55
+ require 'rake/rdoctask'
56
+ Rake::RDocTask.new do |rdoc|
57
+ if File.exist?('VERSION.yml')
58
+ config = YAML.load(File.read('VERSION.yml'))
59
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
60
+ else
61
+ version = ""
62
+ end
63
+
64
+ rdoc.rdoc_dir = 'rdoc'
65
+ rdoc.title = "ramazon_advertising #{version}"
66
+ rdoc.rdoc_files.include('README*')
67
+ rdoc.rdoc_files.include('lib/**/*.rb')
68
+ end
69
+
70
+ begin
71
+ require "YARD"
72
+ if File.exist?('VERSION.yml')
73
+ config = YAML.load(File.read('VERSION.yml'))
74
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
75
+ else
76
+ version = ""
77
+ end
78
+
79
+ YARD::Rake::YardocTask.new do |t|
80
+ end
81
+ rescue LoadError
82
+
83
+ end
84
+
85
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,15 @@
1
+ @errors
2
+ Feature: Friendly errors
3
+ As a user of ramazon_advertising
4
+ I want to get friendly error messages
5
+ So that I know I did something wrong
6
+
7
+ Background:
8
+ Given I have a valid access key
9
+ And I have a valid secret key
10
+
11
+ Scenario: I don't specify a keyword for search
12
+ When I perform the product search
13
+ Then I should get an error
14
+ And the error should have a "code" of "AWS.MinimumParameterRequirement"
15
+ And the error should have a "message"
@@ -0,0 +1,12 @@
1
+ Feature: As a user of the Ramazon Advertising API
2
+ I want a list of browse nodes
3
+ So that I have starting points for browse node traversal
4
+
5
+ Scenario: Retrieving browse nodes
6
+ Given I want browse nodes to be stored in a temporary file
7
+ And the browse node temporary file doesn't exist
8
+ When I retrieve root nodes
9
+ Then I should get a temporary file for root nodes
10
+ And I should have a "Books" root node
11
+ And I should have a "Grocery" root node
12
+ And I should have a list of root nodes
@@ -0,0 +1,23 @@
1
+ Feature: Retrieving a product
2
+ As a user of the Ramazon_advertising_api
3
+ I want to get specific information
4
+ In order to leverage the data provided by the api
5
+
6
+ Background:
7
+ Given I have a valid access key
8
+ And I have a valid secret key
9
+
10
+ When I try to find the asin "B000NU2CY4"
11
+ Then I should get a product
12
+ And the product should have the "title" "Gladiator [Blu-ray]"
13
+ And the product should have a "manufacturer"
14
+ And the product should have a "product_group"
15
+ And the product should have a "sales_rank"
16
+ And the product should have a "large_image"
17
+ And the product should have a "list_price"
18
+ And the product should have a "upc"
19
+ And the product should have a "lowest_new_price"
20
+ And the product should have a "new_count"
21
+ And the product should have a "used_count"
22
+
23
+
@@ -0,0 +1,15 @@
1
+ Feature: Search for products
2
+ As a user of the Ramazon_advertising api
3
+ I want to perform a search
4
+ In order to find products my customers will want to purchase
5
+
6
+ Background:
7
+ Given I have a valid access key
8
+ And I have a valid secret key
9
+
10
+ Given I am searching with the "search_index" of "DVD"
11
+ And I am searching with the "browse_node" of "130"
12
+ When I perform the product search
13
+ Then I should get a list of products
14
+ And the list of products should have more than 1 product
15
+ And each product should have the "product_group" "DVD"
@@ -0,0 +1,8 @@
1
+ Given /^I have a valid access key$/ do
2
+ configatron.ramazon.access_key.should_not be_nil
3
+ end
4
+
5
+ Given /^I have a valid secret key$/ do
6
+ configatron.ramazon.secret_key.should_not be_nil
7
+ end
8
+
@@ -0,0 +1,36 @@
1
+ Given /^I want browse nodes to be stored in a temporary file$/ do
2
+ @browse_root_filename = File.join(File.dirname(__FILE__), '..', 'support', 'root_nodes.yml')
3
+ end
4
+
5
+ Given /^the browse node temporary file doesn't exist$/ do
6
+ FileUtils.rm_f @browse_root_filename
7
+ end
8
+
9
+ When /^I retrieve root nodes$/ do
10
+ if @browse_root_filename
11
+ Ramazon::BrowseNode.generate_root_nodes @browse_root_filename
12
+ else
13
+ Ramazon::BrowseNode.generate_root_nodes
14
+ end
15
+ end
16
+
17
+ Then /^I should get a temporary file for root nodes$/ do
18
+ FileTest.exists?(@browse_root_filename).should be_true
19
+ end
20
+
21
+ Then /^I should have a "([^\"]*)" root node$/ do |name|
22
+ get_root_nodes[name].should_not be_nil
23
+ end
24
+
25
+ Then /^I should have a list of root nodes$/ do
26
+ get_root_nodes.should_not be_empty
27
+ end
28
+
29
+ def get_root_nodes
30
+ if @browse_root_filename
31
+ nodes = Ramazon::BrowseNode.root_nodes(@browse_root_filename)
32
+ else
33
+ nodes = Ramazon::BrowseNode.root_nodes
34
+ end
35
+
36
+ end
@@ -0,0 +1,14 @@
1
+ Then /^I should get an error$/ do
2
+ @error.should_not be_nil
3
+ end
4
+
5
+ Then /^the error should have a "([^\"]*)" of "([^\"]*)"$/ do |attr, value|
6
+ @error.send(attr).should eql(value)
7
+ end
8
+
9
+ Then /^the error should have a "([^\"]*)"$/ do |attr|
10
+ @error.send(attr).should_not be_nil
11
+ end
12
+
13
+
14
+
@@ -0,0 +1,48 @@
1
+ When /^I try to find the asin "([^\"]*)"$/ do |asin|
2
+ @products = Ramazon::Product.find(:item_id => asin, :response_group => "Medium")
3
+ @product = @products[0]
4
+ end
5
+
6
+ Given /^I am searching with the "([^\"]*)" of "([^\"]*)"$/ do |attr, value|
7
+ @search_options ||= {}
8
+ @search_options[attr] = value
9
+ end
10
+
11
+ When /^I perform the product search$/ do
12
+ begin
13
+ @search_options ||= {}
14
+ @products = Ramazon::Product.find(@search_options)
15
+ rescue Ramazon::Error => e
16
+ @error = e
17
+ end
18
+ end
19
+
20
+ Then /^I should get a list of products$/ do
21
+ raise @error if @error
22
+ @products.should_not be_empty
23
+ end
24
+
25
+ Then /^the list of products should have more than (\d+) product$/ do |count|
26
+ @products.should have_at_least(count.to_i + 1).items
27
+ end
28
+
29
+
30
+ Then /^each product should have the "([^\"]*)" "([^\"]*)"$/ do |attr, value|
31
+ @products.each do |p|
32
+ p.send(attr).should eql(value)
33
+ end
34
+ end
35
+
36
+ Then /^I should get a product$/ do
37
+ raise @error if @error
38
+ @product.should_not be_nil
39
+ end
40
+
41
+ Then /^the product should have the "([^\"]*)" "([^\"]*)"$/ do |attr, value|
42
+ @product.send(attr).should eql(value)
43
+ end
44
+
45
+ Then /^the product should have a "([^\"]*)"$/ do |attr|
46
+ @product.send(attr).should_not be_nil
47
+ end
48
+
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__) + '..', '..', 'lib'))
2
+ require 'lib/ramazon_advertising'
3
+
4
+ require 'spec/expectations'
5
+
6
+ configatron.ramazon.configure_from_yaml(File.join(File.dirname(__FILE__), "ramazon_advertising.yml"))
7
+
@@ -0,0 +1,2 @@
1
+ access_key: SOMETHING
2
+ secret_key: SOMETHING SECRET
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ module AbstractElement
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def abstract_element(name, type, options = {})
9
+ element(name, type, options.merge(:parser => :abstract_parse, :raw => true))
10
+ end
11
+
12
+ def abstract_parse(xml, options = {})
13
+ tag XML::Parser.string(xml).parse.root.name
14
+ parse(xml)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module Ramazon
2
+ class BrowseNode
3
+ include HTTParty
4
+
5
+ DEFAULT_ROOT_FILE = File.join(File.dirname(__FILE__), '..', 'root_nodes.yml')
6
+ def self.generate_root_nodes(file_name = DEFAULT_ROOT_FILE)
7
+ if Ramazon::Configuration.locale == :us
8
+ doc = Nokogiri::HTML(get('http://www.amazon.com').body)
9
+ root_nodes = {}
10
+ doc.search(".navSaMenu .navSaChildItem a").each do |element|
11
+ if element["href"] =~ /node=(\d+)\&/
12
+ root_nodes[element.content] = $1
13
+ end
14
+ end
15
+
16
+ unless root_nodes.empty?
17
+ FileUtils.rm_f(file_name)
18
+ File.open(file_name, 'w') do |f|
19
+ f.write(root_nodes.to_yaml)
20
+ end
21
+ end
22
+ else
23
+ #todo correlate ECS locales to actual amazon.* urls
24
+ raise "generating root nodes for locale's other than the US is not supported"
25
+ end
26
+ end
27
+
28
+ def self.root_nodes(file_name = DEFAULT_ROOT_FILE)
29
+ @root_nodes ||= File.open(file_name) { |yf| YAML::load(yf) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,66 @@
1
+ module Ramazon
2
+ # the class that persists configuration information for Amazon requests
3
+ class Configuration
4
+ class << self
5
+ LOCALES = [:ca, :de, :fr, :jp, :uk, :us]
6
+
7
+ # set the locale for future requests
8
+ # will raise an exception if the locale isn't in the Configuration::LOCALES collection
9
+ def locale=(locale)
10
+ if LOCALES.include?(locale)
11
+ configatron.ramazon.locale = locale
12
+ else
13
+ raise "unknown locale"
14
+ end
15
+ end
16
+
17
+ # get the current locale (defaults to the us)
18
+ def locale
19
+ configatron.ramazon.locale || :us
20
+ end
21
+
22
+ # get the current access key
23
+ def access_key
24
+ configatron.ramazon.access_key
25
+ end
26
+
27
+ # set the current access key
28
+ # @param key [String] access key you're using to access the advertising api
29
+ def access_key=(key)
30
+ configatron.ramazon.access_key = key
31
+ end
32
+
33
+ # get the current secret key that is used for request signing
34
+ def secret_key
35
+ configatron.ramazon.secret_key
36
+ end
37
+
38
+ # set the secret key so that requests can be appropriately signed
39
+ # @param key [String] secret key you're using to sign advertising requests
40
+ def secret_key=(key)
41
+ configatron.ramazon.secret_key = key
42
+ end
43
+
44
+ # get the correct host based on your locale
45
+ def base_uri
46
+ if locale == :us
47
+ "http://ecs.amazonaws.com"
48
+ else
49
+ "http://ecs.amazonaws.#{locale}"
50
+ end
51
+ end
52
+
53
+ # get the full path including locale specific host and /onca/xml path
54
+ def uri
55
+ "#{base_uri}#{path}"
56
+ end
57
+
58
+ # get the path where requests should be dispatched to
59
+ def path
60
+ "/onca/xml"
61
+ end
62
+
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,12 @@
1
+ module Ramazon
2
+ class Error < StandardError
3
+ include HappyMapper
4
+ tag "Error"
5
+ element :code, String, :tag => "Code"
6
+ element :message, String, :tag => "Message"
7
+
8
+ def to_s
9
+ "#{self.code}: #{self.message}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ # An image object returned by Amazon's product data
3
+ # Available accessors include
4
+ # +url+::
5
+ # The url of the image
6
+ # +height+::
7
+ # The height of the image
8
+ # +width+::
9
+ # The width of the image
10
+ class Image
11
+ include HappyMapper
12
+ include Ramazon::AbstractElement
13
+ tag "Image"
14
+ element :url, String, :tag => "URL"
15
+ element :height, Integer, :tag => "Height"
16
+ element :width, Integer, :tag => "Width"
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ # A price object returned by Amazon's product data
3
+ # Available accessors include
4
+ # +amount+::
5
+ # The unformatted ammount of the price
6
+ # +currency_code+::
7
+ # The currency of the offer
8
+ # +formatted_price+::
9
+ # The formatted price of the offer
10
+ class Price
11
+ include HappyMapper
12
+ include Ramazon::AbstractElement
13
+
14
+ element :amount, String, :tag => "Amount"
15
+ element :currency_code, String, :tag => "CurrencyCode"
16
+ element :formatted_price, String, :tag => "FormattedPrice"
17
+ end
18
+ end
@@ -0,0 +1,144 @@
1
+ module Ramazon
2
+ # Find and get product details with this class
3
+ # Currently supports the following accessors
4
+ # (all other elements can be accessed via nokogiri selectors and the get method)
5
+ # +asin+::
6
+ # Amazon Identifier
7
+ # +upc+::
8
+ # UPC ID
9
+ # +title+::
10
+ # Title of the product
11
+ # +product_group+::
12
+ # The category/product_group of the product
13
+ # +manufacturer+::
14
+ # The manufacturer of the product
15
+ # +brand+::
16
+ # The brand of the product
17
+ # +url+::
18
+ # The Amazon URL of the product
19
+ # +small_image+::
20
+ # The small image that Amazon provides (NOTE: Returns Ramazon::Image object)
21
+ # +medium_image+::
22
+ # The medium image that Amazon provides (NOTE: Returns Ramazon::Image object)
23
+ # +large_image+::
24
+ # The large image that Amazon provides (NOTE: Returns Ramazon::Image object)
25
+ # +list_price+::
26
+ # The list price of the item (NOTE: Returns Ramazon::Price object)
27
+ # +lowest_new_price+::
28
+ # The lowest new price from the offer summary (NOTE: Returns Ramazon::Price object)
29
+ # +sales_rank+::
30
+ # The sales rank of the product
31
+ # +new_count+::
32
+ # The quantity of new item offers
33
+ # +used_count+::
34
+ # The quantity of used item offers
35
+ # +collectible_count+::
36
+ # The quantity of collectible item offers
37
+ # +refurbished_count+::
38
+ # The quantity of refurbished item offers
39
+ # +release_date+::
40
+ # The release date of the product
41
+ # +original_release_date+::
42
+ # The original release date of the product
43
+ # @example find an individual item
44
+ # @products = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")
45
+ # @products[0].title
46
+ # @products[0].asin
47
+ # @products[0].upc
48
+ # @products[0].large_image.url
49
+ # @products[0].url
50
+ #
51
+
52
+ class Product
53
+ include HappyMapper
54
+ include Ramazon::AbstractElement
55
+ tag 'Item'
56
+
57
+ element :asin, String, :tag => 'ASIN'
58
+ element :upc, String, :tag => 'ItemAttributes/UPC'
59
+ element :title, String, :tag => 'Title', :deep => true
60
+ element :product_group, String, :tag => 'ItemAttributes/ProductGroup'
61
+ element :manufacturer, String, :tag => 'ItemAttributes/Manufacturer'
62
+ element :brand, String, :tag => 'ItemAttributes/Brand'
63
+ element :url, String, :tag => 'DetailPageURL'
64
+ abstract_element :small_image, Ramazon::Image, :tag => "SmallImage"
65
+ abstract_element :large_image, Ramazon::Image, :tag => "LargeImage"
66
+ abstract_element :medium_image, Ramazon::Image, :tag => "MediumImage"
67
+ abstract_element :tiny_image, Ramazon::Image, :tag => "TinyImage"
68
+ abstract_element :thumb_image, Ramazon::Image, :tag => "ThumbImage"
69
+ abstract_element :list_price, Ramazon::Price, :tag => "ItemAttributes/ListPrice"
70
+ abstract_element :lowest_new_price, Ramazon::Price, :tag => "OfferSummary/LowestNewPrice"
71
+
72
+ element :sales_rank, Integer, :tag => "SalesRank"
73
+
74
+ element :new_count, Integer, :tag => "OfferSummary/TotalNew"
75
+ element :used_count, Integer, :tag => "OfferSummary/TotalUsed"
76
+ element :collectible_count, Integer, :tag => "OfferSummary/TotalCollectible"
77
+ element :refurbished_count, Integer, :tag => "OfferSummary/TotalRefurbished"
78
+
79
+ element :release_date, Date, :tag => 'ItemAttributes/ReleaseDate'
80
+ element :original_release_date, Date, :tag => 'ItemAttributes/OriginalReleaseDate'
81
+
82
+
83
+ # Creates the worker that performs the delta indexing
84
+ # @param options Amazon request options (you can use an underscore convention)
85
+ # (ie. passing the :response_group option will be converted to "ResponseGroup")
86
+ # <tt>:item_id</tt> - the ASIN or UPC you're looking for
87
+ # @return [Array] array of Ramazon::Product objects
88
+ def self.find(*args)
89
+ options = args.extract_options!
90
+ if options[:item_id]
91
+ item_lookup(options[:item_id], options)
92
+ else
93
+ options[:operation] ||= "ItemSearch"
94
+ options[:search_index] ||= "Blended"
95
+ options[:item_page] ||= 1
96
+ res = Ramazon::Request.new(options).submit
97
+ Ramazon::ProductCollection.create_from_results(options[:item_page] || 1, 10, res)
98
+ end
99
+ end
100
+
101
+ # Performs an item lookup
102
+ # @param item_id the ASIN or UPC you're looking for
103
+ # @options additional Amazon request options (i.e. :response_group)
104
+ def self.item_lookup(item_id, options = {})
105
+ req = Ramazon::Request.new({:item_id => item_id,
106
+ :operation => "ItemLookup"}.merge(options))
107
+ res = req.submit
108
+
109
+ Ramazon::ProductCollection.create_from_results(1,1,res)
110
+ end
111
+
112
+ # assembles the available images for the object
113
+ # @return [Hash] hash of symbolized image_name => Ramazon::Image pairs
114
+ def images
115
+ if !@images
116
+ @images = {}
117
+ @images[:thumb] = self.thumb_image if self.thumb_image
118
+ @images[:tiny_image] = self.tiny_image if self.tiny_image
119
+ @images[:small] = self.small_image if self.small_image
120
+ @images[:medium] = self.medium_image if self.medium_image
121
+ @images[:large] = self.large_image if self.large_image
122
+ end
123
+ end
124
+
125
+ attr_accessor :xml_doc
126
+ def self.parse(xml, options = {})
127
+ node = XML::Parser.string(xml.to_s).parse.root
128
+ node.find("//Item").collect do |n|
129
+ p = super(n.to_s)
130
+ p.xml_doc = Nokogiri::XML.parse(n.to_s)
131
+ p
132
+ end
133
+ end
134
+
135
+ # perform a nokogiri search on the product's XML
136
+ # @param args Passes directly to a Nokogiri::Xml.parse(xml).search method
137
+ # @example find the actor
138
+ # @product = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")[0]
139
+ # @product.get("ItemAttributes Actor").collect{|a| a.content}
140
+ def get(*args)
141
+ result = @xml_doc.search(args)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,27 @@
1
+ module Ramazon
2
+ class ProductCollection < WillPaginate::Collection
3
+ include HappyMapper
4
+
5
+ def self.create_from_results(page, per_page, body)
6
+ results = Items.parse(body, {})[0]
7
+ col = create(page, 10, results.total_results || 0) do |pager|
8
+ pager.replace(results.products)
9
+ end
10
+
11
+ col
12
+ end
13
+
14
+ end
15
+
16
+ class Items
17
+ include HappyMapper
18
+ tag 'Items'
19
+
20
+ element :total_results, Integer, :tag => 'TotalResults'
21
+ element :total_pages, Integer, :tag => 'TotalPages'
22
+ has_many :products,
23
+ Ramazon::Product,
24
+ :tag => "Item",
25
+ :raw => true
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ #this code courtesy of Pat Allan and Rails Core
2
+ module Ramazon
3
+ module HashExcept
4
+ # Returns a new hash without the given keys.
5
+ def except(*keys)
6
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
7
+ reject { |key,| rejected.include?(key) }
8
+ end
9
+
10
+ # Replaces the hash without only the given keys.
11
+ def except!(*keys)
12
+ replace(except(*keys))
13
+ end
14
+ end
15
+ end
16
+
17
+ Hash.send(
18
+ :include, Ramazon::HashExcept
19
+ ) unless Hash.instance_methods.include?("except")
20
+
21
+ module Ramazon
22
+ module ArrayExtractOptions
23
+ def extract_options!
24
+ last.is_a?(::Hash) ? pop : {}
25
+ end
26
+ end
27
+ end
28
+
29
+ Array.send(
30
+ :include, Ramazon::ArrayExtractOptions
31
+ ) unless Array.instance_methods.include?("extract_options!")
32
+
33
+ module Ramazon
34
+ module ClassAttributeMethods
35
+ def cattr_reader(*syms)
36
+ syms.flatten.each do |sym|
37
+ next if sym.is_a?(Hash)
38
+ class_eval(<<-EOS, __FILE__, __LINE__)
39
+ unless defined? @@#{sym}
40
+ @@#{sym} = nil
41
+ end
42
+
43
+ def self.#{sym}
44
+ @@#{sym}
45
+ end
46
+
47
+ def #{sym}
48
+ @@#{sym}
49
+ end
50
+ EOS
51
+ end
52
+ end
53
+
54
+ def cattr_writer(*syms)
55
+ options = syms.extract_options!
56
+ syms.flatten.each do |sym|
57
+ class_eval(<<-EOS, __FILE__, __LINE__)
58
+ unless defined? @@#{sym}
59
+ @@#{sym} = nil
60
+ end
61
+
62
+ def self.#{sym}=(obj)
63
+ @@#{sym} = obj
64
+ end
65
+
66
+ #{"
67
+ def #{sym}=(obj)
68
+ @@#{sym} = obj
69
+ end
70
+ " unless options[:instance_writer] == false }
71
+ EOS
72
+ end
73
+ end
74
+
75
+ def cattr_accessor(*syms)
76
+ cattr_reader(*syms)
77
+ cattr_writer(*syms)
78
+ end
79
+ end
80
+ end
81
+
82
+ Class.extend(
83
+ Ramazon::ClassAttributeMethods
84
+ ) unless Class.respond_to?(:cattr_reader)
85
+
86
+ module Ramazon
87
+ module MetaClass
88
+ def metaclass
89
+ class << self
90
+ self
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ unless Object.new.respond_to?(:metaclass)
97
+ Object.send(:include, Ramazon::MetaClass)
98
+ end
99
+
@@ -0,0 +1,82 @@
1
+ module Ramazon
2
+ #the object used to prepare and submit requests to amazon
3
+ class Request
4
+ include HTTParty
5
+ attr_accessor :options
6
+
7
+ def initialize(options = {})
8
+ self.options = options.merge(default_options)
9
+ end
10
+
11
+ def submit
12
+ @response = self.class.get(signed_url)
13
+ if valid?
14
+ @response.body
15
+ else
16
+ raise self.errors[0], "The following response errors were returned: #{self.errors.collect{|e| e.to_s}}"
17
+ end
18
+ end
19
+
20
+ # Error checking for responses (will return true if errors are present)
21
+ # @returns [boolean] whether the response yielded errors
22
+ def valid?
23
+ @response && errors.empty?
24
+ end
25
+
26
+ #errors returned from amazon
27
+ def errors
28
+ @errors ||= Ramazon::Error.parse(@response.body) || []
29
+ end
30
+
31
+ #use this if any type of caching is desired (TBD)
32
+ def unsigned_path
33
+ qs = ""
34
+
35
+ sorted_params.each do |key, value|
36
+ qs << "&#{key}=#{URI.encode(value.to_s)}"
37
+ end
38
+
39
+ qs.size > 0 ? qs[1..qs.size] : qs
40
+ end
41
+
42
+ def signed_url
43
+ self.options[:timestamp] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S")
44
+ encoded_param_strings = []
45
+ sorted_params.each do |p|
46
+ encoded_param_strings << "#{p[0]}=#{CGI::escape(p[1].to_s)}"
47
+ end
48
+ string_to_sign =
49
+ "GET
50
+ #{Ramazon::Configuration.base_uri.gsub("http://", "")}
51
+ #{Ramazon::Configuration.path}
52
+ #{encoded_param_strings.join("&")}"
53
+
54
+ signature = Ramazon::Signatory.sign(string_to_sign)
55
+ encoded_param_strings << "Signature=" + signature
56
+
57
+ "#{Ramazon::Configuration.uri}?#{encoded_param_strings.join("&")}"
58
+ end
59
+
60
+ def sorted_params
61
+ params.sort {|a, b| a <=> b}
62
+ end
63
+
64
+ def params
65
+ formatted_params = {}
66
+ self.options.each do |key, value|
67
+ formatted_params[key.to_s.classify] = value.is_a?(Array) ? value.join(",") : value
68
+ end
69
+
70
+ formatted_params
71
+ end
72
+
73
+ private
74
+ def default_options
75
+ {
76
+ :service => "AWSECommerceService",
77
+ :version => "2009-07-01",
78
+ :AWSAccessKeyId => Ramazon::Configuration.access_key
79
+ }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,10 @@
1
+ module Ramazon
2
+ class Signatory
3
+ def self.sign(string_to_sign)
4
+ sha1 = HMAC::SHA256.digest(Ramazon::Configuration.secret_key, string_to_sign)
5
+
6
+ #Base64 encoding adds a linefeed to the end of the string so chop the last character!
7
+ CGI.escape(Base64.encode64(sha1).chomp)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,34 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ require "rubygems"
4
+
5
+ require "fileutils"
6
+ require "base64"
7
+ require "hmac-sha2"
8
+ require "digest/sha1"
9
+ require "digest/sha2"
10
+ require "cgi"
11
+
12
+ require "will_paginate"
13
+ require "httparty"
14
+ require "happymapper"
15
+ require "nokogiri"
16
+ require "configatron"
17
+ require "active_support/inflector"
18
+
19
+ require "ramazon/rails_additions"
20
+
21
+ require "ramazon/abstract_element"
22
+
23
+ require "ramazon/signatory"
24
+ require "ramazon/configuration"
25
+ require "ramazon/error"
26
+ require "ramazon/request"
27
+
28
+ require "ramazon/image"
29
+ require "ramazon/price"
30
+ require "ramazon/product"
31
+ require "ramazon/product_collection"
32
+ require "ramazon/browse_node"
33
+
34
+ configatron.ramazon.locale = :us
@@ -0,0 +1,64 @@
1
+ ---
2
+ Apparel (Kids & Baby): "1040662"
3
+ Baby: "165796011"
4
+ Gourmet Food: "3370831"
5
+ Home Appliances: "361395011"
6
+ Camera & Photo: "502394"
7
+ All Sports & Outdoors: "3375251"
8
+ Health & Personal Care: "3760901"
9
+ Cell Phones & Service: "301185"
10
+ Home Audio & Theater: "667846011"
11
+ Power & Hand Tools: "328182011"
12
+ Golf: "3410851"
13
+ Team Sports: "706809011"
14
+ Action Sports: "706812011"
15
+ Vacuums & Storage: "510080"
16
+ Office Products & Supplies: "1064954"
17
+ Magazines: "1263069011"
18
+ Industrial & Scientific: "16310091"
19
+ Jewelry: "3367581"
20
+ Pet Supplies: "12923371"
21
+ Home Improvement: "228013"
22
+ Bedding & Bath: "1057792"
23
+ TV & Video: "1266092011"
24
+ Computers & Accessories: "541966"
25
+ Books: "4"
26
+ Motorcycle & ATV: "346333011"
27
+ Outdoor Power Equipment: "551242"
28
+ Outdoor Recreation: "706814011"
29
+ Natural & Organic: "51537011"
30
+ "Furniture & D\xC3\xA9cor": "1057794"
31
+ Newspapers: "1263068011"
32
+ MP3 & Media Players: "172630"
33
+ Accessories: "1268192011"
34
+ MP3 Downloads: "163856011"
35
+ Kindle Books: "1286228011"
36
+ Toys & Games: "165793011"
37
+ Beauty: "3760911"
38
+ Grocery: "16310101"
39
+ Blogs: "401358011"
40
+ Video Games: "471306"
41
+ Cycling: "3403201"
42
+ Watches: "377110011"
43
+ Movies: "163414"
44
+ Sewing, Craft & Hobby: "12890711"
45
+ Software: "229548"
46
+ Kindle Store: "133141011"
47
+ Game Downloads: "979455011"
48
+ Video On Demand: "16261631"
49
+ Movies & TV: "130"
50
+ Textbooks: "465600"
51
+ Shoes: "672123011"
52
+ Apparel & Accessories: "1036592"
53
+ Kitchen & Dining: "284507"
54
+ Amazon Shorts: "13993911"
55
+ Blu-ray: "193640011"
56
+ Car Electronics & GPS: "1077068"
57
+ PC Games: "229575"
58
+ Computer Components: "193870011"
59
+ Music: "173425"
60
+ Automotive: "15684181"
61
+ Fan Gear: "3386071"
62
+ Exercise & Fitness: "3407731"
63
+ Patio, Lawn & Garden: "286168"
64
+ Musical Instruments: "11091801"
@@ -0,0 +1,9 @@
1
+ namespace :ramazon do
2
+ desc "Generate the yml file used for looking up root browse nodes"
3
+ task :generate_node_yml do
4
+ puts "Generating root nodes"
5
+ Ramazon::BrowseNode.generate_root_nodes
6
+ puts "Done"
7
+ end
8
+ end
9
+
@@ -0,0 +1,92 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{ramazon_advertising}
5
+ s.version = "0.1.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Dan Pickett"]
9
+ s.date = %q{2009-09-08}
10
+ s.description = %q{TODO: longer description of your gem}
11
+ s.email = %q{dpickett@enlightsolutions.com}
12
+ s.extra_rdoc_files = [
13
+ "LICENSE",
14
+ "README.rdoc"
15
+ ]
16
+ s.files = [
17
+ ".document",
18
+ ".gitignore",
19
+ "LICENSE",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "features/friendly_errors.feature",
24
+ "features/generate_root_browse_nodes.feature",
25
+ "features/retrieving_a_product.feature",
26
+ "features/searching_for_products.feature",
27
+ "features/step_definitions/auth_steps.rb",
28
+ "features/step_definitions/browse_node_steps.rb",
29
+ "features/step_definitions/error_steps.rb",
30
+ "features/step_definitions/product_steps.rb",
31
+ "features/step_definitions/ramazon_advertising_steps.rb",
32
+ "features/support/env.rb",
33
+ "features/support/ramazon_advertising.example.yml",
34
+ "lib/ramazon/abstract_element.rb",
35
+ "lib/ramazon/browse_node.rb",
36
+ "lib/ramazon/configuration.rb",
37
+ "lib/ramazon/error.rb",
38
+ "lib/ramazon/image.rb",
39
+ "lib/ramazon/price.rb",
40
+ "lib/ramazon/product.rb",
41
+ "lib/ramazon/product_collection.rb",
42
+ "lib/ramazon/rails_additions.rb",
43
+ "lib/ramazon/request.rb",
44
+ "lib/ramazon/signatory.rb",
45
+ "lib/ramazon_advertising.rb",
46
+ "lib/root_nodes.yml",
47
+ "lib/tasks/ramazon.rake",
48
+ "ramazon_advertising.gemspec",
49
+ "spec/ramazon/configuration_spec.rb",
50
+ "spec/spec_helper.rb"
51
+ ]
52
+ s.homepage = %q{http://github.com/dpickett/ramazon_advertising}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.5}
56
+ s.summary = %q{TODO: one-line summary of your gem}
57
+ s.test_files = [
58
+ "spec/ramazon/configuration_spec.rb",
59
+ "spec/spec_helper.rb"
60
+ ]
61
+
62
+ if s.respond_to? :specification_version then
63
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
64
+ s.specification_version = 3
65
+
66
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
67
+ s.add_runtime_dependency(%q<jnunemaker-httparty>, [">= 0.4.3"])
68
+ s.add_runtime_dependency(%q<jnunemaker-happymapper>, [">= 0.2.5"])
69
+ s.add_runtime_dependency(%q<mislav-will_paginate>, [">= 2.3.11"])
70
+ s.add_runtime_dependency(%q<nokogiri>, [">= 1.3.3"])
71
+ s.add_runtime_dependency(%q<configatron>, [">= 2.4.1"])
72
+ s.add_runtime_dependency(%q<rails>, [">= 2.3.3"])
73
+ s.add_runtime_dependency(%q<ruby-hmac>, [">= 0.3.2"])
74
+ else
75
+ s.add_dependency(%q<jnunemaker-httparty>, [">= 0.4.3"])
76
+ s.add_dependency(%q<jnunemaker-happymapper>, [">= 0.2.5"])
77
+ s.add_dependency(%q<mislav-will_paginate>, [">= 2.3.11"])
78
+ s.add_dependency(%q<nokogiri>, [">= 1.3.3"])
79
+ s.add_dependency(%q<configatron>, [">= 2.4.1"])
80
+ s.add_dependency(%q<rails>, [">= 2.3.3"])
81
+ s.add_dependency(%q<ruby-hmac>, [">= 0.3.2"])
82
+ end
83
+ else
84
+ s.add_dependency(%q<jnunemaker-httparty>, [">= 0.4.3"])
85
+ s.add_dependency(%q<jnunemaker-happymapper>, [">= 0.2.5"])
86
+ s.add_dependency(%q<mislav-will_paginate>, [">= 2.3.11"])
87
+ s.add_dependency(%q<nokogiri>, [">= 1.3.3"])
88
+ s.add_dependency(%q<configatron>, [">= 2.4.1"])
89
+ s.add_dependency(%q<rails>, [">= 2.3.3"])
90
+ s.add_dependency(%q<ruby-hmac>, [">= 0.3.2"])
91
+ end
92
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+
3
+ describe Ramazon::Configuration do
4
+ it "should default to the us" do
5
+ Ramazon::Configuration.locale.should eql(:us)
6
+ end
7
+
8
+ it "should raise an exception if a set a locale that doesn't exist" do
9
+ lambda {Ramazon::Configuration.locale = :ffsf}.should raise_error
10
+ end
11
+
12
+ it "should derive a url of the locale" do
13
+ Ramazon::Configuration.locale = :de
14
+ Ramazon::Configuration.base_uri.should =~ /#{Ramazon::Configuration.locale.to_s}/
15
+ end
16
+
17
+ it "should use .com for a us address" do
18
+ Ramazon::Configuration.locale = :us
19
+ Ramazon::Configuration.base_uri.should =~ /\.com/
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'ramazon_advertising'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+
8
+ Spec::Runner.configure do |config|
9
+
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dpickett-ramazon_advertising
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Dan Pickett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: jnunemaker-httparty
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.4.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: jnunemaker-happymapper
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.5
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: mislav-will_paginate
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.3.11
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: nokogiri
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.3.3
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: configatron
57
+ type: :runtime
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.4.1
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: rails
67
+ type: :runtime
68
+ version_requirement:
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.3.3
74
+ version:
75
+ - !ruby/object:Gem::Dependency
76
+ name: ruby-hmac
77
+ type: :runtime
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.3.2
84
+ version:
85
+ description: "TODO: longer description of your gem"
86
+ email: dpickett@enlightsolutions.com
87
+ executables: []
88
+
89
+ extensions: []
90
+
91
+ extra_rdoc_files:
92
+ - LICENSE
93
+ - README.rdoc
94
+ files:
95
+ - .document
96
+ - .gitignore
97
+ - LICENSE
98
+ - README.rdoc
99
+ - Rakefile
100
+ - VERSION
101
+ - features/friendly_errors.feature
102
+ - features/generate_root_browse_nodes.feature
103
+ - features/retrieving_a_product.feature
104
+ - features/searching_for_products.feature
105
+ - features/step_definitions/auth_steps.rb
106
+ - features/step_definitions/browse_node_steps.rb
107
+ - features/step_definitions/error_steps.rb
108
+ - features/step_definitions/product_steps.rb
109
+ - features/step_definitions/ramazon_advertising_steps.rb
110
+ - features/support/env.rb
111
+ - features/support/ramazon_advertising.example.yml
112
+ - lib/ramazon/abstract_element.rb
113
+ - lib/ramazon/browse_node.rb
114
+ - lib/ramazon/configuration.rb
115
+ - lib/ramazon/error.rb
116
+ - lib/ramazon/image.rb
117
+ - lib/ramazon/price.rb
118
+ - lib/ramazon/product.rb
119
+ - lib/ramazon/product_collection.rb
120
+ - lib/ramazon/rails_additions.rb
121
+ - lib/ramazon/request.rb
122
+ - lib/ramazon/signatory.rb
123
+ - lib/ramazon_advertising.rb
124
+ - lib/root_nodes.yml
125
+ - lib/tasks/ramazon.rake
126
+ - ramazon_advertising.gemspec
127
+ - spec/ramazon/configuration_spec.rb
128
+ - spec/spec_helper.rb
129
+ has_rdoc: false
130
+ homepage: http://github.com/dpickett/ramazon_advertising
131
+ post_install_message:
132
+ rdoc_options:
133
+ - --charset=UTF-8
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: "0"
141
+ version:
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: "0"
147
+ version:
148
+ requirements: []
149
+
150
+ rubyforge_project:
151
+ rubygems_version: 1.2.0
152
+ signing_key:
153
+ specification_version: 3
154
+ summary: "TODO: one-line summary of your gem"
155
+ test_files:
156
+ - spec/ramazon/configuration_spec.rb
157
+ - spec/spec_helper.rb