rthbound-suitcase 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/README.md +83 -0
- data/Rakefile +13 -0
- data/examples/hash_adapter.rb +15 -0
- data/examples/hotel_image_db.rb +24 -0
- data/examples/redis_adapter.rb +29 -0
- data/lib/suitcase.rb +16 -0
- data/lib/suitcase/car_rental.rb +57 -0
- data/lib/suitcase/codes.rb +5 -0
- data/lib/suitcase/configuration.rb +29 -0
- data/lib/suitcase/core_ext/string.rb +13 -0
- data/lib/suitcase/hotel.rb +355 -0
- data/lib/suitcase/hotel/amenity.rb +46 -0
- data/lib/suitcase/hotel/bed_type.rb +21 -0
- data/lib/suitcase/hotel/cache.rb +52 -0
- data/lib/suitcase/hotel/ean_exception.rb +35 -0
- data/lib/suitcase/hotel/helpers.rb +198 -0
- data/lib/suitcase/hotel/image.rb +19 -0
- data/lib/suitcase/hotel/location.rb +67 -0
- data/lib/suitcase/hotel/nightly_rate.rb +14 -0
- data/lib/suitcase/hotel/payment_option.rb +41 -0
- data/lib/suitcase/hotel/reservation.rb +15 -0
- data/lib/suitcase/hotel/room.rb +138 -0
- data/lib/suitcase/hotel/session.rb +7 -0
- data/lib/suitcase/hotel/surcharge.rb +23 -0
- data/lib/suitcase/version.rb +3 -0
- data/rthbound-suitcase.gemspec +30 -0
- data/test/car_rentals/car_rental_test.rb +30 -0
- data/test/hotels/amenity_test.rb +23 -0
- data/test/hotels/caching_test.rb +42 -0
- data/test/hotels/ean_exception_test.rb +24 -0
- data/test/hotels/helpers_test.rb +52 -0
- data/test/hotels/hotel_location_test.rb +23 -0
- data/test/hotels/hotel_test.rb +112 -0
- data/test/hotels/image_test.rb +20 -0
- data/test/hotels/payment_option_test.rb +15 -0
- data/test/hotels/reservation_test.rb +15 -0
- data/test/hotels/room_test.rb +50 -0
- data/test/hotels/session_test.rb +14 -0
- data/test/keys.rb +43 -0
- data/test/minitest_helper.rb +29 -0
- data/test/support/fake_response.rb +13 -0
- metadata +220 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class Amenity
|
4
|
+
attr_accessor :id, :description
|
5
|
+
|
6
|
+
BITS = { business_services: 1,
|
7
|
+
fitness_center: 2,
|
8
|
+
hot_tub: 4,
|
9
|
+
internet_access: 8,
|
10
|
+
kids_activities: 16,
|
11
|
+
kitchen: 32,
|
12
|
+
pets_allowed: 64,
|
13
|
+
swimming_pool: 128,
|
14
|
+
restaurant: 256,
|
15
|
+
whirlpool_bath: 1024,
|
16
|
+
breakfast: 2048,
|
17
|
+
babysitting: 4096,
|
18
|
+
jacuzzi: 8192,
|
19
|
+
parking: 16384,
|
20
|
+
room_service: 32768,
|
21
|
+
accessible_path: 65536,
|
22
|
+
accessible_bathroom: 131072,
|
23
|
+
roll_in_shower: 262144,
|
24
|
+
handicapped_parking: 524288,
|
25
|
+
in_room_accessibility: 1048576,
|
26
|
+
deaf_accessiblity: 2097152,
|
27
|
+
braille_or_signage: 4194304,
|
28
|
+
free_airport_shuttle: 8388608,
|
29
|
+
indoor_pool: 16777216,
|
30
|
+
outdoor_pool: 33554432,
|
31
|
+
extended_parking: 67108864,
|
32
|
+
free_parking: 134217728
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(info)
|
36
|
+
@id, @description = info[:id], info[:description]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_mask(bitmask)
|
40
|
+
return nil unless bitmask
|
41
|
+
|
42
|
+
BITS.select { |amenity, bit| (bitmask & bit) > 0 }.keys
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
# Public: A BedType represents a bed configuration for a Room.
|
4
|
+
class BedType
|
5
|
+
# Internal: The ID of the BedType.
|
6
|
+
attr_accessor :id
|
7
|
+
|
8
|
+
# Internal: The description of the BedType.
|
9
|
+
attr_accessor :description
|
10
|
+
|
11
|
+
# Internal: Create a new BedType.
|
12
|
+
#
|
13
|
+
# info - A Hash from the parsed API response with the following keys:
|
14
|
+
# :id - The ID of the BedType.
|
15
|
+
# :description 3- The description of the BedType.
|
16
|
+
def initialize(info)
|
17
|
+
@id, @description = info[:id], info[:description]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class Cache
|
4
|
+
attr_accessor :store
|
5
|
+
|
6
|
+
def initialize(store)
|
7
|
+
@store = store
|
8
|
+
end
|
9
|
+
|
10
|
+
def save_query(action, params, response)
|
11
|
+
%w(apiKey cid customerSessionId customerIpAddress locale
|
12
|
+
customerUserAgent).each do |param|
|
13
|
+
params.delete(param)
|
14
|
+
end
|
15
|
+
params.delete("currencyCode") unless action == :paymentInfo
|
16
|
+
|
17
|
+
string_params = keys_to_strings(params)
|
18
|
+
|
19
|
+
@store[action] ||= {}
|
20
|
+
@store[action] = @store[action].merge(string_params => response)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_query(action, params)
|
24
|
+
string_params = keys_to_strings(params)
|
25
|
+
@store[action] ? @store[action][string_params] : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def keys
|
29
|
+
@store.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def cached?(action, params)
|
33
|
+
string_params = keys_to_strings(params)
|
34
|
+
@store[action] && @store[action][string_params]
|
35
|
+
end
|
36
|
+
|
37
|
+
def undefine_query(action, params)
|
38
|
+
string_params = keys_to_strings(params)
|
39
|
+
@store[action].delete(string_params) if @store[action]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def keys_to_strings(hash)
|
45
|
+
hash.inject({}) do |memo, (k, v)|
|
46
|
+
memo[k.to_s] = v
|
47
|
+
memo
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
# Public: An Exception to be raised from all EAN API-related errors.
|
4
|
+
class EANException < Exception
|
5
|
+
# Internal: Setter for the recovery information.
|
6
|
+
attr_writer :recovery
|
7
|
+
|
8
|
+
# Public: Getter for the recovery information.
|
9
|
+
attr_reader :recovery
|
10
|
+
|
11
|
+
# Internal: Setter for the error type.
|
12
|
+
attr_writer :type
|
13
|
+
|
14
|
+
# Public: Getter for the error type..
|
15
|
+
attr_reader :type
|
16
|
+
|
17
|
+
# Internal: Create a new EAN exception.
|
18
|
+
#
|
19
|
+
# message - The String error message returned by the API.
|
20
|
+
# type - The Symbol type of the error.
|
21
|
+
def initialize(message, type = nil)
|
22
|
+
@type = type
|
23
|
+
super(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Check if the error is recoverable. If it is, recovery information
|
27
|
+
# is in the attribute recovery.
|
28
|
+
#
|
29
|
+
# Returns a Boolean based on whether the error is recoverable.
|
30
|
+
def recoverable?
|
31
|
+
@recovery.is_a?(Hash)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
module Suitcase
|
2
|
+
# Internal: Various methods for doing things that many files need to in the
|
3
|
+
# library.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# parameterize(something: "else", another: "thing")
|
8
|
+
# # => "something=else&another=thing"
|
9
|
+
class Hotel
|
10
|
+
module Helpers
|
11
|
+
# Internal: Defaults for the builder options to Helpers#url.
|
12
|
+
URL_DEFAULTS = {
|
13
|
+
include_key: true,
|
14
|
+
include_cid: true,
|
15
|
+
secure: false,
|
16
|
+
as_form: false,
|
17
|
+
session: Session.new
|
18
|
+
}
|
19
|
+
|
20
|
+
# Internal: Parameterize a Hash.
|
21
|
+
#
|
22
|
+
# hash - The Hash to be parameterized.
|
23
|
+
#
|
24
|
+
# Examples
|
25
|
+
#
|
26
|
+
# parameterize(something: "else", another: "thing")
|
27
|
+
# # => "something=else&another=thing"
|
28
|
+
#
|
29
|
+
# Returns a parameterized String.
|
30
|
+
def parameterize(hash)
|
31
|
+
hash.map { |key, value| "#{key}=#{value}" }.join "&"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Internal: Build an API URL.
|
35
|
+
#
|
36
|
+
# builder - A Hash with the following required keys:
|
37
|
+
# :method - The API method to be put in the URL.
|
38
|
+
# :params - The params to be put in the URL.
|
39
|
+
# And the following optional ones:
|
40
|
+
# :include_key - Whether to include the API key or not.
|
41
|
+
# :include_cid - Whether to include the API cid or not.
|
42
|
+
# :secure - Whether or not for the request to be sent over
|
43
|
+
# HTTPS.
|
44
|
+
# :as_form - Whether or not to include the parameters in the
|
45
|
+
# URL.
|
46
|
+
# :session - The Session associated with the request.
|
47
|
+
#
|
48
|
+
# Examples
|
49
|
+
#
|
50
|
+
# url(secure: true, as_form: true, method: "myMethod", params: {})
|
51
|
+
# # => #<URI::HTTPS URL:https://book.api.ean.com/.../rs/hotel/v3/myMethod>
|
52
|
+
#
|
53
|
+
# Returns the URI with the builder's information.
|
54
|
+
def url(builder)
|
55
|
+
builder = URL_DEFAULTS.merge(builder)
|
56
|
+
builder[:session] ||= URL_DEFAULTS[:session]
|
57
|
+
method, params = builder[:method], builder[:params]
|
58
|
+
params["apiKey"] = Configuration.hotel_api_key if builder[:include_key]
|
59
|
+
params["cid"] = (Configuration.hotel_cid ||= 55505) if builder[:include_cid]
|
60
|
+
params["sig"] = generate_signature if Configuration.use_signature_auth?
|
61
|
+
params["minorRev"] = Configuration.ean_revision
|
62
|
+
|
63
|
+
url = main_url(builder[:secure]) + method.to_s + (builder[:as_form] ? "" : "?")
|
64
|
+
|
65
|
+
params.merge!(build_session_params(builder[:session]))
|
66
|
+
url += parameterize(params) unless builder[:as_form]
|
67
|
+
URI.parse(URI.escape(url))
|
68
|
+
end
|
69
|
+
|
70
|
+
# Internal: Get the root URL for the Hotels API.
|
71
|
+
#
|
72
|
+
# secure - Whether or not the URL should be HTTPS or not.
|
73
|
+
#
|
74
|
+
# Returns the URL.
|
75
|
+
def main_url(secure)
|
76
|
+
url = "http#{secure ? "s" : ""}://#{secure ? "book." : ""}"
|
77
|
+
url += "api.ean.com/ean-services/rs/hotel/v3/"
|
78
|
+
url
|
79
|
+
end
|
80
|
+
|
81
|
+
# Internal: Parse the JSON response at the given URL and handle errors.
|
82
|
+
#
|
83
|
+
# uri - The URI to parse the JSON from.
|
84
|
+
#
|
85
|
+
# Returns the parsed JSON.
|
86
|
+
def parse_response(uri)
|
87
|
+
response = Net::HTTP.get_response(uri)
|
88
|
+
|
89
|
+
if response.code.to_i == 403
|
90
|
+
if response.body.include?("Forbidden")
|
91
|
+
e = EANException.new("You have not been granted permission to access the requested method or object.")
|
92
|
+
e.type = :forbidden
|
93
|
+
elsif response.body.include?("Not Authorized")
|
94
|
+
e = EANException.new("The API key associated with your request was not recognized, or the digital signature was incorrect.")
|
95
|
+
e.type = :not_authorized
|
96
|
+
elsif response.body.include?("Developer Inactive")
|
97
|
+
e = EANException.new("The API key you are using to access the API has not been approved, is not correct, or has been disabled. If using SIG Authentication, your digital signature is incorrect and does not match the one generated when receiving your request.")
|
98
|
+
e.type = :developer_inactive
|
99
|
+
elsif response.body.include?("Queries Per Second Limit")
|
100
|
+
e = EANException.new("The API key you are using has attempted to access the API too many times in one second.")
|
101
|
+
e.type = :query_limit
|
102
|
+
elsif response.body.include?("Account Over Rate Limit")
|
103
|
+
e = EANException.new("The API key you are using has attempted to access the API too many times in the rate limiting period.")
|
104
|
+
e.type = :rate_limit
|
105
|
+
elsif response.body.include?("Rate Limit Exceeded")
|
106
|
+
e = EANException.new("The service you have requested is over capacity.")
|
107
|
+
e.type = :over_capacity
|
108
|
+
elsif response.body.include?("Authentication Failure")
|
109
|
+
e = EANException.new("The combination of authentication checks failed.")
|
110
|
+
e.type = :authentication_failure
|
111
|
+
else
|
112
|
+
e = EANException.new("An unknown error occured: #{response.body}.")
|
113
|
+
e.type = :unknown
|
114
|
+
end
|
115
|
+
|
116
|
+
raise e
|
117
|
+
end
|
118
|
+
|
119
|
+
JSON.parse(Net::HTTP.get_response(uri).body)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Internal: Raise the errors returned from the response.
|
123
|
+
#
|
124
|
+
# info - The parsed JSON to get the errors from.
|
125
|
+
#
|
126
|
+
# Returns nothing.
|
127
|
+
def handle_errors(info)
|
128
|
+
key = info.keys.first
|
129
|
+
if info[key] && info[key]["EanWsError"]
|
130
|
+
message = info[key]["EanWsError"]["presentationMessage"]
|
131
|
+
exception = EANException.new(message)
|
132
|
+
if message =~ /Multiple locations/ && (info = info[key]["LocationInfos"])
|
133
|
+
exception.type = :multiple_locations
|
134
|
+
exception.recovery = {
|
135
|
+
alternate_locations: info["LocationInfo"].map do |info|
|
136
|
+
Location.new(
|
137
|
+
destination_id: info["destinationId"],
|
138
|
+
type: info["type"],
|
139
|
+
city: info["city"],
|
140
|
+
province: info["stateProvinceCode"]
|
141
|
+
)
|
142
|
+
end
|
143
|
+
}
|
144
|
+
elsif message =~ /Data in this request could not be validated/
|
145
|
+
exception.type = :invalid_data
|
146
|
+
end
|
147
|
+
|
148
|
+
raise exception
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Internal: Get the base URL based on a Hash of info.
|
153
|
+
#
|
154
|
+
# info - A Hash with only one required key:
|
155
|
+
# :booking - Whether or not it is a booking request.
|
156
|
+
#
|
157
|
+
# Returns the base URL.
|
158
|
+
def base_url(info)
|
159
|
+
main_url(info[:booking])
|
160
|
+
end
|
161
|
+
|
162
|
+
# Internal: Build a Hash of params from a Session.
|
163
|
+
#
|
164
|
+
# session - The Session to generate params from.
|
165
|
+
#
|
166
|
+
# Returns the Hash of params.
|
167
|
+
def build_session_params(session)
|
168
|
+
session_info = {}
|
169
|
+
session_info["customerSessionId"] = session.id if session.id
|
170
|
+
session_info["customerIpAddress"] = session.ip_address if session.ip_address
|
171
|
+
session_info["locale"] = session.locale if session.locale
|
172
|
+
session_info["currencyCode"] = session.currency_code if session.currency_code
|
173
|
+
session_info["customerUserAgent"] = session.user_agent if session.user_agent
|
174
|
+
session_info
|
175
|
+
end
|
176
|
+
|
177
|
+
# Internal: Update the Session's ID with the parsed JSON.
|
178
|
+
#
|
179
|
+
# parsed - The parsed JSON to fetch the ID from.
|
180
|
+
# session - The Session to update.
|
181
|
+
#
|
182
|
+
# Returns nothing.
|
183
|
+
def update_session(parsed, session)
|
184
|
+
session ||= Session.new
|
185
|
+
session.id = parsed[parsed.keys.first]["customerSessionId"] if parsed[parsed.keys.first]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Internal: Generate a digital signature to authenticate with.
|
189
|
+
#
|
190
|
+
# Returns the generated signature.
|
191
|
+
def generate_signature
|
192
|
+
Digest::MD5.hexdigest(Configuration.hotel_api_key +
|
193
|
+
Configuration.hotel_shared_secret +
|
194
|
+
Time.now.to_i.to_s)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Image
|
3
|
+
attr_accessor :id, :url, :caption, :width, :height, :thumbnail_url, :name
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@id = data["hotelImageId"]
|
7
|
+
@name = data["name"]
|
8
|
+
@caption = data["caption"]
|
9
|
+
@url = data["url"]
|
10
|
+
@thumbnail_url = data["thumbnailURL"]
|
11
|
+
@width = data["width"]
|
12
|
+
@height = data["height"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def size
|
16
|
+
width.to_s + "x" + height.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class Location
|
4
|
+
attr_accessor :destination_id, :type, :active, :city, :province,
|
5
|
+
:country, :country_code
|
6
|
+
|
7
|
+
def initialize(info)
|
8
|
+
info.each do |k, v|
|
9
|
+
instance_variable_set("@" + k.to_s, v)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
include Helpers
|
15
|
+
|
16
|
+
# Public: Find a Location.
|
17
|
+
#
|
18
|
+
# info - A Hash of information to search by, including city & address.
|
19
|
+
#
|
20
|
+
# Returns an Array of Location's.
|
21
|
+
def find(info)
|
22
|
+
params = {}
|
23
|
+
[:city, :address].each do |dup|
|
24
|
+
params[dup] = info[dup] if info[dup]
|
25
|
+
end
|
26
|
+
if info[:destination_string]
|
27
|
+
params[:destinationString] = info[:destination_string]
|
28
|
+
end
|
29
|
+
|
30
|
+
if Configuration.cache? and Configuration.cache.cached?(:geoSearch, params)
|
31
|
+
raw = Configuration.cache.get_query(:geoSearch, params)
|
32
|
+
else
|
33
|
+
url = url(:method => 'geoSearch', :params => params, :session => info[:session])
|
34
|
+
raw = parse_response(url)
|
35
|
+
handle_errors(raw)
|
36
|
+
end
|
37
|
+
|
38
|
+
parse(raw)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(raw)
|
42
|
+
[raw["LocationInfoResponse"]["LocationInfos"]["LocationInfo"]].flatten.map do |raw|
|
43
|
+
Location.new(
|
44
|
+
province: raw["stateProvinceCode"],
|
45
|
+
destination_id: raw["destinationId"],
|
46
|
+
type: raw["type"],
|
47
|
+
city: raw["city"],
|
48
|
+
active: raw["active"],
|
49
|
+
code: raw["code"],
|
50
|
+
country: raw["country"],
|
51
|
+
country_code: raw["countryCode"]
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_errors(info)
|
57
|
+
key = info.keys.first
|
58
|
+
if info[key] && info[key]["EanWsError"]
|
59
|
+
message = info[key]["EanWsError"]["presentationMessage"]
|
60
|
+
end
|
61
|
+
|
62
|
+
raise EANException.new(message) if message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|