fassbinder 0.0.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/.gitignore +7 -0
- data/Gemfile +3 -0
- data/LICENSE +13 -0
- data/README.md +4 -0
- data/Rakefile +11 -0
- data/fassbinder.gemspec +29 -0
- data/lib/fassbinder/algorithm.rb +25 -0
- data/lib/fassbinder/book.rb +29 -0
- data/lib/fassbinder/condition.rb +29 -0
- data/lib/fassbinder/description.rb +59 -0
- data/lib/fassbinder/errors.rb +3 -0
- data/lib/fassbinder/offer.rb +38 -0
- data/lib/fassbinder/request.rb +22 -0
- data/lib/fassbinder/seller.rb +29 -0
- data/lib/fassbinder/struct.rb +15 -0
- data/lib/fassbinder/version.rb +3 -0
- data/lib/fassbinder.rb +7 -0
- data/spec/fabricators/condition_fabricator.rb +7 -0
- data/spec/fabricators/offer_fabricator.rb +8 -0
- data/spec/fabricators/seller_fabricator.rb +15 -0
- data/spec/fassbinder/algorithm_spec.rb +49 -0
- data/spec/fassbinder/book_spec.rb +50 -0
- data/spec/fassbinder/condition_spec.rb +31 -0
- data/spec/fassbinder/description_spec.rb +55 -0
- data/spec/fassbinder/offer_spec.rb +133 -0
- data/spec/fassbinder/request_spec.rb +42 -0
- data/spec/fassbinder/seller_spec.rb +71 -0
- data/spec/fassbinder/struct_spec.rb +28 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/credentials.rb +3 -0
- data/spec/support/faker.rb +19 -0
- data/spec/support/vcr.rb +12 -0
- metadata +177 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/fassbinder.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'fassbinder/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'fassbinder'
|
7
|
+
s.version = Fassbinder::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Paper Cavalier']
|
10
|
+
s.email = 'code@papercavalier.com'
|
11
|
+
s.homepage = 'https://rubygems.org/gems/fassbinder'
|
12
|
+
s.summary = %q{Wraps Amazon in a loving embrace.}
|
13
|
+
s.description = %q{Fassbinder wraps Amazon in a loving embrace.}
|
14
|
+
|
15
|
+
s.rubyforge_project = 'fassbinder'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
|
22
|
+
s.add_dependency('kosher', '~> 0.2.4')
|
23
|
+
s.add_dependency('sucker', '~> 1.3.1')
|
24
|
+
s.add_development_dependency('fabrication', '~> 0.9.5')
|
25
|
+
s.add_development_dependency('rspec', '~> 2.5.0')
|
26
|
+
s.add_development_dependency('ruby-debug19', '~> 0.11.6')
|
27
|
+
s.add_development_dependency('vcr', '~> 1.7.0')
|
28
|
+
s.add_development_dependency('webmock', '~> 1.6.2')
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Fassbinder
|
2
|
+
class Algorithm
|
3
|
+
def initialize(response)
|
4
|
+
raise InvalidResponseError unless response.valid?
|
5
|
+
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def books
|
10
|
+
@response.map('Item') do |item|
|
11
|
+
Book.build(item)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def errors
|
16
|
+
@response.errors.map do |error|
|
17
|
+
error['Message'].scan(/[0-9A-Z]{10}/).first rescue nil
|
18
|
+
end.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
def response
|
22
|
+
@response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Kosher
|
2
|
+
class Book < Struct.new(:asin, :offers, :offers_count, :sales_rank)
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def build(doc)
|
6
|
+
asin = doc['ASIN']
|
7
|
+
sales_rank = doc['SalesRank'].to_i
|
8
|
+
offers_count = doc['Offers']['TotalOffers'].to_i
|
9
|
+
offers = build_offers(doc['Offers']['Offer'])
|
10
|
+
|
11
|
+
new(asin, offers, offers_count, sales_rank)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_offers(offers)
|
17
|
+
[offers].flatten.compact.map do |offer|
|
18
|
+
|
19
|
+
# Senify Yen because Ruby Money says so
|
20
|
+
if offer['OfferListing']['Price']['CurrencyCode'] == 'JPY'
|
21
|
+
offer['OfferListing']['Price']['Amount'] = offer['OfferListing']['Price']['Amount'].to_i * 100
|
22
|
+
end
|
23
|
+
|
24
|
+
Offer.build(offer)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Kosher
|
2
|
+
class Condition < Struct.new(:grade)
|
3
|
+
|
4
|
+
alias_method :to_i, :grade
|
5
|
+
|
6
|
+
CONDITIONS = {
|
7
|
+
'new' => 1,
|
8
|
+
'mint' => 2,
|
9
|
+
'verygood' => 3,
|
10
|
+
'good' => 4,
|
11
|
+
'acceptable' => 5 }
|
12
|
+
|
13
|
+
def initialize(string = '')
|
14
|
+
self.grade = CONDITIONS[string] || 6
|
15
|
+
end
|
16
|
+
|
17
|
+
def kosher?
|
18
|
+
grade <= 4
|
19
|
+
end
|
20
|
+
|
21
|
+
def new?
|
22
|
+
grade == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def used?
|
26
|
+
!new?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Kosher
|
2
|
+
class Description < String
|
3
|
+
DAMAGED = "\\b(?:missing|torn|broken|split|discard|withdrawn|rent|stain|school|damaged|water)"
|
4
|
+
EXLIB = "(?:e?x|discarded|retired|former|has|have)[\\s._-]*lib"
|
5
|
+
MARKED = "(highlight|hilit|underlin)"
|
6
|
+
MISSING_VOL = "(vols?|volume) only"
|
7
|
+
REVIEW_COPY = "\\b(?:uncorrected|advanced?\\sreview|arc)\\b"
|
8
|
+
|
9
|
+
def kosher?
|
10
|
+
!(present? && bad?)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def bad?
|
16
|
+
damaged? ||
|
17
|
+
ex_library? ||
|
18
|
+
marked? ||
|
19
|
+
missing_volume? ||
|
20
|
+
review_copy?
|
21
|
+
end
|
22
|
+
|
23
|
+
def damaged?
|
24
|
+
matches(DAMAGED)
|
25
|
+
end
|
26
|
+
|
27
|
+
def does_not_match(value)
|
28
|
+
!match(Regexp.new(value, true))
|
29
|
+
end
|
30
|
+
|
31
|
+
def ex_library?
|
32
|
+
matches(EXLIB) && does_not_match(negation_of(EXLIB))
|
33
|
+
end
|
34
|
+
|
35
|
+
def marked?
|
36
|
+
matches(MARKED) && does_not_match(negation_of(MARKED))
|
37
|
+
end
|
38
|
+
|
39
|
+
def matches(value)
|
40
|
+
!does_not_match(value)
|
41
|
+
end
|
42
|
+
|
43
|
+
def missing_volume?
|
44
|
+
matches(MISSING_VOL)
|
45
|
+
end
|
46
|
+
|
47
|
+
def negation_of(value)
|
48
|
+
"(?:no|not an?)\\s+#{value}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def present?
|
52
|
+
self != ''
|
53
|
+
end
|
54
|
+
|
55
|
+
def review_copy?
|
56
|
+
matches(REVIEW_COPY)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Kosher
|
2
|
+
class Offer < Struct.new(
|
3
|
+
:seller,
|
4
|
+
:condition,
|
5
|
+
:description,
|
6
|
+
:ships_in,
|
7
|
+
:ships_free,
|
8
|
+
:cents,
|
9
|
+
:exchange_id,
|
10
|
+
:listing_id)
|
11
|
+
|
12
|
+
def self.build(doc)
|
13
|
+
offer = new
|
14
|
+
offer.seller = Seller.build(doc['Merchant'])
|
15
|
+
|
16
|
+
attributes = doc['OfferAttributes']
|
17
|
+
offer.condition = Condition.new(attributes['SubCondition'])
|
18
|
+
offer.description = Description.new(attributes['ConditionNote'].to_s)
|
19
|
+
|
20
|
+
listing = doc['OfferListing']
|
21
|
+
offer.ships_in = listing['AvailabilityAttributes']['MaximumHours'].to_i
|
22
|
+
offer.ships_free = listing['IsEligibleForSuperSaverShipping'] == '1'
|
23
|
+
offer.cents = listing['Price']['Amount'].to_i
|
24
|
+
offer.exchange_id = listing['ExchangeId']
|
25
|
+
offer.listing_id = listing['OfferListingId']
|
26
|
+
|
27
|
+
offer
|
28
|
+
end
|
29
|
+
|
30
|
+
def kosher?
|
31
|
+
condition.kosher? && seller.kosher? && description.kosher? && ships_now?
|
32
|
+
end
|
33
|
+
|
34
|
+
def ships_now?
|
35
|
+
ships_in.to_i <= 48
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Fassbinder
|
2
|
+
class Request < Sucker::Request
|
3
|
+
def initialize(args = {})
|
4
|
+
super
|
5
|
+
self.<<({
|
6
|
+
'Operation' => 'ItemLookup',
|
7
|
+
'ItemLookup.Shared.IdType' => 'ASIN',
|
8
|
+
'ItemLookup.Shared.Condition' => 'All',
|
9
|
+
'ItemLookup.Shared.MerchantId' => 'All',
|
10
|
+
'ItemLookup.Shared.ResponseGroup' => ['OfferFull', 'SalesRank'] })
|
11
|
+
end
|
12
|
+
|
13
|
+
def batchify(asins)
|
14
|
+
self.<<({ 'ItemLookup.1.ItemId' => asins[0, 10] })
|
15
|
+
self.<<({ 'ItemLookup.2.ItemId' => asins[10, 10] }) if asins.size > 10
|
16
|
+
end
|
17
|
+
|
18
|
+
def get
|
19
|
+
Algorithm.new(super)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Kosher
|
2
|
+
class Seller < Struct.new(:merchant_id, :name, :average_rating)
|
3
|
+
class << self
|
4
|
+
attr_accessor :blacklist
|
5
|
+
|
6
|
+
def build(doc)
|
7
|
+
merchant_id = doc['MerchantId']
|
8
|
+
name = doc['Name']
|
9
|
+
average_rating = doc['AverageFeedbackRating'].to_f
|
10
|
+
|
11
|
+
new(merchant_id, name, average_rating)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def blacklist
|
16
|
+
self.class.blacklist
|
17
|
+
end
|
18
|
+
|
19
|
+
def blacklisted?
|
20
|
+
blacklist.include?(merchant_id) rescue false
|
21
|
+
end
|
22
|
+
|
23
|
+
def kosher?
|
24
|
+
return false if blacklisted?
|
25
|
+
|
26
|
+
average_rating == 0.0 || average_rating > 4.7
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/fassbinder.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Fabricator(:seller, :class_name => 'kosher/seller') do
|
2
|
+
merchant_id Faker::Amazon.merchant_id
|
3
|
+
end
|
4
|
+
|
5
|
+
Fabricator(:new_seller, :from => :seller) do
|
6
|
+
average_rating 0.0
|
7
|
+
end
|
8
|
+
|
9
|
+
Fabricator(:good_seller, :from => :seller) do
|
10
|
+
average_rating 5.0
|
11
|
+
end
|
12
|
+
|
13
|
+
Fabricator(:bad_seller, :from => :seller) do
|
14
|
+
average_rating 4.5
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Algorithm do
|
5
|
+
use_vcr_cassette 'batch-request'
|
6
|
+
|
7
|
+
let(:asins) do
|
8
|
+
|
9
|
+
# The last ASIN does not exist.
|
10
|
+
%w{
|
11
|
+
0816614024 0143105825 0485113600 0816616779 0942299078
|
12
|
+
0816614008 144006654X 0486400360 0486417670 087220474X
|
13
|
+
0486454398 0268018359 1604246014 184467598X 0312427182
|
14
|
+
1844674282 0745640974 0745646441 0826489540 2081232191 }
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:algorithm) do
|
18
|
+
request = Request.new(credentials)
|
19
|
+
request.locale = :us
|
20
|
+
request.batchify(asins)
|
21
|
+
request.get
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".new" do
|
25
|
+
it "raises an error if response is not valid" do
|
26
|
+
response = mock('Response')
|
27
|
+
response.stub!(:valid?).and_return(false)
|
28
|
+
|
29
|
+
expect do
|
30
|
+
Algorithm.new(response)
|
31
|
+
end.to raise_error InvalidResponseError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#books" do
|
36
|
+
it "should return found books" do
|
37
|
+
algorithm.items.count.should eql 19
|
38
|
+
algorithm.items.first.should be_a Book
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#errors" do
|
43
|
+
it "should return ASINs that are not found" do
|
44
|
+
algorithm.errors.count.should eql 1
|
45
|
+
algorithm.errors.first.should eql '2081232191'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Item do
|
5
|
+
it "should descend from Kosher::Struct" do
|
6
|
+
Item.ancestors.should include Kosher::Struct
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ".build" do
|
10
|
+
use_vcr_cassette '0143105825'
|
11
|
+
|
12
|
+
let(:asin) { '0143105825' }
|
13
|
+
|
14
|
+
it "should build an item" do
|
15
|
+
request = Request.new(credentials)
|
16
|
+
request.locale = :us
|
17
|
+
request.batchify([asin])
|
18
|
+
algorithm = request.get
|
19
|
+
response = algorithm.instance_variable_get(:@response)
|
20
|
+
doc = response.find('Item').first
|
21
|
+
item = Item.build(doc)
|
22
|
+
|
23
|
+
item.should be_a Item
|
24
|
+
item.asin.should eql asin
|
25
|
+
item.offers_count.should be > 0
|
26
|
+
item.sales_rank.should be > 0
|
27
|
+
item.offers.count.should > 0
|
28
|
+
item.offers.first.should be_a Offer
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should senify Yen in Japanese requests" do
|
32
|
+
request = Request.new(credentials)
|
33
|
+
request.locale = :jp
|
34
|
+
request.batchify([asin])
|
35
|
+
algorithm = request.get
|
36
|
+
response = algorithm.instance_variable_get(:@response)
|
37
|
+
doc = response.find('Item').first
|
38
|
+
|
39
|
+
doc['Offers']['Offer'].each do |offer|
|
40
|
+
offer['OfferListing']['Price']['Amount'] = 1
|
41
|
+
end
|
42
|
+
item = Item.build(doc)
|
43
|
+
|
44
|
+
item.offers.each do |offer|
|
45
|
+
offer.cents.should eql 100
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Condition do
|
5
|
+
describe "#to_i" do
|
6
|
+
def this(str)
|
7
|
+
Condition.new(str).to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
it "casts as integer" do
|
11
|
+
this('new').should eql 1
|
12
|
+
this('mint').should eql 2
|
13
|
+
this('verygood').should eql 3
|
14
|
+
this('good').should eql 4
|
15
|
+
this('acceptable').should eql 5
|
16
|
+
end
|
17
|
+
|
18
|
+
it "casts unrecognized conditions as 6" do
|
19
|
+
this('refurbished').should eql 6
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#kosher?" do
|
24
|
+
it "returns true if condition is good or better" do
|
25
|
+
Condition.new('verygood').should be_kosher
|
26
|
+
Condition.new('good').should be_kosher
|
27
|
+
Condition.new('acceptable').should_not be_kosher
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Description do
|
5
|
+
def this(value)
|
6
|
+
Description.new(value)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "inherits from String" do
|
10
|
+
Description.ancestors.should include String
|
11
|
+
end
|
12
|
+
|
13
|
+
it "validates a blank description" do
|
14
|
+
this('').should be_kosher
|
15
|
+
end
|
16
|
+
|
17
|
+
it "validates a non-blank description" do
|
18
|
+
this('foo').should be_kosher
|
19
|
+
end
|
20
|
+
|
21
|
+
it "does not validate advance review copies" do
|
22
|
+
this('Uncorrected review copy').should_not be_kosher
|
23
|
+
this('arc').should_not be_kosher
|
24
|
+
this('arc.').should_not be_kosher
|
25
|
+
|
26
|
+
this('marc').should be_kosher
|
27
|
+
end
|
28
|
+
|
29
|
+
it "does not validate marked books" do
|
30
|
+
this('Some highlighting').should_not be_kosher
|
31
|
+
this('Underlining.').should_not be_kosher
|
32
|
+
this('Good. Hiliting.').should_not be_kosher
|
33
|
+
|
34
|
+
this('No highlighting.').should be_kosher
|
35
|
+
end
|
36
|
+
|
37
|
+
it "does not validate books with missing volumes" do
|
38
|
+
this('First vol only.').should_not be_kosher
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not validate damaged or worn books" do
|
42
|
+
this('Different').should be_kosher
|
43
|
+
this('Rental').should_not be_kosher
|
44
|
+
this('Torn pages').should_not be_kosher
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not validate withdrawn library copies" do
|
48
|
+
this('xlib').should_not be_kosher
|
49
|
+
this('ex-library').should_not be_kosher
|
50
|
+
this('retired library copy').should_not be_kosher
|
51
|
+
|
52
|
+
this('Not an ex-library').should be_kosher
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Offer do
|
5
|
+
let(:offer) { Fabricate(:offer) }
|
6
|
+
|
7
|
+
describe ".build" do
|
8
|
+
use_vcr_cassette '0143105825'
|
9
|
+
|
10
|
+
let(:asin) { '0143105825' }
|
11
|
+
|
12
|
+
let(:doc) do
|
13
|
+
request = Request.new(credentials)
|
14
|
+
request.locale = :us
|
15
|
+
request.batchify([asin])
|
16
|
+
algorithm = request.get
|
17
|
+
|
18
|
+
response = algorithm.instance_variable_get(:@response)
|
19
|
+
response.find('Offer').first
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should build an offer" do
|
23
|
+
offer = Offer.build(doc)
|
24
|
+
|
25
|
+
offer.should be_a Offer
|
26
|
+
expect do
|
27
|
+
offer.kosher?
|
28
|
+
end.should_not raise_error
|
29
|
+
end
|
30
|
+
|
31
|
+
it "populates the offer listing ID" do
|
32
|
+
offer = Offer.build(doc)
|
33
|
+
offer.listing_id.should_not be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "populates the exchange ID" do
|
37
|
+
offer = Offer.build(doc)
|
38
|
+
offer.exchange_id.should_not be_nil
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should handle blank descriptions" do
|
42
|
+
doc['OfferAttributes']['ConditionNote'] = ''
|
43
|
+
offer = Offer.build(doc)
|
44
|
+
|
45
|
+
offer.description.should eql ''
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#kosher?" do
|
50
|
+
context "when condition is kosher" do
|
51
|
+
before do
|
52
|
+
offer.condition = Fabricate(:kosher_condition, :grade => 1)
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when seller is kosher" do
|
56
|
+
before do
|
57
|
+
offer.seller = Fabricate(:good_seller)
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when description is kosher" do
|
61
|
+
context "when offer ships now" do
|
62
|
+
it "returns true" do
|
63
|
+
offer.should be_kosher
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when offer does not ship now" do
|
68
|
+
before do
|
69
|
+
offer.ships_in = 96
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns false" do
|
73
|
+
offer.should_not be_kosher
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when description is not kosher" do
|
79
|
+
before do
|
80
|
+
offer.description = Kosher::Description.new("Withdrawn library book")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns false" do
|
84
|
+
offer.should_not be_kosher
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when seller is not kosher" do
|
90
|
+
before do
|
91
|
+
offer.seller = Fabricate(:bad_seller)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns false" do
|
95
|
+
offer.should_not be_kosher
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when condition is not kosher" do
|
101
|
+
before do
|
102
|
+
offer.condition = Fabricate(:kosher_condition, :grade => 5)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns false" do
|
106
|
+
offer.should_not be_kosher
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#ships_now?" do
|
112
|
+
context "when offer ships within 48 hours" do
|
113
|
+
before do
|
114
|
+
offer.ships_in = 48
|
115
|
+
end
|
116
|
+
|
117
|
+
it "returns true" do
|
118
|
+
offer.send(:ships_now?).should be_true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when offer ships in over 48 hours" do
|
123
|
+
before do
|
124
|
+
offer.ships_in = 96
|
125
|
+
end
|
126
|
+
|
127
|
+
it "returns false" do
|
128
|
+
offer.send(:ships_now?).should be_false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Fassbinder
|
4
|
+
describe Request do
|
5
|
+
let(:request) { Request.new(credentials) }
|
6
|
+
|
7
|
+
describe ".new" do
|
8
|
+
it "defines a batch request" do
|
9
|
+
request.parameters['Operation'].should eql 'ItemLookup'
|
10
|
+
request.parameters['ItemLookup.Shared.IdType'].should eql 'ASIN'
|
11
|
+
request.parameters['ItemLookup.Shared.Condition'].should eql 'All'
|
12
|
+
request.parameters['ItemLookup.Shared.MerchantId'].should eql 'All'
|
13
|
+
request.parameters['ItemLookup.Shared.ResponseGroup'].should eql ['OfferFull', 'SalesRank']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#batchify" do
|
18
|
+
it "adds up to 20 ASINs to the worker's parameters" do
|
19
|
+
asins = (0..19).to_a
|
20
|
+
request.batchify(asins)
|
21
|
+
|
22
|
+
request.parameters['ItemLookup.1.ItemId'].should eql (0..9).to_a
|
23
|
+
request.parameters['ItemLookup.2.ItemId'].should eql (10..19).to_a
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#get" do
|
28
|
+
before do
|
29
|
+
VCR.http_stubbing_adapter.http_connections_allowed = true
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns an algorithm" do
|
33
|
+
Request.stub!(:get)
|
34
|
+
|
35
|
+
request.locale = :us
|
36
|
+
request.batchify('foo')
|
37
|
+
|
38
|
+
request.get.should be_a Algorithm
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Seller do
|
5
|
+
describe ".build" do
|
6
|
+
use_vcr_cassette '0143105825'
|
7
|
+
|
8
|
+
let(:asin) { '0143105825' }
|
9
|
+
|
10
|
+
let(:response) do
|
11
|
+
request = Request.new(credentials)
|
12
|
+
request.locale = :us
|
13
|
+
request.batchify([asin])
|
14
|
+
algorithm = request.get
|
15
|
+
|
16
|
+
algorithm.instance_variable_get(:@response)
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:seller) do
|
20
|
+
docs = response.find('Merchant')
|
21
|
+
Seller.build(docs.first)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "populates the merchant ID of a seller" do
|
25
|
+
seller.merchant_id.should match /^[0-9A-Z]{13,14}$/
|
26
|
+
end
|
27
|
+
|
28
|
+
it "populates the name of a seller" do
|
29
|
+
seller.name.should_not be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "populates the average rating of a seller" do
|
33
|
+
seller.average_rating.should be_an_instance_of Float
|
34
|
+
end
|
35
|
+
|
36
|
+
it "builds a newly-launched seller" do
|
37
|
+
doc = response.find('Merchant').detect { |doc| doc["AverageFeedbackRating"] == "0.0" }
|
38
|
+
new_seller = Seller.build(doc)
|
39
|
+
|
40
|
+
new_seller.average_rating.should eql 0.0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#kosher?" do
|
45
|
+
it "returns true if seller's average rating is 0.0" do
|
46
|
+
seller = Fabricate(:new_seller)
|
47
|
+
seller.should be_kosher
|
48
|
+
end
|
49
|
+
|
50
|
+
it "returns true if seller's average rating is above 4.7" do
|
51
|
+
seller = Fabricate(:good_seller)
|
52
|
+
seller.should be_kosher
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns false if sellers' average rating is 4.7 or below" do
|
56
|
+
seller = Fabricate(:bad_seller)
|
57
|
+
seller.should_not be_kosher
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns false if seller is blacklisted" do
|
61
|
+
seller = Fabricate(:good_seller)
|
62
|
+
seller.should be_kosher
|
63
|
+
|
64
|
+
seller.merchant_id = Faker::Amazon.merchant_id
|
65
|
+
Seller.blacklist = [seller.merchant_id]
|
66
|
+
|
67
|
+
seller.should_not be_kosher
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kosher
|
4
|
+
describe Struct do
|
5
|
+
before(:all) do
|
6
|
+
class Foo < Struct.new(:bar); end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "to_json" do
|
10
|
+
it "converts to JSON" do
|
11
|
+
foo = Foo.new
|
12
|
+
foo.bar = 1
|
13
|
+
|
14
|
+
foo.to_json.should eql "{\"bar\":1}"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "handles nested structs" do
|
18
|
+
foo_1 = Foo.new
|
19
|
+
foo_2 = Foo.new
|
20
|
+
|
21
|
+
foo_2.bar = 1
|
22
|
+
foo_1.bar = foo_2
|
23
|
+
|
24
|
+
foo_1.to_json.should eql "{\"bar\":{\"bar\":1}}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Faker
|
2
|
+
module Amazon
|
3
|
+
class << self
|
4
|
+
def asin
|
5
|
+
raw = 9.times.map { rand(10).to_s }
|
6
|
+
(raw << Bookland::ISBN.new.send(:check_digit_10, raw)).join
|
7
|
+
end
|
8
|
+
|
9
|
+
def ean
|
10
|
+
Bookland::ISBN.to_13(asin)
|
11
|
+
end
|
12
|
+
|
13
|
+
def merchant_id
|
14
|
+
seed = ('A'..'Z').to_a + (0..9).to_a
|
15
|
+
14.times.map { seed[rand(seed.size)] }.join
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/spec/support/vcr.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'vcr'
|
2
|
+
|
3
|
+
VCR.config do |c|
|
4
|
+
c.cassette_library_dir = "#{File.dirname(__FILE__)}/../fixtures/cassette_library"
|
5
|
+
c.stub_with :webmock
|
6
|
+
c.ignore_localhost = true
|
7
|
+
c.default_cassette_options = { :record => :new_episodes, :match_requests_on => [:host] }
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.extend VCR::RSpec::Macros
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fassbinder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paper Cavalier
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-09 00:00:00 +00:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: kosher
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.2.4
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sucker
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ~>
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 1.3.1
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: fabrication
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.5
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rspec
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.5.0
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: ruby-debug19
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.11.6
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: vcr
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 1.7.0
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: webmock
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ~>
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 1.6.2
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id007
|
93
|
+
description: Fassbinder wraps Amazon in a loving embrace.
|
94
|
+
email: code@papercavalier.com
|
95
|
+
executables: []
|
96
|
+
|
97
|
+
extensions: []
|
98
|
+
|
99
|
+
extra_rdoc_files: []
|
100
|
+
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- Gemfile
|
104
|
+
- LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- fassbinder.gemspec
|
108
|
+
- lib/fassbinder.rb
|
109
|
+
- lib/fassbinder/algorithm.rb
|
110
|
+
- lib/fassbinder/book.rb
|
111
|
+
- lib/fassbinder/condition.rb
|
112
|
+
- lib/fassbinder/description.rb
|
113
|
+
- lib/fassbinder/errors.rb
|
114
|
+
- lib/fassbinder/offer.rb
|
115
|
+
- lib/fassbinder/request.rb
|
116
|
+
- lib/fassbinder/seller.rb
|
117
|
+
- lib/fassbinder/struct.rb
|
118
|
+
- lib/fassbinder/version.rb
|
119
|
+
- spec/fabricators/condition_fabricator.rb
|
120
|
+
- spec/fabricators/offer_fabricator.rb
|
121
|
+
- spec/fabricators/seller_fabricator.rb
|
122
|
+
- spec/fassbinder/algorithm_spec.rb
|
123
|
+
- spec/fassbinder/book_spec.rb
|
124
|
+
- spec/fassbinder/condition_spec.rb
|
125
|
+
- spec/fassbinder/description_spec.rb
|
126
|
+
- spec/fassbinder/offer_spec.rb
|
127
|
+
- spec/fassbinder/request_spec.rb
|
128
|
+
- spec/fassbinder/seller_spec.rb
|
129
|
+
- spec/fassbinder/struct_spec.rb
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
- spec/support/credentials.rb
|
132
|
+
- spec/support/faker.rb
|
133
|
+
- spec/support/vcr.rb
|
134
|
+
has_rdoc: true
|
135
|
+
homepage: https://rubygems.org/gems/fassbinder
|
136
|
+
licenses: []
|
137
|
+
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
|
141
|
+
require_paths:
|
142
|
+
- lib
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
144
|
+
none: false
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: "0"
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: "0"
|
155
|
+
requirements: []
|
156
|
+
|
157
|
+
rubyforge_project: fassbinder
|
158
|
+
rubygems_version: 1.5.2
|
159
|
+
signing_key:
|
160
|
+
specification_version: 3
|
161
|
+
summary: Wraps Amazon in a loving embrace.
|
162
|
+
test_files:
|
163
|
+
- spec/fabricators/condition_fabricator.rb
|
164
|
+
- spec/fabricators/offer_fabricator.rb
|
165
|
+
- spec/fabricators/seller_fabricator.rb
|
166
|
+
- spec/fassbinder/algorithm_spec.rb
|
167
|
+
- spec/fassbinder/book_spec.rb
|
168
|
+
- spec/fassbinder/condition_spec.rb
|
169
|
+
- spec/fassbinder/description_spec.rb
|
170
|
+
- spec/fassbinder/offer_spec.rb
|
171
|
+
- spec/fassbinder/request_spec.rb
|
172
|
+
- spec/fassbinder/seller_spec.rb
|
173
|
+
- spec/fassbinder/struct_spec.rb
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
- spec/support/credentials.rb
|
176
|
+
- spec/support/faker.rb
|
177
|
+
- spec/support/vcr.rb
|