reevoomark-ruby-api 2.0.0
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.
- 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: []
|