hungrytable 0.0.8 → 1.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 +7 -0
- data/README.md +299 -22
- data/RELEASE_NOTES.md +16 -0
- data/lib/hungrytable/api_health_monitor.rb +106 -0
- data/lib/hungrytable/circuit_breaker.rb +177 -0
- data/lib/hungrytable/config.rb +38 -16
- data/lib/hungrytable/enhanced_errors.rb +127 -0
- data/lib/hungrytable/errors.rb +36 -0
- data/lib/hungrytable/get_request.rb +17 -6
- data/lib/hungrytable/post_request.rb +17 -6
- data/lib/hungrytable/request.rb +42 -5
- data/lib/hungrytable/request_extensions.rb +17 -4
- data/lib/hungrytable/request_header.rb +25 -25
- data/lib/hungrytable/reservation_cancel.rb +19 -5
- data/lib/hungrytable/reservation_make.rb +30 -9
- data/lib/hungrytable/reservation_status.rb +56 -0
- data/lib/hungrytable/restaurant.rb +43 -31
- data/lib/hungrytable/restaurant_search.rb +63 -34
- data/lib/hungrytable/restaurant_slotlock.rb +32 -10
- data/lib/hungrytable/version.rb +3 -1
- data/lib/hungrytable.rb +117 -65
- metadata +43 -179
- data/.gitignore +0 -6
- data/.rvmrc +0 -1
- data/Gemfile +0 -5
- data/Guardfile +0 -16
- data/Rakefile +0 -8
- data/hungrytable.gemspec +0 -37
- data/test/restaurant_get_details_result.json +0 -6
- data/test/restaurant_search_result.json +0 -7
- data/test/test_helper.rb +0 -18
- data/test/unit/config_test.rb +0 -43
- data/test/unit/get_request_test.rb +0 -0
- data/test/unit/hungrytable/user_test.rb +0 -28
- data/test/unit/post_request_test.rb +0 -0
- data/test/unit/request_test.rb +0 -0
- data/test/unit/reservation_cancel_test.rb +0 -0
- data/test/unit/reservation_make_test.rb +0 -0
- data/test/unit/restaurant_search_test.rb +0 -0
- data/test/unit/restaurant_slotlock_test.rb +0 -0
- data/test/unit/restaurant_test.rb +0 -39
- data/test/user_login_result.json +0 -6
data/lib/hungrytable/config.rb
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# Configuration module for Hungrytable
|
|
2
5
|
module Config
|
|
3
|
-
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :partner_id, :oauth_key, :oauth_secret, :base_url
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
def partner_id
|
|
10
|
+
@partner_id ||= ENV.fetch('OT_PARTNER_ID') do
|
|
11
|
+
raise ConfigurationError, 'OT_PARTNER_ID must be set via ENV or Config.partner_id='
|
|
12
|
+
end
|
|
13
|
+
end
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
def oauth_key
|
|
16
|
+
@oauth_key ||= ENV.fetch('OT_OAUTH_KEY') do
|
|
17
|
+
raise ConfigurationError, 'OT_OAUTH_KEY must be set via ENV or Config.oauth_key='
|
|
18
|
+
end
|
|
19
|
+
end
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
def oauth_secret
|
|
22
|
+
@oauth_secret ||= ENV.fetch('OT_OAUTH_SECRET') do
|
|
23
|
+
raise ConfigurationError, 'OT_OAUTH_SECRET must be set via ENV or Config.oauth_secret='
|
|
24
|
+
end
|
|
25
|
+
end
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
def base_url
|
|
28
|
+
@base_url ||= 'https://secure.opentable.com/api/otapi_v3.ashx'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Reset configuration to defaults (useful for testing)
|
|
32
|
+
def reset!
|
|
33
|
+
@partner_id = nil
|
|
34
|
+
@oauth_key = nil
|
|
35
|
+
@oauth_secret = nil
|
|
36
|
+
@base_url = nil
|
|
37
|
+
end
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
# Check if all required configuration is present
|
|
40
|
+
def valid?
|
|
41
|
+
partner_id && oauth_key && oauth_secret
|
|
42
|
+
true
|
|
43
|
+
rescue ConfigurationError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
24
46
|
end
|
|
25
47
|
end
|
|
26
48
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hungrytable
|
|
4
|
+
# Enhanced error classes with diagnostic information
|
|
5
|
+
module EnhancedErrors
|
|
6
|
+
# Enhance UnauthorizedError with diagnostic info
|
|
7
|
+
class EnhancedUnauthorizedError < UnauthorizedError
|
|
8
|
+
attr_reader :diagnostics
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil)
|
|
11
|
+
@diagnostics = build_diagnostics
|
|
12
|
+
super(build_message(message))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_message(original_message)
|
|
18
|
+
msg = original_message || 'Authentication failed. Check your OAuth credentials.'
|
|
19
|
+
msg += "\n\nDiagnostics:\n"
|
|
20
|
+
msg += " Partner ID: #{mask_credential(Config.partner_id)}\n"
|
|
21
|
+
msg += " OAuth Key: #{mask_credential(Config.oauth_key)}\n"
|
|
22
|
+
msg += " Endpoint: #{Config.base_url}\n"
|
|
23
|
+
msg += "\nPossible causes:\n"
|
|
24
|
+
msg += " 1. Invalid or expired OAuth credentials\n"
|
|
25
|
+
msg += " 2. Partner ID does not match OAuth key/secret\n"
|
|
26
|
+
msg += " 3. Credentials not authorized for this API version\n"
|
|
27
|
+
msg += " 4. API endpoint has been deprecated\n"
|
|
28
|
+
msg += "\nRecommended actions:\n"
|
|
29
|
+
msg += " 1. Verify credentials: OT_PARTNER_ID, OT_OAUTH_KEY, OT_OAUTH_SECRET\n"
|
|
30
|
+
msg += " 2. Contact OpenTable: partnersupport@opentable.com\n"
|
|
31
|
+
msg += " 3. Run diagnostic: bin/verify_api\n"
|
|
32
|
+
msg
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_diagnostics
|
|
36
|
+
{
|
|
37
|
+
partner_id: mask_credential(Config.partner_id),
|
|
38
|
+
oauth_key: mask_credential(Config.oauth_key),
|
|
39
|
+
endpoint: Config.base_url,
|
|
40
|
+
timestamp: Time.now.utc.iso8601,
|
|
41
|
+
circuit_breaker_state: CircuitBreaker.state
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mask_credential(credential)
|
|
46
|
+
return 'NOT SET' unless credential
|
|
47
|
+
return '***' if credential.length <= 8
|
|
48
|
+
|
|
49
|
+
"#{credential[0..3]}...#{credential[-4..]}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Enhance ServerError with diagnostic info
|
|
54
|
+
class EnhancedServerError < ServerError
|
|
55
|
+
attr_reader :diagnostics
|
|
56
|
+
|
|
57
|
+
def initialize(message = nil, status_code = nil)
|
|
58
|
+
@status_code = status_code
|
|
59
|
+
@diagnostics = build_diagnostics
|
|
60
|
+
super(build_message(message))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_message(original_message)
|
|
66
|
+
msg = original_message || "OpenTable server error (#{@status_code})"
|
|
67
|
+
msg += "\n\nDiagnostics:\n"
|
|
68
|
+
msg += " Endpoint: #{Config.base_url}\n"
|
|
69
|
+
msg += " Status Code: #{@status_code}\n"
|
|
70
|
+
msg += " Circuit Breaker: #{CircuitBreaker.state}\n"
|
|
71
|
+
msg += "\nPossible causes:\n"
|
|
72
|
+
msg += " 1. OpenTable API is experiencing issues\n"
|
|
73
|
+
msg += " 2. Endpoint has been deprecated or moved\n"
|
|
74
|
+
msg += " 3. Rate limiting or quota exceeded\n"
|
|
75
|
+
msg += "\nRecommended actions:\n"
|
|
76
|
+
msg += " 1. Check OpenTable status page\n"
|
|
77
|
+
msg += " 2. Wait and retry (circuit breaker will help)\n"
|
|
78
|
+
msg += " 3. Contact OpenTable support if issue persists\n"
|
|
79
|
+
msg += " 4. Run diagnostic: bin/verify_api\n"
|
|
80
|
+
msg
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_diagnostics
|
|
84
|
+
{
|
|
85
|
+
endpoint: Config.base_url,
|
|
86
|
+
status_code: @status_code,
|
|
87
|
+
timestamp: Time.now.utc.iso8601,
|
|
88
|
+
circuit_breaker_state: CircuitBreaker.state
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Enhance HTTPError with retry suggestions
|
|
94
|
+
class EnhancedHTTPError < HTTPError
|
|
95
|
+
attr_reader :diagnostics, :retryable
|
|
96
|
+
|
|
97
|
+
def initialize(message, retryable: false)
|
|
98
|
+
@retryable = retryable
|
|
99
|
+
@diagnostics = build_diagnostics
|
|
100
|
+
super(build_message(message))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build_message(original_message)
|
|
106
|
+
msg = original_message
|
|
107
|
+
msg += "\n\nRetryable: #{@retryable ? 'Yes' : 'No'}\n"
|
|
108
|
+
msg += "Circuit Breaker State: #{CircuitBreaker.state}\n"
|
|
109
|
+
|
|
110
|
+
if @retryable
|
|
111
|
+
msg += "\nThis error may be transient. Consider retrying.\n"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
msg
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_diagnostics
|
|
118
|
+
{
|
|
119
|
+
endpoint: Config.base_url,
|
|
120
|
+
timestamp: Time.now.utc.iso8601,
|
|
121
|
+
circuit_breaker_state: CircuitBreaker.state,
|
|
122
|
+
retryable: @retryable
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hungrytable
|
|
4
|
+
# Base error class for all Hungrytable errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Configuration errors
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# API errors
|
|
11
|
+
class APIError < Error
|
|
12
|
+
attr_reader :error_code, :error_message
|
|
13
|
+
|
|
14
|
+
def initialize(error_code, error_message)
|
|
15
|
+
@error_code = error_code
|
|
16
|
+
@error_message = error_message
|
|
17
|
+
super("OpenTable API Error #{error_code}: #{error_message}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# HTTP errors
|
|
22
|
+
class HTTPError < Error; end
|
|
23
|
+
class NotFoundError < HTTPError; end
|
|
24
|
+
class UnauthorizedError < HTTPError; end
|
|
25
|
+
class ServerError < HTTPError; end
|
|
26
|
+
|
|
27
|
+
# Validation errors
|
|
28
|
+
class ValidationError < Error; end
|
|
29
|
+
class MissingRequiredFieldError < ValidationError; end
|
|
30
|
+
|
|
31
|
+
# OpenTable specific errors mapped from error codes
|
|
32
|
+
class ReservationError < APIError; end
|
|
33
|
+
class SlotlockError < APIError; end
|
|
34
|
+
class RestaurantNotFoundError < APIError; end
|
|
35
|
+
class NoAvailabilityError < APIError; end
|
|
36
|
+
end
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# HTTP GET request handler
|
|
2
5
|
class GetRequest < Request
|
|
6
|
+
# Default timeout for HTTP requests (in seconds)
|
|
7
|
+
DEFAULT_TIMEOUT = 30
|
|
8
|
+
|
|
3
9
|
private
|
|
4
|
-
def auth_header
|
|
5
|
-
Hungrytable::RequestHeader.new(:get, @uri, {}, {})
|
|
6
|
-
end
|
|
7
10
|
|
|
8
11
|
def make_request
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
response = HTTP
|
|
13
|
+
.timeout(DEFAULT_TIMEOUT)
|
|
14
|
+
.headers('Authorization' => auth_header)
|
|
15
|
+
.get(uri)
|
|
16
|
+
|
|
17
|
+
handle_http_errors(response)
|
|
18
|
+
rescue HTTP::Error => e
|
|
19
|
+
raise HTTPError, "HTTP request failed: #{e.message}"
|
|
12
20
|
end
|
|
13
21
|
|
|
22
|
+
def auth_header
|
|
23
|
+
RequestHeader.new(:get, uri, {}, {}).to_s
|
|
24
|
+
end
|
|
14
25
|
end
|
|
15
26
|
end
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# HTTP POST request handler
|
|
2
5
|
class PostRequest < Request
|
|
6
|
+
# Default timeout for HTTP requests (in seconds)
|
|
7
|
+
DEFAULT_TIMEOUT = 30
|
|
8
|
+
|
|
3
9
|
private
|
|
4
|
-
def auth_header
|
|
5
|
-
Hungrytable::RequestHeader.new(:post, @uri, {}, {})
|
|
6
|
-
end
|
|
7
10
|
|
|
8
11
|
def make_request
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
response = HTTP
|
|
13
|
+
.timeout(DEFAULT_TIMEOUT)
|
|
14
|
+
.headers('Authorization' => auth_header)
|
|
15
|
+
.post(uri, form: params)
|
|
16
|
+
|
|
17
|
+
handle_http_errors(response)
|
|
18
|
+
rescue HTTP::Error => e
|
|
19
|
+
raise HTTPError, "HTTP request failed: #{e.message}"
|
|
12
20
|
end
|
|
13
21
|
|
|
22
|
+
def auth_header
|
|
23
|
+
RequestHeader.new(:post, uri, params, {}).to_s
|
|
24
|
+
end
|
|
14
25
|
end
|
|
15
26
|
end
|
data/lib/hungrytable/request.rb
CHANGED
|
@@ -1,17 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# Base class for API requests
|
|
2
5
|
class Request
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
attr_reader :uri, :params
|
|
7
|
+
|
|
8
|
+
def initialize(uri, params = {})
|
|
9
|
+
@uri = "#{Hungrytable::Config.base_url}#{uri}" # Fixed: don't mutate base_url
|
|
5
10
|
@params = params
|
|
6
11
|
end
|
|
7
12
|
|
|
13
|
+
# Parse the JSON response
|
|
14
|
+
# @return [Hash] parsed JSON response
|
|
15
|
+
# @raise [Hungrytable::HTTPError] if the response is invalid
|
|
8
16
|
def parsed_response
|
|
9
|
-
JSON.parse(
|
|
17
|
+
JSON.parse(response_body)
|
|
18
|
+
rescue JSON::ParserError => e
|
|
19
|
+
raise HTTPError, "Failed to parse response: #{e.message}"
|
|
10
20
|
end
|
|
11
21
|
|
|
12
22
|
private
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
|
|
24
|
+
# Get the response body from the HTTP request
|
|
25
|
+
# @return [String] response body
|
|
26
|
+
def response_body
|
|
27
|
+
@response_body ||= make_request
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Make the HTTP request (to be implemented by subclasses)
|
|
31
|
+
# @return [String] response body
|
|
32
|
+
def make_request
|
|
33
|
+
raise NotImplementedError, 'Subclasses must implement make_request'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Handle HTTP errors
|
|
37
|
+
# @param response [HTTP::Response] the HTTP response object
|
|
38
|
+
# @raise [Hungrytable::HTTPError] if the response indicates an error
|
|
39
|
+
def handle_http_errors(response)
|
|
40
|
+
case response.status.code
|
|
41
|
+
when 200..299
|
|
42
|
+
response.body.to_s
|
|
43
|
+
when 401
|
|
44
|
+
raise UnauthorizedError, 'Authentication failed. Check your OAuth credentials.'
|
|
45
|
+
when 404
|
|
46
|
+
raise NotFoundError, "Resource not found: #{uri}"
|
|
47
|
+
when 500..599
|
|
48
|
+
raise ServerError, "OpenTable server error (#{response.status.code})"
|
|
49
|
+
else
|
|
50
|
+
raise HTTPError, "HTTP error #{response.status.code}: #{response.body}"
|
|
51
|
+
end
|
|
15
52
|
end
|
|
16
53
|
end
|
|
17
54
|
end
|
|
@@ -1,19 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# Shared functionality for classes that make API requests
|
|
5
|
+
# Follows DRY principle - don't repeat yourself
|
|
2
6
|
module RequestExtensions
|
|
3
7
|
extend ActiveSupport::Concern
|
|
4
8
|
|
|
5
9
|
private
|
|
10
|
+
|
|
11
|
+
# Get the request object (lazy-loaded)
|
|
12
|
+
# @return [Request] the request object
|
|
6
13
|
def request
|
|
7
14
|
@request ||= @requester.new(request_uri, params)
|
|
8
15
|
end
|
|
9
16
|
|
|
17
|
+
# Ensure all required options are present
|
|
18
|
+
# @raise [MissingRequiredFieldError] if any required field is missing
|
|
10
19
|
def ensure_required_opts
|
|
11
|
-
required_opts
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
return unless respond_to?(:required_opts, true)
|
|
21
|
+
|
|
22
|
+
missing = required_opts.reject { |key| opts.key?(key) }
|
|
23
|
+
return if missing.empty?
|
|
24
|
+
|
|
25
|
+
raise MissingRequiredFieldError, "Missing required fields: #{missing.join(', ')}"
|
|
14
26
|
end
|
|
15
27
|
|
|
16
|
-
#
|
|
28
|
+
# Default params (can be overridden in classes that send POST requests)
|
|
29
|
+
# @return [Hash] request parameters
|
|
17
30
|
def params
|
|
18
31
|
{}
|
|
19
32
|
end
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Modified from simple_oauth (https://github.com/laserlemon/simple_oauth)
|
|
2
4
|
module Hungrytable
|
|
3
5
|
class RequestHeader
|
|
4
|
-
|
|
5
|
-
ATTRIBUTE_KEYS = %w(consumer_key nonce signature_method timestamp token version).map(&:to_sym)
|
|
6
|
+
ATTRIBUTE_KEYS = %w[consumer_key nonce signature_method timestamp token version].map(&:to_sym)
|
|
6
7
|
|
|
7
8
|
def self.default_options
|
|
8
9
|
{
|
|
9
|
-
:
|
|
10
|
-
:
|
|
11
|
-
:
|
|
12
|
-
:
|
|
13
|
-
:
|
|
14
|
-
:
|
|
15
|
-
:
|
|
10
|
+
nonce: OpenSSL::Random.random_bytes(16).unpack1('H*'),
|
|
11
|
+
signature_method: 'HMAC-SHA1',
|
|
12
|
+
timestamp: Time.now.to_i.to_s,
|
|
13
|
+
version: '1.0',
|
|
14
|
+
consumer_key: Hungrytable::Config.oauth_key,
|
|
15
|
+
consumer_secret: Hungrytable::Config.oauth_secret,
|
|
16
|
+
token: ''
|
|
16
17
|
}
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.encode(value)
|
|
20
|
-
|
|
21
|
+
CGI.escape(value.to_s).gsub('+', '%20').gsub('%7E', '~')
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def self.decode(value)
|
|
24
|
-
|
|
25
|
+
CGI.unescape(value.to_s)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
attr_reader :method, :params, :options
|
|
@@ -33,7 +34,7 @@ module Hungrytable
|
|
|
33
34
|
@uri.normalize!
|
|
34
35
|
@uri.fragment = nil
|
|
35
36
|
@params = params
|
|
36
|
-
@options =
|
|
37
|
+
@options = self.class.default_options.merge(oauth)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def url
|
|
@@ -43,7 +44,7 @@ module Hungrytable
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def to_s
|
|
46
|
-
%
|
|
47
|
+
%(OAuth realm="http://www.opentable.com/", #{normalized_attributes})
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def valid?(secrets = {})
|
|
@@ -55,38 +56,38 @@ module Hungrytable
|
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def signed_attributes
|
|
58
|
-
attributes.merge(:
|
|
59
|
+
attributes.merge(oauth_signature: signature)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
63
64
|
def normalized_attributes
|
|
64
|
-
signed_attributes.sort_by{|k,
|
|
65
|
+
signed_attributes.sort_by { |k, _v| k.to_s }.map { |k, v| %(#{k}="#{self.class.encode(v)}") }.join(', ')
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def attributes
|
|
68
|
-
ATTRIBUTE_KEYS.inject({}){|a,k| options.key?(k) ? a.merge(
|
|
69
|
+
ATTRIBUTE_KEYS.inject({}) { |a, k| options.key?(k) ? a.merge("oauth_#{k}": options[k]) : a }
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def signature
|
|
72
|
-
send(options[:signature_method].downcase.tr('-', '_')
|
|
73
|
+
send("#{options[:signature_method].downcase.tr('-', '_')}_signature")
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def hmac_sha1_signature
|
|
76
|
-
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest
|
|
77
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA1'), secret, signature_base)).chomp.gsub("\n", '')
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def secret
|
|
80
|
-
options.values_at(:consumer_secret, :token_secret).map{|v| self.class.encode(v) }.join('&')
|
|
81
|
+
options.values_at(:consumer_secret, :token_secret).map { |v| self.class.encode(v) }.join('&')
|
|
81
82
|
end
|
|
82
|
-
|
|
83
|
+
alias plaintext_signature secret
|
|
83
84
|
|
|
84
85
|
def signature_base
|
|
85
|
-
[method, url, normalized_params].map{|v| self.class.encode(v) }.join('&')
|
|
86
|
+
[method, url, normalized_params].map { |v| self.class.encode(v) }.join('&')
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def normalized_params
|
|
89
|
-
signature_params.map{|p| p.map{|v| self.class.encode(v) } }.sort.map{|p| p.join('=') }.join('&')
|
|
90
|
+
signature_params.map { |p| p.map { |v| self.class.encode(v) } }.sort.map { |p| p.join('=') }.join('&')
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
def signature_params
|
|
@@ -94,16 +95,15 @@ module Hungrytable
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
def url_params
|
|
97
|
-
CGI.parse(@uri.query || '').inject([]){|p,(k,vs)| p + vs.sort.map{|v| [k, v] } }
|
|
98
|
+
CGI.parse(@uri.query || '').inject([]) { |p, (k, vs)| p + vs.sort.map { |v| [k, v] } }
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def rsa_sha1_signature
|
|
101
|
-
Base64.encode64(private_key.sign(OpenSSL::Digest
|
|
102
|
+
Base64.encode64(private_key.sign(OpenSSL::Digest.new('SHA1'), signature_base)).chomp.gsub("\n", '')
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def private_key
|
|
105
106
|
OpenSSL::PKey::RSA.new(options[:consumer_secret])
|
|
106
107
|
end
|
|
107
|
-
|
|
108
108
|
end
|
|
109
109
|
end
|
|
@@ -1,30 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# Cancels an existing restaurant reservation
|
|
2
5
|
class ReservationCancel
|
|
3
6
|
include RequestExtensions
|
|
4
7
|
|
|
5
8
|
attr_reader :opts
|
|
6
9
|
|
|
7
|
-
def initialize
|
|
10
|
+
def initialize(opts = {})
|
|
8
11
|
@opts = opts
|
|
9
12
|
ensure_required_opts
|
|
10
13
|
@requester = opts[:requester] || GetRequest
|
|
11
14
|
end
|
|
12
15
|
|
|
16
|
+
# Check if the cancellation was successful
|
|
17
|
+
# @return [Boolean] true if no errors
|
|
13
18
|
def successful?
|
|
14
|
-
details[
|
|
19
|
+
details['ns:ErrorID'].to_s == '0'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get error messages if cancellation failed
|
|
23
|
+
# @return [String, nil] error message or nil
|
|
24
|
+
def error_message
|
|
25
|
+
details['ns:ErrorMessage']
|
|
15
26
|
end
|
|
16
27
|
|
|
17
28
|
private
|
|
29
|
+
|
|
18
30
|
def request_uri
|
|
19
|
-
"/reservation/?pid=#{Config.partner_id}&rid=#{opts[:restaurant_id]}&
|
|
31
|
+
"/reservation/?pid=#{Config.partner_id}&rid=#{opts[:restaurant_id]}&" \
|
|
32
|
+
"conf=#{CGI.escape(opts[:confirmation_number].to_s)}&email=#{CGI.escape(opts[:email_address])}"
|
|
20
33
|
end
|
|
21
34
|
|
|
35
|
+
# @return [Hash] cancellation results from API response
|
|
22
36
|
def details
|
|
23
|
-
request.parsed_response[
|
|
37
|
+
@details ||= request.parsed_response['Results'] || {}
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
def required_opts
|
|
27
|
-
%
|
|
41
|
+
%i[email_address confirmation_number restaurant_id]
|
|
28
42
|
end
|
|
29
43
|
end
|
|
30
44
|
end
|
|
@@ -1,28 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hungrytable
|
|
4
|
+
# Makes a new restaurant reservation
|
|
2
5
|
class ReservationMake
|
|
3
6
|
include RequestExtensions
|
|
4
7
|
|
|
5
8
|
attr_reader :restaurant_slotlock, :opts
|
|
6
9
|
|
|
7
|
-
def initialize
|
|
10
|
+
def initialize(restaurant_slotlock, opts = {})
|
|
8
11
|
@opts = opts
|
|
9
12
|
ensure_required_opts
|
|
10
|
-
@requester
|
|
13
|
+
@requester = opts[:requester] || PostRequest
|
|
11
14
|
@restaurant_slotlock = restaurant_slotlock
|
|
12
15
|
end
|
|
13
16
|
|
|
17
|
+
# Check if the reservation was successful
|
|
18
|
+
# @return [Boolean] true if no errors
|
|
14
19
|
def successful?
|
|
15
|
-
details[
|
|
20
|
+
details['ns:ErrorID'].to_s == '0'
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
# Get the confirmation number for the reservation
|
|
24
|
+
# @return [String, nil] confirmation number or nil if unsuccessful
|
|
18
25
|
def confirmation_number
|
|
19
26
|
return nil unless successful?
|
|
20
|
-
|
|
27
|
+
|
|
28
|
+
details['ns:ConfirmationNumber']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get error messages if reservation failed
|
|
32
|
+
# @return [String, nil] error message or nil
|
|
33
|
+
def error_message
|
|
34
|
+
details['ns:ErrorMessage']
|
|
21
35
|
end
|
|
22
36
|
|
|
23
37
|
private
|
|
38
|
+
|
|
24
39
|
def required_opts
|
|
25
|
-
%
|
|
40
|
+
%i[email_address firstname lastname phone]
|
|
26
41
|
end
|
|
27
42
|
|
|
28
43
|
def default_options
|
|
@@ -30,22 +45,28 @@ module Hungrytable
|
|
|
30
45
|
'OTannouncementOption' => '0',
|
|
31
46
|
'RestaurantEmailOption' => '0',
|
|
32
47
|
'firsttimediner' => '0',
|
|
33
|
-
'specialinstructions' =>
|
|
48
|
+
'specialinstructions' => opts[:specialinstructions] || '',
|
|
34
49
|
'slotlockid' => restaurant_slotlock.slotlock_id
|
|
35
50
|
}.merge(restaurant_slotlock.params)
|
|
36
51
|
end
|
|
37
52
|
|
|
38
53
|
def params
|
|
39
|
-
|
|
54
|
+
# Filter out internal options (like :requester) before converting to API parameters
|
|
55
|
+
internal_opts = %i[requester]
|
|
56
|
+
api_opts = opts.except(*internal_opts)
|
|
57
|
+
|
|
58
|
+
# Convert symbol keys to strings for API
|
|
59
|
+
user_opts = api_opts.transform_keys(&:to_s)
|
|
60
|
+
default_options.merge(user_opts)
|
|
40
61
|
end
|
|
41
62
|
|
|
42
63
|
def request_uri
|
|
43
64
|
"/reservation/?pid=#{Config.partner_id}&st=0"
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
# @return [Hash] reservation results from API response
|
|
46
68
|
def details
|
|
47
|
-
request.parsed_response[
|
|
69
|
+
@details ||= request.parsed_response['MakeResults'] || {}
|
|
48
70
|
end
|
|
49
|
-
|
|
50
71
|
end
|
|
51
72
|
end
|