reevoomark-ruby-api 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/lib/reevoomark.rb +42 -0
- data/lib/reevoomark/cache.rb +32 -0
- data/lib/reevoomark/cache/entry.rb +49 -0
- data/lib/reevoomark/client.rb +29 -0
- data/lib/reevoomark/document.rb +90 -0
- data/lib/reevoomark/document/factory.rb +54 -0
- data/lib/reevoomark/fetcher.rb +43 -0
- data/lib/reevoomark/version.rb +3 -0
- data/spec/acceptance/reevoomark_caching_spec.rb +126 -0
- data/spec/acceptance/reevoomark_spec.rb +82 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/cache_spec.rb +42 -0
- data/spec/unit/client_spec.rb +56 -0
- data/spec/unit/document_spec.rb +115 -0
- data/spec/unit/fetcher_spec.rb +36 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
M2NiODc2MzQyMjljMDQxMTFlMzAxNjlkZGVhNmU1ZDFiM2EyZjM4OA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MTI5YmQzOWFlZTRiNTUzMzA3YWY0YmM4N2NjYTk4MGJlODI0NTk4NQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZjE5N2IxNDJkMTAzMjE2ZWRkNDUzMTUxMDY1MWFiZTNhMjIyMWMyOTRmYTk4
|
10
|
+
ZTI4YTRmYjViYTFkMzA3NjM1NDNjZDNmZDA5MTFmMTQ1MWIzZThiZGQxZjg3
|
11
|
+
YTRiZTYyZDI1ZDEyNDIwY2JkNjIwNmRlYzQ1N2IyNTUxOTQ3MDQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZjNhYzhhODZkN2Y5NTYwMTY4Zjc1Y2RmMTg5YmYyM2FkNzk1YTQ1OWMzZjk1
|
14
|
+
ZTE3OTE3MTMzYjQzMGM4MDE4ZjMzZDU3NmIzZjg2MWExYzAzNzlkOWEyMzg5
|
15
|
+
NWM4ODdhYzE1MDQ4ZjE1NjVmN2E2YjgxYjU5NGYwMGJlZTJjNzE=
|
data/lib/reevoomark.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'uri'
|
3
|
+
require 'httpclient'
|
4
|
+
require 'digest/md5'
|
5
|
+
|
6
|
+
|
7
|
+
# Usage:
|
8
|
+
#
|
9
|
+
# # Somewhere in you application config, build a client.
|
10
|
+
# $reevoomark_client = ReevooMark.create_client(
|
11
|
+
# Rails.root.join("tmp/reevoo_cache"),
|
12
|
+
# "http://mark.reevoo.com/reevoomark/embeddable_reviews.html"
|
13
|
+
# )
|
14
|
+
#
|
15
|
+
# # In your controller (assuming @entry.sku is your product SKU):
|
16
|
+
# @reevoo_reviews = $reevoomark_client.fetch('YOUR TRKREF', @entry.sku)
|
17
|
+
#
|
18
|
+
# # In your view:
|
19
|
+
# <%= @reevoo_reviews.body %>
|
20
|
+
|
21
|
+
module ReevooMark
|
22
|
+
# Legacy API.
|
23
|
+
# Creates a new client every time, so considered bad for business.
|
24
|
+
def self.new(cache_dir, url, trkref, sku)
|
25
|
+
create_client(cache_dir, url).fetch(trkref, sku)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a new client.
|
29
|
+
def self.create_client(cache_dir, base_url, options = {})
|
30
|
+
cache = ReevooMark::Cache.new(cache_dir)
|
31
|
+
fetcher = ReevooMark::Fetcher.new(options[:timeout] || 1)
|
32
|
+
ReevooMark::Client.new(cache, fetcher, base_url)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'reevoomark/version'
|
37
|
+
require 'reevoomark/document'
|
38
|
+
require 'reevoomark/document/factory'
|
39
|
+
require 'reevoomark/client'
|
40
|
+
require 'reevoomark/fetcher'
|
41
|
+
require 'reevoomark/cache'
|
42
|
+
require 'reevoomark/cache/entry'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class ReevooMark::Cache
|
2
|
+
# Create a new cache repository, storing it's cache in the given dir.
|
3
|
+
def initialize(cache_dir)
|
4
|
+
FileUtils.mkdir_p(cache_dir) unless File.exist?(cache_dir)
|
5
|
+
@cache_dir = cache_dir
|
6
|
+
end
|
7
|
+
|
8
|
+
# Fetch the cache entry, don't worry if it's expired.
|
9
|
+
def fetch_expired(remote_url, options = {})
|
10
|
+
entry = entry_for(remote_url)
|
11
|
+
if entry.exists?
|
12
|
+
entry.revalidate_for(options[:revalidate_for])
|
13
|
+
entry.document
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Fetch an unexpired cached document, or store the result of the block.
|
18
|
+
def fetch(remote_url, &fetcher)
|
19
|
+
entry = entry_for(remote_url)
|
20
|
+
if entry.valid?
|
21
|
+
entry.document
|
22
|
+
else
|
23
|
+
entry.document = fetcher.call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def entry_for(remote_url)
|
29
|
+
digest = Digest::MD5.hexdigest(remote_url)
|
30
|
+
Entry.new("#{@cache_dir}/#{digest}.cache")
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class ReevooMark::Cache::Entry
|
2
|
+
attr_reader :cache_path
|
3
|
+
|
4
|
+
def initialize(cache_path)
|
5
|
+
@cache_path = Pathname.new(cache_path)
|
6
|
+
end
|
7
|
+
|
8
|
+
def exists?
|
9
|
+
@cache_path.exist?
|
10
|
+
end
|
11
|
+
|
12
|
+
def expired?
|
13
|
+
document.expired?
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
exists? and not expired?
|
18
|
+
end
|
19
|
+
|
20
|
+
def document
|
21
|
+
raise "Loading from cache, where no cache exists is bad." unless exists?
|
22
|
+
@document ||= YAML.load(read)
|
23
|
+
end
|
24
|
+
|
25
|
+
def document= doc
|
26
|
+
@document = nil # Flush the memoized value
|
27
|
+
write doc.to_yaml
|
28
|
+
doc
|
29
|
+
end
|
30
|
+
|
31
|
+
def revalidate_for(max_age)
|
32
|
+
if exists?
|
33
|
+
self.document = document.revalidated_for(max_age)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
def m_time
|
39
|
+
cache_path.mtime if exists?
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(data)
|
43
|
+
cache_path.open('w'){ |f| f.puts data }
|
44
|
+
end
|
45
|
+
|
46
|
+
def read
|
47
|
+
cache_path.read if exists?
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class ReevooMark::Client
|
2
|
+
DEFAULT_URL = 'http://mark.reevoo.com/reevoomark/embeddable_reviews.html'
|
3
|
+
|
4
|
+
def initialize(cache, fetcher, url = DEFAULT_URL)
|
5
|
+
@cache, @fetcher, @url = cache, fetcher, url
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(trkref, sku)
|
9
|
+
remote_url = url_for(trkref, sku)
|
10
|
+
@cache.fetch(remote_url){ remote_fetch(remote_url) }
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def remote_fetch(remote_url)
|
16
|
+
document = @fetcher.fetch(remote_url)
|
17
|
+
if document.status_code < 500
|
18
|
+
document
|
19
|
+
else
|
20
|
+
@cache.fetch_expired(remote_url, :revalidate_for => 300) || document
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def url_for(trkref, sku)
|
25
|
+
sep = (@url =~ /\?/) ? "&" : "?"
|
26
|
+
"#{@url}#{sep}sku=#{sku}&retailer=#{trkref}"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class ReevooMark::Document
|
2
|
+
attr_reader :time, :status_code, :age, :max_age, :counts
|
3
|
+
|
4
|
+
def self.from_response(response)
|
5
|
+
ReevooMark::Document::Factory.from_response(response)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.error
|
9
|
+
ReevooMark::Document::Factory.new_error_document
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(time, max_age, age, status_code, body, counts)
|
13
|
+
@time, @max_age, @age = time, max_age, age
|
14
|
+
@status_code, @body = status_code, body
|
15
|
+
@counts = counts
|
16
|
+
end
|
17
|
+
|
18
|
+
def identity_values
|
19
|
+
[current_age(0), @content_values]
|
20
|
+
end
|
21
|
+
|
22
|
+
def content_values
|
23
|
+
[@status_code, @body, @counts]
|
24
|
+
end
|
25
|
+
|
26
|
+
def == other
|
27
|
+
identity_values == other.identity_values
|
28
|
+
end
|
29
|
+
|
30
|
+
def === other
|
31
|
+
content_values == other.content_values
|
32
|
+
end
|
33
|
+
|
34
|
+
def any?
|
35
|
+
review_count > 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def review_count
|
39
|
+
@counts[:review_count]
|
40
|
+
end
|
41
|
+
|
42
|
+
def offer_count
|
43
|
+
@counts[:offer_count]
|
44
|
+
end
|
45
|
+
|
46
|
+
def conversation_count
|
47
|
+
@counts[:conversation_count]
|
48
|
+
end
|
49
|
+
|
50
|
+
def best_price
|
51
|
+
@counts[:best_price]
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_valid?
|
55
|
+
status_code < 500
|
56
|
+
end
|
57
|
+
|
58
|
+
def body
|
59
|
+
if is_valid?
|
60
|
+
@body
|
61
|
+
else
|
62
|
+
""
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
alias render body
|
67
|
+
|
68
|
+
def expired?(now = nil)
|
69
|
+
now ||= Time.now
|
70
|
+
max_age < current_age(now)
|
71
|
+
end
|
72
|
+
|
73
|
+
def revalidated_for(max_age)
|
74
|
+
ReevooMark::Document.new(
|
75
|
+
Time.now.to_i,
|
76
|
+
max_age || @max_age,
|
77
|
+
0,
|
78
|
+
@status_code,
|
79
|
+
@body,
|
80
|
+
@counts
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
def current_age(now = nil)
|
86
|
+
now ||= Time.now.to_i
|
87
|
+
now.to_i - (time.to_i - age.to_i)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ReevooMark::Document::Factory
|
2
|
+
class HeaderSet < Hash
|
3
|
+
def initialize(hash)
|
4
|
+
hash.each do |k,v|
|
5
|
+
self[k] = v
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def [] k
|
10
|
+
super(k.downcase)
|
11
|
+
end
|
12
|
+
|
13
|
+
def []= k,v
|
14
|
+
super(k.downcase, v)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
HEADER_MAPPING = {
|
19
|
+
:review_count => 'X-Reevoo-ReviewCount',
|
20
|
+
:offer_count => 'X-Reevoo-OfferCount',
|
21
|
+
:conversation_count => 'X-Reevoo-ConversationCount',
|
22
|
+
:best_price => 'X-Reevoo-BestPrice'
|
23
|
+
}
|
24
|
+
|
25
|
+
# Factory method for building a document from a HTTP response.
|
26
|
+
def self.from_response(response)
|
27
|
+
headers = HeaderSet.new(response.headers)
|
28
|
+
|
29
|
+
counts = HEADER_MAPPING.inject(Hash.new(0)){ |acc, (name, header)|
|
30
|
+
acc.merge(name => headers[header].to_i)
|
31
|
+
}
|
32
|
+
|
33
|
+
if cache_header = headers['Cache-Control']
|
34
|
+
max_age = cache_header.match("max-age=([0-9]+)")[1].to_i
|
35
|
+
else
|
36
|
+
max_age = 300
|
37
|
+
end
|
38
|
+
|
39
|
+
age = headers['Age'].to_i
|
40
|
+
|
41
|
+
ReevooMark::Document.new(
|
42
|
+
Time.now,
|
43
|
+
max_age,
|
44
|
+
age,
|
45
|
+
response.status_code,
|
46
|
+
response.body,
|
47
|
+
counts
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.new_error_document
|
52
|
+
ReevooMark::Document.new(Time.now, 300, 0, 599, "", {})
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class ReevooMark::Fetcher
|
2
|
+
FetchError = Class.new(RuntimeError)
|
3
|
+
|
4
|
+
attr_reader :headers
|
5
|
+
|
6
|
+
def initialize(timeout)
|
7
|
+
@timeout = timeout
|
8
|
+
@http_client = HTTPClient.new
|
9
|
+
@http_client.connect_timeout = timeout
|
10
|
+
@headers = {
|
11
|
+
'User-Agent' => "ReevooMark Ruby Widget/#{ReevooMark::VERSION}",
|
12
|
+
'Referer' => "http://#{Socket::gethostname}"
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch(remote_url)
|
17
|
+
response = fetch_http(remote_url)
|
18
|
+
ReevooMark::Document.from_response(response)
|
19
|
+
rescue FetchError
|
20
|
+
ReevooMark::Document.error
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def log(message)
|
26
|
+
if defined? Rails
|
27
|
+
Rails.logger.debug message
|
28
|
+
else
|
29
|
+
STDERR.puts message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_http(remote_url)
|
34
|
+
log "ReevooMark::Fetcher: Fetching #{remote_url}"
|
35
|
+
Timeout.timeout(@timeout){
|
36
|
+
return @http_client.get(remote_url, nil, headers)
|
37
|
+
}
|
38
|
+
rescue => e
|
39
|
+
raise FetchError, "#{e.class} #{e.message}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "ReevooMark caching" do
|
4
|
+
before do
|
5
|
+
stub_request(:get, /.*example.*/).to_return(:body => "test")
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'with an empty cache' do
|
9
|
+
it 'saves a valid fetched response to the cache file' do
|
10
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")
|
11
|
+
|
12
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
13
|
+
File.open("tmp/cache/#{filename}.cache", 'r').read.should match(/test/)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "saves a 404 response to the cache file" do
|
17
|
+
stub_request(:get, /.*example.*/).to_return(:body => "No content found", :status => 404)
|
18
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")
|
19
|
+
|
20
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
21
|
+
File.open("tmp/cache/#{filename}.cache", 'r').read.should match(/No content found/)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "saves a 500 response to the cache file" do
|
25
|
+
stub_request(:get, /.*example.*/).to_return(:body => "My face is on fire", :status => 500)
|
26
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")
|
27
|
+
|
28
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
29
|
+
File.open("tmp/cache/#{filename}.cache", 'r').read.should match(/My face is on fire/)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with a valid cache' do
|
34
|
+
before do
|
35
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
36
|
+
example = ReevooMark::Document.new(
|
37
|
+
Time.now.to_i,
|
38
|
+
1,
|
39
|
+
0,
|
40
|
+
200,
|
41
|
+
"I'm a cache record.",
|
42
|
+
:review_count => 1,
|
43
|
+
:offer_count => 2,
|
44
|
+
:conversation_count => 3,
|
45
|
+
:best_price => 4
|
46
|
+
)
|
47
|
+
|
48
|
+
File.open("tmp/cache/#{filename}.cache", 'w') do |file|
|
49
|
+
file << example.to_yaml
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
subject {ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")}
|
54
|
+
|
55
|
+
it "does NOT make an http request" do
|
56
|
+
subject
|
57
|
+
WebMock.should_not have_requested(:get, "http://example.com/foo?sku=SKU123&retailer=PNY")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns the cached response body" do
|
61
|
+
subject.review_count.should == 1
|
62
|
+
subject.offer_count.should == 2
|
63
|
+
subject.conversation_count.should == 3
|
64
|
+
subject.best_price.should == 4
|
65
|
+
subject.render.should == "I'm a cache record."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with an expired cache' do
|
70
|
+
before do
|
71
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
72
|
+
example = ReevooMark::Document.new(
|
73
|
+
Time.now.to_i - 60*60,
|
74
|
+
1,
|
75
|
+
0,
|
76
|
+
200,
|
77
|
+
"I'm a cache record.",
|
78
|
+
:review_count => 1,
|
79
|
+
:offer_count => 2,
|
80
|
+
:conversation_count => 3,
|
81
|
+
:best_price => 4
|
82
|
+
)
|
83
|
+
|
84
|
+
File.open("tmp/cache/#{filename}.cache", 'w') do |file|
|
85
|
+
file << example.to_yaml
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
subject {ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")}
|
90
|
+
|
91
|
+
it "makes an http request" do
|
92
|
+
subject
|
93
|
+
WebMock.should have_requested(:get, "http://example.com/foo?sku=SKU123&retailer=PNY")
|
94
|
+
end
|
95
|
+
|
96
|
+
context "and a functioning server" do
|
97
|
+
it "returns the response body" do
|
98
|
+
subject.render.should == "test"
|
99
|
+
end
|
100
|
+
it 'saves the fetched response to the cache file' do
|
101
|
+
subject
|
102
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
103
|
+
File.open("tmp/cache/#{filename}.cache", 'r').read.should match(/test/)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "and an erroring server" do
|
108
|
+
|
109
|
+
before do
|
110
|
+
stub_request(:get, /.*example.*/).to_return(:body => "My face is on fire", :status => 500)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "returns the cached response body" do
|
114
|
+
subject.render.should == "I'm a cache record."
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'does not save the fetched response to the cache file' do
|
118
|
+
subject
|
119
|
+
filename = Digest::MD5.hexdigest("http://example.com/foo?sku=SKU123&retailer=PNY")
|
120
|
+
File.open("tmp/cache/#{filename}.cache", 'r').read.should match(/I'm a cache record/)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ReevooMark do
|
4
|
+
|
5
|
+
describe "a new ReevooMark instance" do
|
6
|
+
it "requires 4 arguments" do
|
7
|
+
lambda { ReevooMark.new }.should raise_exception ArgumentError
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "the http request it makes" do
|
11
|
+
it "GETs the url with the trkref and sku" do
|
12
|
+
stub_request(:get, "http://example.com/foo?sku=SKU123&retailer=PNY").to_return(:body => "")
|
13
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")
|
14
|
+
WebMock.should have_requested(:get, "http://example.com/foo?sku=SKU123&retailer=PNY")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "copes fine with urls that already have query strings" do
|
18
|
+
stub_request(:get, "http://example.com/foo?bar=baz&sku=SKU123&retailer=PNY").to_return(:body => "")
|
19
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo?bar=baz", "PNY", "SKU123")
|
20
|
+
WebMock.should have_requested(:get, "http://example.com/foo?bar=baz&sku=SKU123&retailer=PNY")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "passes the correct headers in the request" do
|
24
|
+
stub_request(:get, /.*example.*/).to_return(:body => "")
|
25
|
+
expected_headers_hash = {
|
26
|
+
'User-Agent' => "ReevooMark Ruby Widget/#{ReevooMark::VERSION}",
|
27
|
+
'Referer' => "http://#{Socket::gethostname}"
|
28
|
+
}
|
29
|
+
|
30
|
+
ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123")
|
31
|
+
WebMock.should have_requested(:get, /.*example.*/).with(:headers => expected_headers_hash)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with a new ReevooMark instance" do
|
37
|
+
before do
|
38
|
+
stub_request(:get, /.*example.*/).to_return(
|
39
|
+
:headers => {
|
40
|
+
"X-Reevoo-ReviewCount" => 12,
|
41
|
+
"X-Reevoo-OfferCount" => 9,
|
42
|
+
"X-Reevoo-ConversationCount" => 165,
|
43
|
+
"X-Reevoo-BestPrice" => 19986
|
44
|
+
},
|
45
|
+
:body => "test"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
subject { ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123") }
|
49
|
+
|
50
|
+
it "parses the body" do
|
51
|
+
subject.render.should == "test"
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'parses the headers' do
|
55
|
+
subject.review_count.should == 12
|
56
|
+
subject.offer_count.should == 9
|
57
|
+
subject.conversation_count.should == 165
|
58
|
+
subject.best_price.should == 19986
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with a ReevooMark instance that failed to load due to server error" do
|
63
|
+
|
64
|
+
before do
|
65
|
+
stub_request(:get, /.*example.*/).to_return(:body => "Some sort of server error", :status => 500)
|
66
|
+
end
|
67
|
+
subject { ReevooMark.new("tmp/cache/", "http://example.com/foo", "PNY", "SKU123") }
|
68
|
+
|
69
|
+
describe "#render" do
|
70
|
+
it "returns a blank string" do
|
71
|
+
subject.render.should == ""
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'returns zero for the counts' do
|
76
|
+
subject.review_count.should be_zero
|
77
|
+
subject.offer_count.should be_zero
|
78
|
+
subject.conversation_count.should be_zero
|
79
|
+
subject.best_price.should be_zero
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'pry'
|
5
|
+
require 'rspec'
|
6
|
+
require 'webmock/rspec'
|
7
|
+
require 'reevoomark'
|
8
|
+
|
9
|
+
# Dir["spec/support/**/*.rb"].each { |f| require File.expand_path(f) }
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before do
|
13
|
+
FileUtils.rm Dir.glob('tmp/cache/*')
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ReevooMark::Cache do
|
4
|
+
subject{ ReevooMark::Cache.new("tmp/cache") }
|
5
|
+
let(:valid_doc){
|
6
|
+
ReevooMark::Document.new(Time.now.to_i, 100, 0, 200, "I am a document", {})
|
7
|
+
}
|
8
|
+
let(:expired_doc){
|
9
|
+
ReevooMark::Document.new(Time.now.to_i - 101, 100, 0, 200, "I am a document", {})
|
10
|
+
}
|
11
|
+
|
12
|
+
describe "#fetch" do
|
13
|
+
|
14
|
+
it "fetches valid documents" do
|
15
|
+
subject.fetch("foo"){ valid_doc } # Prime cache
|
16
|
+
doc = subject.fetch("foo"){ raise "Was not expecting to be run" }
|
17
|
+
doc.should == valid_doc
|
18
|
+
end
|
19
|
+
|
20
|
+
it "skips expired documents" do
|
21
|
+
subject.fetch("foo"){ expired_doc } # Prime cache
|
22
|
+
doc = subject.fetch("foo"){ valid_doc }
|
23
|
+
doc.should == valid_doc
|
24
|
+
doc = subject.fetch("foo"){ raise "Don't get here" }
|
25
|
+
doc.should == valid_doc
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "fetch_expired" do
|
31
|
+
it "returns even an expires document" do
|
32
|
+
subject.fetch("foo"){ expired_doc } # Prime cache
|
33
|
+
revalidated = subject.fetch_expired("foo", :revalidate_for => 30)
|
34
|
+
revalidated.should === expired_doc
|
35
|
+
revalidated.should_not be_expired
|
36
|
+
end
|
37
|
+
|
38
|
+
it "returns nil if nothing was found" do
|
39
|
+
subject.fetch_expired('bar', :revalidate_for => 30).should be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ReevooMark::Client do
|
4
|
+
let(:valid_doc){
|
5
|
+
ReevooMark::Document.new(Time.now.to_i, 100, 0, 200, "I am a document", {})
|
6
|
+
}
|
7
|
+
let(:invalid_doc){
|
8
|
+
ReevooMark::Document.new(Time.now.to_i, 100, 0, 500, "I am a ndnsment", {})
|
9
|
+
}
|
10
|
+
|
11
|
+
let(:cache){ double(:cache) }
|
12
|
+
let(:fetcher){ double(:fetcher) }
|
13
|
+
let(:url){ "http://example.com/foo?bar=bum" }
|
14
|
+
subject{
|
15
|
+
ReevooMark::Client.new(cache, fetcher, url)
|
16
|
+
}
|
17
|
+
|
18
|
+
describe "#fetch" do
|
19
|
+
it "fetches from the cache first" do
|
20
|
+
cache.should_receive(:fetch).with(
|
21
|
+
"http://example.com/foo?bar=bum&sku=123&retailer=TST"
|
22
|
+
)
|
23
|
+
|
24
|
+
subject.fetch("TST", "123")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "uses the remote_fetcher if the cache misses" do
|
28
|
+
def cache.fetch(remote_url, &block)
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
|
32
|
+
fetcher.should_receive(:fetch).and_return valid_doc
|
33
|
+
subject.fetch("TST", "123").should == valid_doc
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
it "falls back to an existing cached document if response is an error" do
|
38
|
+
def cache.fetch(remote_url, &block)
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
cache.should_receive(:fetch_expired).and_return(valid_doc)
|
42
|
+
fetcher.should_receive(:fetch).and_return invalid_doc
|
43
|
+
subject.fetch("TST", "123").should == valid_doc
|
44
|
+
end
|
45
|
+
|
46
|
+
it "if all we have is errors, let them eat errors" do
|
47
|
+
def cache.fetch(remote_url, &block)
|
48
|
+
yield
|
49
|
+
end
|
50
|
+
cache.should_receive(:fetch_expired).and_return(invalid_doc)
|
51
|
+
fetcher.should_receive(:fetch).and_return invalid_doc
|
52
|
+
subject.fetch("TST", "123").should == invalid_doc
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ReevooMark::Document do
|
4
|
+
describe "#any?" do
|
5
|
+
it "returns true iff the review_count > 0" do
|
6
|
+
doc = ReevooMark::Document.new(
|
7
|
+
time = double(),
|
8
|
+
max_age = double(),
|
9
|
+
age = double(),
|
10
|
+
status_code = double(),
|
11
|
+
body = double(),
|
12
|
+
counts = {:review_count => 4}
|
13
|
+
)
|
14
|
+
|
15
|
+
doc.any?.should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "returns false iff the review_count <= 0" do
|
19
|
+
doc = ReevooMark::Document.new(
|
20
|
+
time = double(),
|
21
|
+
max_age = double(),
|
22
|
+
age = double(),
|
23
|
+
status_code = double(),
|
24
|
+
body = double(),
|
25
|
+
counts = {:review_count => 0}
|
26
|
+
)
|
27
|
+
|
28
|
+
doc.any?.should be_false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "has some attributes it just parrots back" do
|
33
|
+
doc = ReevooMark::Document.new(
|
34
|
+
time = double(),
|
35
|
+
max_age = double(),
|
36
|
+
age = double(),
|
37
|
+
status_code = double(),
|
38
|
+
body = double(),
|
39
|
+
counts = double()
|
40
|
+
)
|
41
|
+
|
42
|
+
doc.time.should be time
|
43
|
+
doc.max_age.should be max_age
|
44
|
+
doc.age.should be age
|
45
|
+
doc.status_code.should be status_code
|
46
|
+
doc.counts.should be counts
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#body' do
|
50
|
+
it "returns blank if isn't valid" do
|
51
|
+
doc = ReevooMark::Document.new(
|
52
|
+
time = double(),
|
53
|
+
max_age = double(),
|
54
|
+
age = double(),
|
55
|
+
status_code = 500,
|
56
|
+
body = double(),
|
57
|
+
counts = double()
|
58
|
+
)
|
59
|
+
|
60
|
+
doc.body.should == ""
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns the body if it's valid" do
|
64
|
+
doc = ReevooMark::Document.new(
|
65
|
+
time = double(),
|
66
|
+
max_age = double(),
|
67
|
+
age = double(),
|
68
|
+
status_code = 200,
|
69
|
+
body = double(),
|
70
|
+
counts = double()
|
71
|
+
)
|
72
|
+
|
73
|
+
doc.body.should == body
|
74
|
+
|
75
|
+
doc = ReevooMark::Document.new(
|
76
|
+
time = double(),
|
77
|
+
max_age = double(),
|
78
|
+
age = double(),
|
79
|
+
status_code = 499,
|
80
|
+
body = double(),
|
81
|
+
counts = double()
|
82
|
+
)
|
83
|
+
|
84
|
+
doc.render.should == body
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with a documnt" do
|
88
|
+
let(:doc){
|
89
|
+
ReevooMark::Document.new(
|
90
|
+
time = 10,
|
91
|
+
max_age = 10,
|
92
|
+
age = 5,
|
93
|
+
nil, nil, nil
|
94
|
+
)
|
95
|
+
}
|
96
|
+
|
97
|
+
describe '#has_expired?' do
|
98
|
+
it "is expired after the correct number of secconds" do
|
99
|
+
doc.should_not be_expired(1)
|
100
|
+
doc.should_not be_expired(6)
|
101
|
+
doc.should_not be_expired(11)
|
102
|
+
doc.should be_expired(16)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#revalidated_for" do
|
107
|
+
it "creates a new document, valid for a given ammoiunt of time" do
|
108
|
+
doc.revalidated_for(1).should_not be_expired
|
109
|
+
doc.revalidated_for(-1).should be_expired
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ReevooMark::Fetcher do
|
4
|
+
describe "#fetch" do
|
5
|
+
|
6
|
+
it "responds with a 5xx when the server is slow" do
|
7
|
+
fetcher = ReevooMark::Fetcher.new(1)
|
8
|
+
Timeout.should_receive(:timeout).and_raise(Timeout::Error)
|
9
|
+
document = fetcher.fetch("http://example.com/foo")
|
10
|
+
document.status_code.should == 599
|
11
|
+
end
|
12
|
+
|
13
|
+
it "responds with a 5xx when the server is broken" do
|
14
|
+
fetcher = ReevooMark::Fetcher.new(1)
|
15
|
+
HTTPClient.any_instance.should_receive(:get).and_raise(RuntimeError)
|
16
|
+
document = fetcher.fetch("http://example.com/foo")
|
17
|
+
document.status_code.should == 599
|
18
|
+
end
|
19
|
+
|
20
|
+
it "responds with a document when all is fine" do
|
21
|
+
fetcher = ReevooMark::Fetcher.new(1)
|
22
|
+
stub_request(:get, /.*example.*/).to_return(:body => "", :status => 200)
|
23
|
+
document = fetcher.fetch("http://example.com/foo")
|
24
|
+
document.status_code.should == 200
|
25
|
+
end
|
26
|
+
|
27
|
+
it "responds with a document when there is a 404" do
|
28
|
+
fetcher = ReevooMark::Fetcher.new(1)
|
29
|
+
stub_request(:get, /.*example.*/).to_return(:body => "foo", :status => 404)
|
30
|
+
document = fetcher.fetch("http://example.com/foo")
|
31
|
+
document.status_code.should == 404
|
32
|
+
document.body.should == "foo"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reevoomark-ruby-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Reevoo Developers
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2012-06-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httpclient
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Reevoo's ReevooMark & Traffic server-side ruby implementation. This API
|
28
|
+
is free to use but requires you to be a Reevoo customer.
|
29
|
+
email:
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/reevoomark/cache/entry.rb
|
35
|
+
- lib/reevoomark/cache.rb
|
36
|
+
- lib/reevoomark/client.rb
|
37
|
+
- lib/reevoomark/document/factory.rb
|
38
|
+
- lib/reevoomark/document.rb
|
39
|
+
- lib/reevoomark/fetcher.rb
|
40
|
+
- lib/reevoomark/version.rb
|
41
|
+
- lib/reevoomark.rb
|
42
|
+
- spec/acceptance/reevoomark_caching_spec.rb
|
43
|
+
- spec/acceptance/reevoomark_spec.rb
|
44
|
+
- spec/spec_helper.rb
|
45
|
+
- spec/unit/cache_spec.rb
|
46
|
+
- spec/unit/client_spec.rb
|
47
|
+
- spec/unit/document_spec.rb
|
48
|
+
- spec/unit/fetcher_spec.rb
|
49
|
+
homepage: http://www.reevoo.com
|
50
|
+
licenses: []
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.0.3
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Implement ReevooMark on your ruby-based website.
|
72
|
+
test_files: []
|