roverjoe 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.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
|
+

|
|
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
|