sucker 1.0.0.beta.2 → 1.0.0.beta.3
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/History.md +14 -0
- data/LICENSE +2 -2
- data/README.md +66 -0
- data/lib/sucker/request.rb +10 -2
- data/lib/sucker/response.rb +13 -6
- data/lib/sucker/version.rb +1 -1
- data/spec/integration/alternate_versions_spec.rb +27 -0
- data/spec/integration/errors_spec.rb +2 -2
- data/spec/integration/france_spec.rb +1 -1
- data/spec/integration/images_spec.rb +1 -1
- data/spec/integration/item_lookup_spec.rb +4 -4
- data/spec/integration/item_search_spec.rb +1 -1
- data/spec/integration/japan_spec.rb +1 -1
- data/spec/integration/keyword_search_spec.rb +28 -0
- data/spec/integration/multiple_locales_spec.rb +3 -6
- data/spec/integration/power_search_spec.rb +36 -0
- data/spec/integration/related_items_spec.rb +3 -3
- data/spec/integration/seller_listing_search_spec.rb +1 -1
- data/spec/integration/twenty_items_spec.rb +1 -1
- data/spec/unit/sucker/response_spec.rb +13 -4
- metadata +12 -5
- data/README.markdown +0 -69
data/History.md
ADDED
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
(The MIT License)
|
2
2
|
|
3
|
-
Copyright (c)
|
3
|
+
Copyright (c) 2010 Paper Cavalier
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
6
6
|
a copy of this software and associated documentation files (the
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
19
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
20
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
21
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
Sucker
|
2
|
+
======
|
3
|
+
|
4
|
+
Sucker is a Ruby wrapper to the [Amazon Product Advertising API](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html). It runs on [curb](http://github.com/taf2/curb) and [the Nokogiri implementation of the XML Mini module](http://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini/nokogiri.rb) in Active Support. It's fast and supports __the entire API__.
|
5
|
+
|
6
|
+

|
7
|
+
|
8
|
+
Examples
|
9
|
+
--------
|
10
|
+
|
11
|
+
Set up a worker.
|
12
|
+
|
13
|
+
worker = Sucker.new(
|
14
|
+
:locale => "us",
|
15
|
+
:key => "API KEY",
|
16
|
+
:secret => "API SECRET")
|
17
|
+
|
18
|
+
Prepare a request.
|
19
|
+
|
20
|
+
asin_batch = %w{
|
21
|
+
0816614024 0143105825 0485113600 0816616779 0942299078
|
22
|
+
0816614008 144006654X 0486400360 0486417670 087220474X }
|
23
|
+
worker << {
|
24
|
+
"Operation" => "ItemLookup",
|
25
|
+
"IdType" => "ASIN",
|
26
|
+
"ItemId" => asin_batch,
|
27
|
+
"ResponseGroup" => ["ItemAttributes", "OfferFull"] }
|
28
|
+
|
29
|
+
Perform the request.
|
30
|
+
|
31
|
+
response = worker.get
|
32
|
+
|
33
|
+
Debug.
|
34
|
+
|
35
|
+
p response.valid?,
|
36
|
+
response.code,
|
37
|
+
response.time,
|
38
|
+
response.body,
|
39
|
+
response.xml,
|
40
|
+
response.to_hash
|
41
|
+
|
42
|
+
Iterate over items in your item lookup.
|
43
|
+
|
44
|
+
response.find("Item") { |item| ... }
|
45
|
+
|
46
|
+
Iterate over errors.
|
47
|
+
|
48
|
+
response.find("Error") { |error| ... }
|
49
|
+
|
50
|
+
Repeat ad infinitum.
|
51
|
+
|
52
|
+
Check my [integration specs](http://github.com/papercavalier/sucker/tree/master/spec/integration/) for more examples. See [twenty items](http://github.com/papercavalier/sucker/tree/master/spec/integration/twenty_items_spec.rb) and [multiple locales](http://github.com/papercavalier/sucker/tree/master/spec/integration/multiple_locales_spec.rb) for relatively advanced usage.
|
53
|
+
|
54
|
+
Last but not least, dive into the [API docs](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html) to construct your own queries.
|
55
|
+
|
56
|
+
Stubbing
|
57
|
+
--------
|
58
|
+
|
59
|
+
Use [VCR](http://github.com/myronmarston/vcr) to stub your requests.
|
60
|
+
|
61
|
+
Caveat: Match URIs on host only and create a new cassette for each stubbed query. [This is how I do it](http://github.com/papercavalier/sucker/blob/master/spec/support/vcr.rb).
|
62
|
+
|
63
|
+
Compatibility
|
64
|
+
-------------
|
65
|
+
|
66
|
+
Specs pass against Ruby 1.8.7, Ruby 1.9.2, and Rubinius 1.1.
|
data/lib/sucker/request.rb
CHANGED
@@ -37,6 +37,14 @@ module Sucker #:nodoc
|
|
37
37
|
end
|
38
38
|
|
39
39
|
# A helper method that merges a hash into existing parameters
|
40
|
+
#
|
41
|
+
# worker = Sucker.new
|
42
|
+
# worker << {
|
43
|
+
# "Operation" => "ItemLookup",
|
44
|
+
# "IdType" => "ASIN",
|
45
|
+
# "ItemId" => "0816614024",
|
46
|
+
# "ResponseGroup" => "ItemAttributes" }
|
47
|
+
#
|
40
48
|
def <<(hash)
|
41
49
|
self.parameters.merge!(hash)
|
42
50
|
end
|
@@ -82,10 +90,10 @@ module Sucker #:nodoc
|
|
82
90
|
parameters["AWSAccessKeyId"] = token
|
83
91
|
end
|
84
92
|
|
85
|
-
# Sets Amazon API version
|
93
|
+
# Sets the Amazon API version
|
86
94
|
#
|
87
95
|
# worker = Sucker.new
|
88
|
-
# worker.version = '
|
96
|
+
# worker.version = '2010-06-01'
|
89
97
|
#
|
90
98
|
def version=(version)
|
91
99
|
self.parameters["Version"] = version
|
data/lib/sucker/response.rb
CHANGED
@@ -18,15 +18,22 @@ module Sucker #:nodoc
|
|
18
18
|
self.time = curl.total_time
|
19
19
|
end
|
20
20
|
|
21
|
-
# Queries an xpath and returns
|
21
|
+
# Queries an xpath and returns an array of matching nodes
|
22
22
|
#
|
23
|
-
#
|
23
|
+
# Will yield each match if a block is given
|
24
24
|
#
|
25
25
|
# response = worker.get
|
26
|
-
# response.
|
26
|
+
# response.find("Item") { |item| ... }
|
27
27
|
#
|
28
|
-
def
|
29
|
-
xml.xpath("//xmlns:#{path}").map { |node| strip_content(node.to_hash[path]) }
|
28
|
+
def find(path)
|
29
|
+
node = xml.xpath("//xmlns:#{path}").map { |node| strip_content(node.to_hash[path]) }
|
30
|
+
node.each { |e| yield e } if block_given?
|
31
|
+
node
|
32
|
+
end
|
33
|
+
|
34
|
+
def node(path) # :nodoc:
|
35
|
+
warn "[DEPRECATION] `node` is deprecated. Use `find` instead."
|
36
|
+
find(path)
|
30
37
|
end
|
31
38
|
|
32
39
|
# Parses the response into a simple hash
|
@@ -58,7 +65,7 @@ module Sucker #:nodoc
|
|
58
65
|
|
59
66
|
private
|
60
67
|
|
61
|
-
# Let's massage
|
68
|
+
# Let's massage that hash
|
62
69
|
def strip_content(node)
|
63
70
|
case node
|
64
71
|
when Array
|
data/lib/sucker/version.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Sucker
|
4
|
+
describe "Alternate versions" do
|
5
|
+
use_vcr_cassette "integration/alternate_versions", :record => :new_episodes
|
6
|
+
|
7
|
+
let(:worker) do
|
8
|
+
worker = Sucker.new(
|
9
|
+
:locale => "uk",
|
10
|
+
:key => amazon["key"],
|
11
|
+
:secret => amazon["secret"])
|
12
|
+
worker << {
|
13
|
+
"Operation" => "ItemLookup",
|
14
|
+
"IdType" => "ASIN",
|
15
|
+
"ResponseGroup" => "AlternateVersions",
|
16
|
+
"ItemId" => "0141044012" }
|
17
|
+
worker
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns alternate versions" do
|
21
|
+
response = worker.get
|
22
|
+
alternate_versions = response.find("AlternateVersion")
|
23
|
+
alternate_versions.should be_an_instance_of Array
|
24
|
+
alternate_versions.first.should have_key "ASIN"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -23,13 +23,13 @@ module Sucker
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it "returns two errors" do
|
26
|
-
errors = response.
|
26
|
+
errors = response.find("Error")
|
27
27
|
errors.size.should eql 2
|
28
28
|
errors.first["Message"].should include "not a valid value"
|
29
29
|
end
|
30
30
|
|
31
31
|
it "returns one item" do
|
32
|
-
items = response.
|
32
|
+
items = response.find("ItemAttributes")
|
33
33
|
items.size.should eql 1
|
34
34
|
end
|
35
35
|
end
|
@@ -25,7 +25,7 @@ module Sucker
|
|
25
25
|
worker.get
|
26
26
|
end
|
27
27
|
|
28
|
-
let(:item) { response.
|
28
|
+
let(:item) { response.find("Item").first }
|
29
29
|
|
30
30
|
it "returns an item" do
|
31
31
|
item.should be_an_instance_of Hash
|
@@ -41,8 +41,8 @@ module Sucker
|
|
41
41
|
end
|
42
42
|
|
43
43
|
it "returns no errors" do
|
44
|
-
response.
|
45
|
-
response.
|
44
|
+
response.find("Error").should be_empty
|
45
|
+
response.find("Error").should be_an_instance_of Array
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -51,7 +51,7 @@ module Sucker
|
|
51
51
|
|
52
52
|
let(:items) do
|
53
53
|
worker << { "ItemId" => ["0816614024", "0143105825"] }
|
54
|
-
worker.get.
|
54
|
+
worker.get.find("Item")
|
55
55
|
end
|
56
56
|
|
57
57
|
it "returns two items" do
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Sucker
|
4
|
+
describe "Keyword Search" do
|
5
|
+
use_vcr_cassette "integration/keyword_search", :record => :new_episodes
|
6
|
+
|
7
|
+
let(:worker) do
|
8
|
+
worker = Sucker.new(
|
9
|
+
:locale => "us",
|
10
|
+
:key => amazon["key"],
|
11
|
+
:secret => amazon["secret"])
|
12
|
+
worker << {
|
13
|
+
"Operation" => "ItemSearch",
|
14
|
+
"SearchIndex" => "Books",
|
15
|
+
"Keywords" => "zizek lacan",
|
16
|
+
"Sort" => "relevancerank" } # also try salesrank and reviewrank
|
17
|
+
worker
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns matches" do
|
21
|
+
items = worker.get.find("Item")
|
22
|
+
items.size.should > 0
|
23
|
+
items.each do |item|
|
24
|
+
item["ItemAttributes"]["Title"].should_not be_nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -11,8 +11,6 @@ module Sucker
|
|
11
11
|
params = {
|
12
12
|
"Operation" => "ItemLookup",
|
13
13
|
"IdType" => "ASIN",
|
14
|
-
"Condition" => "All",
|
15
|
-
"MerchantId" => "All",
|
16
14
|
"ResponseGroup" => "ItemAttributes",
|
17
15
|
"ItemId" => "0816614024" }
|
18
16
|
|
@@ -27,11 +25,10 @@ module Sucker
|
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
30
|
-
bindings =
|
31
|
-
threads.each do |thread|
|
28
|
+
bindings = threads.map do |thread|
|
32
29
|
thread.join
|
33
|
-
item = thread[:response].
|
34
|
-
|
30
|
+
item = thread[:response].find("Item").first
|
31
|
+
item["ItemAttributes"]["Binding"]
|
35
32
|
end
|
36
33
|
|
37
34
|
bindings.uniq.should =~ %w{ Paperback Taschenbuch Broché ペーパーバック }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# http://docs.amazonwebservices.com/AWSECommerceService/2010-09-01/DG/index.html?PowerSearchSyntax.html
|
4
|
+
#
|
5
|
+
# author: ambrose and binding: (abridged or large print) and pubdate: after 11-1996
|
6
|
+
# subject: history and (Spain or Mexico) and not military and language: Spanish
|
7
|
+
# (subject: marketing and author: kotler) or (publisher: harper and subject: "high technology")
|
8
|
+
# keywords: "high tech*" and not fiction and pubdate: during 1999
|
9
|
+
# isbn: 0446394319 or 0306806819 or 1567993850
|
10
|
+
|
11
|
+
module Sucker
|
12
|
+
describe "Power search" do
|
13
|
+
use_vcr_cassette "integration/power_search", :record => :new_episodes
|
14
|
+
|
15
|
+
let(:worker) do
|
16
|
+
worker = Sucker.new(
|
17
|
+
:locale => "us",
|
18
|
+
:key => amazon["key"],
|
19
|
+
:secret => amazon["secret"])
|
20
|
+
worker << {
|
21
|
+
"Operation" => "ItemSearch",
|
22
|
+
"SearchIndex" => "Books",
|
23
|
+
"Power" => "author:lacan or deleuze and not fiction",
|
24
|
+
"Sort" => "relevancerank" }
|
25
|
+
worker
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns matches" do
|
29
|
+
items = worker.get.find("Item")
|
30
|
+
items.size.should > 0
|
31
|
+
items.each do |item|
|
32
|
+
item["ItemAttributes"]["Title"].should_not be_nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -23,8 +23,8 @@ module Sucker
|
|
23
23
|
it "finds parent and related items" do
|
24
24
|
worker << { "ItemId" => "0415246334" }
|
25
25
|
response = worker.get
|
26
|
-
response.
|
27
|
-
parent_asin = response.
|
26
|
+
response.find("RelatedItem").size.should eql 1
|
27
|
+
parent_asin = response.find("RelatedItem").first["Item"]["ASIN"]
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -34,7 +34,7 @@ module Sucker
|
|
34
34
|
it "finds related items" do
|
35
35
|
worker << { "ItemId" => "B000ASPUES" }
|
36
36
|
response = worker.get
|
37
|
-
response.
|
37
|
+
response.find("RelatedItem").size.should > 1
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -40,16 +40,25 @@ module Sucker
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
context "#
|
44
|
-
it "returns
|
45
|
-
node = response.
|
43
|
+
context "#find" do
|
44
|
+
it "returns an array of matching nodes" do
|
45
|
+
node = response.find("ItemAttributes")
|
46
46
|
node.map { |book| book["ISBN"] }.should eql asins
|
47
47
|
end
|
48
48
|
|
49
49
|
it "returns an empty array if there are no matches" do
|
50
|
-
node = response.
|
50
|
+
node = response.find("Foo")
|
51
51
|
node.should eql []
|
52
52
|
end
|
53
|
+
|
54
|
+
it "yields to a block if given one" do
|
55
|
+
has_yielded = false
|
56
|
+
response.find("ItemAttributes") do |item|
|
57
|
+
has_yielded = true
|
58
|
+
item.should be_an_instance_of Hash
|
59
|
+
end
|
60
|
+
has_yielded.should be_true
|
61
|
+
end
|
53
62
|
end
|
54
63
|
|
55
64
|
context "#to_hash" do
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sucker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 62196357
|
5
5
|
prerelease: true
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 0
|
9
9
|
- 0
|
10
10
|
- beta
|
11
|
-
-
|
12
|
-
version: 1.0.0.beta.
|
11
|
+
- 3
|
12
|
+
version: 1.0.0.beta.3
|
13
13
|
platform: ruby
|
14
14
|
authors:
|
15
15
|
- Hakan Ensari
|
@@ -18,7 +18,7 @@ autorequire:
|
|
18
18
|
bindir: bin
|
19
19
|
cert_chain: []
|
20
20
|
|
21
|
-
date: 2010-10-
|
21
|
+
date: 2010-10-25 00:00:00 +01:00
|
22
22
|
default_executable:
|
23
23
|
dependencies:
|
24
24
|
- !ruby/object:Gem::Dependency
|
@@ -196,15 +196,19 @@ files:
|
|
196
196
|
- lib/sucker/version.rb
|
197
197
|
- lib/sucker.rb
|
198
198
|
- LICENSE
|
199
|
-
- README.
|
199
|
+
- README.md
|
200
|
+
- History.md
|
200
201
|
- spec/fixtures/asins.txt
|
202
|
+
- spec/integration/alternate_versions_spec.rb
|
201
203
|
- spec/integration/errors_spec.rb
|
202
204
|
- spec/integration/france_spec.rb
|
203
205
|
- spec/integration/images_spec.rb
|
204
206
|
- spec/integration/item_lookup_spec.rb
|
205
207
|
- spec/integration/item_search_spec.rb
|
206
208
|
- spec/integration/japan_spec.rb
|
209
|
+
- spec/integration/keyword_search_spec.rb
|
207
210
|
- spec/integration/multiple_locales_spec.rb
|
211
|
+
- spec/integration/power_search_spec.rb
|
208
212
|
- spec/integration/related_items_spec.rb
|
209
213
|
- spec/integration/seller_listing_search_spec.rb
|
210
214
|
- spec/integration/twenty_items_spec.rb
|
@@ -254,13 +258,16 @@ specification_version: 3
|
|
254
258
|
summary: A Ruby wrapper to the Amazon Product Advertising API
|
255
259
|
test_files:
|
256
260
|
- spec/fixtures/asins.txt
|
261
|
+
- spec/integration/alternate_versions_spec.rb
|
257
262
|
- spec/integration/errors_spec.rb
|
258
263
|
- spec/integration/france_spec.rb
|
259
264
|
- spec/integration/images_spec.rb
|
260
265
|
- spec/integration/item_lookup_spec.rb
|
261
266
|
- spec/integration/item_search_spec.rb
|
262
267
|
- spec/integration/japan_spec.rb
|
268
|
+
- spec/integration/keyword_search_spec.rb
|
263
269
|
- spec/integration/multiple_locales_spec.rb
|
270
|
+
- spec/integration/power_search_spec.rb
|
264
271
|
- spec/integration/related_items_spec.rb
|
265
272
|
- spec/integration/seller_listing_search_spec.rb
|
266
273
|
- spec/integration/twenty_items_spec.rb
|
data/README.markdown
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
Sucker
|
2
|
-
======
|
3
|
-
|
4
|
-
Sucker is a Ruby wrapper to the [Amazon Product Advertising API](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html). It runs on [curb](http://github.com/taf2/curb) and the Nokogiri implementation of the XML Mini module in Active Support. It is blazing fast and supports __the entire API__.
|
5
|
-
|
6
|
-

|
7
|
-
|
8
|
-
Examples
|
9
|
-
--------
|
10
|
-
|
11
|
-
Set up a worker.
|
12
|
-
|
13
|
-
worker = Sucker.new(
|
14
|
-
:locale => "us",
|
15
|
-
:key => "API KEY",
|
16
|
-
:secret => "API SECRET")
|
17
|
-
|
18
|
-
Prep a request.
|
19
|
-
|
20
|
-
asin_batch = %w{
|
21
|
-
0816614024 0143105825 0485113600 0816616779 0942299078
|
22
|
-
0816614008 144006654X 0486400360 0486417670 087220474X }
|
23
|
-
worker << {
|
24
|
-
"Operation" => "ItemLookup",
|
25
|
-
"IdType" => "ASIN",
|
26
|
-
"ItemId" => asin_batch,
|
27
|
-
"ResponseGroup" => ["ItemAttributes", "OfferFull"] }
|
28
|
-
|
29
|
-
Perform the request.
|
30
|
-
|
31
|
-
response = worker.get
|
32
|
-
|
33
|
-
Debug.
|
34
|
-
|
35
|
-
if response.valid?
|
36
|
-
p response.code,
|
37
|
-
response.time,
|
38
|
-
response.body,
|
39
|
-
response.xml,
|
40
|
-
response.to_hash
|
41
|
-
end
|
42
|
-
|
43
|
-
Say you performed an item lookup. Iterate over all items and errors.
|
44
|
-
|
45
|
-
response.node("Item").each { |item| ... }
|
46
|
-
response.node("Error").each { |error| ... }
|
47
|
-
|
48
|
-
Perform a new request in a more DSL-y way.
|
49
|
-
|
50
|
-
worker << { "ItemId" => "0486454398" }
|
51
|
-
worker.get.node("Item").each { |item| ... }
|
52
|
-
|
53
|
-
Repeat ad infinitum.
|
54
|
-
|
55
|
-
Check the [integration specs](http://github.com/papercavalier/sucker/tree/master/spec/integration/) for more examples. In particullar, see [twenty items](http://github.com/papercavalier/sucker/tree/master/spec/integration/twenty_items_spec.rb) and [multiple locales](http://github.com/papercavalier/sucker/tree/master/spec/integration/multiple_locales_spec.rb) for advanced usage.
|
56
|
-
|
57
|
-
Finally, dive into the [API docs](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html) to construct your own queries.
|
58
|
-
|
59
|
-
Stubbing
|
60
|
-
--------
|
61
|
-
|
62
|
-
Use [VCR](http://github.com/myronmarston/vcr) to stub your requests. [This file](http://github.com/papercavalier/sucker/blob/master/spec/support/vcr.rb) should help you set up VCR with RSpec.
|
63
|
-
|
64
|
-
Don't match requests on URI. The parameters include a timestamp that results in each URI being unique.
|
65
|
-
|
66
|
-
Compatibility
|
67
|
-
-------------
|
68
|
-
|
69
|
-
Specs pass against Ruby 1.8.7, Ruby 1.9.2, and Rubinius 1.1.
|