roverjoe 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +65 -0
- data/Guardfile +14 -0
- data/README.md +102 -0
- data/Rakefile +7 -0
- data/lib/roverjoe.rb +51 -0
- data/lib/roverjoe/configuration.rb +51 -0
- data/lib/roverjoe/null_logger.rb +11 -0
- data/lib/roverjoe/quota_handler.rb +64 -0
- data/lib/roverjoe/remote_properties_page.rb +45 -0
- data/lib/roverjoe/remote_property.rb +28 -0
- data/lib/roverjoe/request.rb +60 -0
- data/lib/roverjoe/response.rb +64 -0
- data/lib/roverjoe/retry_handler.rb +56 -0
- data/lib/roverjoe/version.rb +3 -0
- data/roverjoe.gemspec +32 -0
- data/spec/fixtures/configurations/consumer_key_only.yml +2 -0
- data/spec/fixtures/configurations/consumer_signature_only.yml +2 -0
- data/spec/fixtures/hostelworld_api/real_propertyinformation_json.txt +1 -0
- data/spec/integration/configuration_spec.rb +35 -0
- data/spec/integration/star_ratings_spec.rb +61 -0
- data/spec/lib/roverjoe/configuration_spec.rb +55 -0
- data/spec/lib/roverjoe/quota_handler_spec.rb +48 -0
- data/spec/lib/roverjoe/remote_properties_page_spec.rb +90 -0
- data/spec/lib/roverjoe/remote_property_spec.rb +25 -0
- data/spec/lib/roverjoe/request_spec.rb +67 -0
- data/spec/lib/roverjoe/response_spec.rb +56 -0
- data/spec/lib/roverjoe/retry_handler_spec.rb +51 -0
- data/spec/lib/roverjoe_spec.rb +93 -0
- data/spec/spec_helper.rb +30 -0
- metadata +179 -0
data/.gitignore
ADDED
data/.rvmrc.example
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.3@roverjoe
|
data/Gemfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
gem 'addressable'
|
4
|
+
gem 'httparty'
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'rake'
|
8
|
+
end
|
9
|
+
|
10
|
+
group :test do
|
11
|
+
gem 'rspec'
|
12
|
+
gem 'guard'
|
13
|
+
gem 'guard-rspec'
|
14
|
+
gem 'guard-cucumber'
|
15
|
+
gem 'webmock'
|
16
|
+
end
|
17
|
+
|
18
|
+
group :cucumber do
|
19
|
+
gem 'cucumber'
|
20
|
+
gem 'rspec'
|
21
|
+
gem 'guard'
|
22
|
+
gem 'guard-rspec'
|
23
|
+
gem 'guard-cucumber'
|
24
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.2.8)
|
5
|
+
builder (3.0.0)
|
6
|
+
crack (0.3.1)
|
7
|
+
cucumber (1.2.1)
|
8
|
+
builder (>= 2.1.2)
|
9
|
+
diff-lcs (>= 1.1.3)
|
10
|
+
gherkin (~> 2.11.0)
|
11
|
+
json (>= 1.4.6)
|
12
|
+
diff-lcs (1.1.3)
|
13
|
+
ffi (1.0.11)
|
14
|
+
gherkin (2.11.0)
|
15
|
+
json (>= 1.4.6)
|
16
|
+
guard (1.1.1)
|
17
|
+
listen (>= 0.4.2)
|
18
|
+
thor (>= 0.14.6)
|
19
|
+
guard-cucumber (1.1.0)
|
20
|
+
cucumber (>= 1.2.0)
|
21
|
+
guard (>= 1.1.0)
|
22
|
+
guard-rspec (1.1.0)
|
23
|
+
guard (>= 1.1)
|
24
|
+
httparty (0.8.3)
|
25
|
+
multi_json (~> 1.0)
|
26
|
+
multi_xml
|
27
|
+
json (1.7.3)
|
28
|
+
listen (0.4.5)
|
29
|
+
rb-fchange (~> 0.0.5)
|
30
|
+
rb-fsevent (~> 0.9.1)
|
31
|
+
rb-inotify (~> 0.8.8)
|
32
|
+
multi_json (1.3.6)
|
33
|
+
multi_xml (0.5.1)
|
34
|
+
rake (0.9.2.2)
|
35
|
+
rb-fchange (0.0.5)
|
36
|
+
ffi
|
37
|
+
rb-fsevent (0.9.1)
|
38
|
+
rb-inotify (0.8.8)
|
39
|
+
ffi (>= 0.5.0)
|
40
|
+
rspec (2.10.0)
|
41
|
+
rspec-core (~> 2.10.0)
|
42
|
+
rspec-expectations (~> 2.10.0)
|
43
|
+
rspec-mocks (~> 2.10.0)
|
44
|
+
rspec-core (2.10.1)
|
45
|
+
rspec-expectations (2.10.0)
|
46
|
+
diff-lcs (~> 1.1.3)
|
47
|
+
rspec-mocks (2.10.1)
|
48
|
+
thor (0.15.3)
|
49
|
+
webmock (1.8.7)
|
50
|
+
addressable (>= 2.2.7)
|
51
|
+
crack (>= 0.1.7)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
addressable
|
58
|
+
cucumber
|
59
|
+
guard
|
60
|
+
guard-cucumber
|
61
|
+
guard-rspec
|
62
|
+
httparty
|
63
|
+
rake
|
64
|
+
rspec
|
65
|
+
webmock
|
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2, :cli => '--color --format nested' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
10
|
+
guard 'cucumber' do
|
11
|
+
watch(%r{^features/.+\.feature$})
|
12
|
+
watch(%r{^features/support/.+$}) { 'features' }
|
13
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
|
14
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# RoverJoe
|
2
|
+
|
3
|
+
![RoverJoe](http://images.wikia.com/puppet/images/3/35/Muppetbremen6.gif)
|
4
|
+
|
5
|
+
RoverJoe: A ruby gem that wraps the HostelWorld API, providing:
|
6
|
+
|
7
|
+
* Recovery from network issuses like timeout and connection reset by
|
8
|
+
peer.
|
9
|
+
* Recovery from quota exceeded conditions
|
10
|
+
* Request and Response objects
|
11
|
+
* Objects for RemoteProperty (work-in-progress) ...
|
12
|
+
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Assuming bundler, edit Gemfile to include
|
17
|
+
|
18
|
+
gem 'roverjoe', '>=0.0.7', git: 'git@github.com:lonelyplanet/roverjoe.git'
|
19
|
+
|
20
|
+
## Use
|
21
|
+
|
22
|
+
### Configuration by file
|
23
|
+
|
24
|
+
Include in your initialization
|
25
|
+
|
26
|
+
RoverJoe.configure_from_file
|
27
|
+
|
28
|
+
Then create config/hostelworld\_api.yml something like this.
|
29
|
+
|
30
|
+
params:
|
31
|
+
format: 'json'
|
32
|
+
consumer_key: 'lonelyplanet.com'
|
33
|
+
consumer_signature: 'somethingverysecret'
|
34
|
+
Language: 'English'
|
35
|
+
uri: 'https://affiliate.xsapi.webresint.com/1.1/'
|
36
|
+
|
37
|
+
Only consumer\_key and consumer\_signature are required, the rest have
|
38
|
+
defaults.
|
39
|
+
|
40
|
+
### Hardcoded configuration
|
41
|
+
|
42
|
+
RoverJoe.configure do |c|
|
43
|
+
c.consumer_key = 'lonelyplanet.com'
|
44
|
+
c.consumer_signature = 'asecretsignature'
|
45
|
+
c.logger = MyApp.logger
|
46
|
+
end
|
47
|
+
|
48
|
+
The default logger does nothing, override if you want to see log output.
|
49
|
+
|
50
|
+
### Logging quota delay stats
|
51
|
+
|
52
|
+
Use the after\_delay callback to capture the stats however you want.
|
53
|
+
|
54
|
+
RoverJoe.configure do |c|
|
55
|
+
c.after_delay { |delay| Stats.capture( "the delay was #{delay}" }
|
56
|
+
end
|
57
|
+
|
58
|
+
### Calling the API - RemoteProperty
|
59
|
+
|
60
|
+
rp = RoverJoe::RemoteProperty( :id => hostel_number )
|
61
|
+
star_rating = rp.star_rating # this triggers API call
|
62
|
+
|
63
|
+
or
|
64
|
+
|
65
|
+
hw_property = RoverJoe::RemoteProperty( :id => hostel_number ).response
|
66
|
+
lp_property.name = hw_property['PropertyName']
|
67
|
+
|
68
|
+
### Calling the API - Request
|
69
|
+
|
70
|
+
req = RoverJoe::Request.new( :propertyInformation, :propertyNumber => hostel_number )
|
71
|
+
result = req.execute
|
72
|
+
|
73
|
+
Result is a hash of all the attributes of the property
|
74
|
+
|
75
|
+
### Calling the API - Availability/Location search example
|
76
|
+
|
77
|
+
a = RoverJoe::Request.new( :propertylocationsearch,
|
78
|
+
:City => 'Athens',
|
79
|
+
:DateStart => (Date.today+21).iso8601,
|
80
|
+
:NumNights => 7
|
81
|
+
).execute
|
82
|
+
|
83
|
+
## Development
|
84
|
+
|
85
|
+
$ git clone git@github.com:lonelyplanet/roverjoe.git
|
86
|
+
$ cd roverjoe
|
87
|
+
|
88
|
+
### RVM
|
89
|
+
|
90
|
+
$ cp .rvmrc.example .rvmrc
|
91
|
+
$ source .rvmrc
|
92
|
+
$ bundle install
|
93
|
+
|
94
|
+
### Specs
|
95
|
+
|
96
|
+
$ rake # all tests
|
97
|
+
$ rspec # all tests
|
98
|
+
$ bundle exec guard # all tests
|
99
|
+
|
100
|
+
$ rspec spec/lib # unit tests
|
101
|
+
$ rspec spec/integration # integration tests
|
102
|
+
|
data/Rakefile
ADDED
data/lib/roverjoe.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
require_relative "roverjoe/remote_property"
|
4
|
+
require_relative "roverjoe/remote_properties_page"
|
5
|
+
require_relative "roverjoe/request"
|
6
|
+
require_relative "roverjoe/response"
|
7
|
+
require_relative "roverjoe/quota_handler"
|
8
|
+
require_relative "roverjoe/retry_handler"
|
9
|
+
require_relative "roverjoe/configuration"
|
10
|
+
require_relative "roverjoe/null_logger"
|
11
|
+
|
12
|
+
|
13
|
+
module RoverJoe
|
14
|
+
|
15
|
+
class PropertyInactiveError < RuntimeError ; end
|
16
|
+
class ExcessiveRetries < RuntimeError ; end
|
17
|
+
|
18
|
+
def self.root
|
19
|
+
FileUtils.pwd
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.logger
|
23
|
+
configuration.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.configure_from_file( file = config_file )
|
27
|
+
reset_configuration
|
28
|
+
y = YAML.load_file( file )
|
29
|
+
configure do |c|
|
30
|
+
c.request_params = y['params']
|
31
|
+
c.request_uri = y['uri']
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.config_file
|
36
|
+
File.join(RoverJoe.root, 'config', 'hostelworld_api.yml')
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.reset_configuration
|
40
|
+
@configuration = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.configuration
|
44
|
+
@configuration ||= Configuration.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.configure
|
48
|
+
yield configuration
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module RoverJoe
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
attr_writer :logger, :request_uri, :consumer_signature, :consumer_key
|
6
|
+
attr_accessor :after_delay_callback
|
7
|
+
|
8
|
+
def logger
|
9
|
+
@logger ||= NullLogger.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# HACK: If XML format is required, it may involve more changes to the gem
|
13
|
+
# than simply overriding the request format from JSON to XML.
|
14
|
+
def default_request_params
|
15
|
+
{
|
16
|
+
'consumer_signature' => consumer_signature,
|
17
|
+
'consumer_key' => consumer_key,
|
18
|
+
'format' => 'json',
|
19
|
+
'Language' => 'English'
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def request_params
|
24
|
+
default_request_params.merge(@request_params ||= {})
|
25
|
+
end
|
26
|
+
|
27
|
+
def request_params=( params )
|
28
|
+
@request_params = params
|
29
|
+
self.consumer_key = params['consumer_key'] if params['consumer_key']
|
30
|
+
self.consumer_signature = params['consumer_signature'] if params['consumer_signature']
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_uri
|
34
|
+
@request_uri ||= 'https://affiliate.xsapi.webresint.com/1.1/'
|
35
|
+
end
|
36
|
+
|
37
|
+
def consumer_signature
|
38
|
+
@consumer_signature ||= 'you-forgot-to-configure-consumer-signature'
|
39
|
+
end
|
40
|
+
|
41
|
+
def consumer_key
|
42
|
+
@consumer_key ||= 'you-forgot-to-configure-consumer-key'
|
43
|
+
end
|
44
|
+
|
45
|
+
def after_delay( &callback )
|
46
|
+
self.after_delay_callback = callback if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module RoverJoe
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'addressable/uri'
|
5
|
+
|
6
|
+
|
7
|
+
class QuotaHandler
|
8
|
+
|
9
|
+
attr_accessor :waiting_last_time, :quota_delay_start
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.waiting_last_time = false
|
13
|
+
end
|
14
|
+
|
15
|
+
# HACK: The original intent was to limit requests to the
|
16
|
+
# hostelworld API, spreading them evenly over the quota window (60
|
17
|
+
# secs) thus being nice to other callers of the API with the same
|
18
|
+
# key. However, it's thought at present that there will only be
|
19
|
+
# one caller for any given API call, and so the actual throttling
|
20
|
+
# is not necessary and not implemented. Thus, all this method does
|
21
|
+
# is allow the calling code to hammer the API until it signals
|
22
|
+
# 'quota exceeded' after which it will keep retrying until the API
|
23
|
+
# becomes available again.
|
24
|
+
#
|
25
|
+
# If we do implement limiting or throttling it may belong in a
|
26
|
+
# different class.
|
27
|
+
def run
|
28
|
+
response = yield
|
29
|
+
|
30
|
+
retry_count = 0
|
31
|
+
while response.quota_exceeded? && retry_count < 200
|
32
|
+
|
33
|
+
log_quota_wait( true )
|
34
|
+
retry_count += 1
|
35
|
+
sleep 2
|
36
|
+
response = yield
|
37
|
+
|
38
|
+
end
|
39
|
+
log_quota_wait( response.quota_exceeded? )
|
40
|
+
raise RuntimeError, "Hostelworld API: Excessive wait for new quota window to start" if response.quota_exceeded?
|
41
|
+
|
42
|
+
response
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def log_quota_wait( waiting_now )
|
48
|
+
if waiting_now != waiting_last_time
|
49
|
+
if waiting_now
|
50
|
+
self.quota_delay_start = Time.now
|
51
|
+
RoverJoe.logger.warn( "Hostelworld API: Quota exceeded delay begins" )
|
52
|
+
else
|
53
|
+
delay = Time.now - quota_delay_start
|
54
|
+
RoverJoe.logger.warn( "Hostelworld API: Quota exceeded delay ends. Total time: #{delay}" )
|
55
|
+
cb = RoverJoe.configuration.after_delay_callback
|
56
|
+
cb.call( delay ) unless cb.nil?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
self.waiting_last_time = waiting_now
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RoverJoe
|
2
|
+
|
3
|
+
class RemotePropertiesPage
|
4
|
+
|
5
|
+
attr_accessor :api_request_class, :page_number
|
6
|
+
|
7
|
+
def initialize( params )
|
8
|
+
@api_request_class = params.delete( :api_request_class ) || RoverJoe::Request
|
9
|
+
@page_number = params.delete( :page )
|
10
|
+
raise ArgumentError, ":page should not be blank" if @page_number.nil? or @page_number == ''
|
11
|
+
end
|
12
|
+
|
13
|
+
def response
|
14
|
+
@response ||= request.execute
|
15
|
+
end
|
16
|
+
|
17
|
+
def request
|
18
|
+
@request ||= api_request_class.new( :propertiesinformation, :PageNumber => self.page_number )
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all(&block)
|
22
|
+
page_1 = new(:page => 1)
|
23
|
+
block.call(page_1)
|
24
|
+
page_count = page_1.total_pages
|
25
|
+
if page_count > 1
|
26
|
+
(2..page_count).each do |n|
|
27
|
+
page = new(:page => n)
|
28
|
+
block.call(page)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def total_pages
|
34
|
+
raise RuntimeError, "'PagesCount' not found in #{response.keys} for RemotePropertiesPage #{page_number}." unless response.keys.include?( 'PagesCount' )
|
35
|
+
response['PagesCount']
|
36
|
+
end
|
37
|
+
|
38
|
+
def properties
|
39
|
+
raise RuntimeError, "'Properties' not found in #{response.keys} for RemotePropertiesPage #{page_number}." unless response.keys.include?( 'Properties' )
|
40
|
+
response['Properties']
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|