class Booker::Client
Constants
- API_GATEWAY_ERRORS
- BOOKER_SERVER_TIMEZONE
- CLIENT_CREDENTIALS_GRANT_TYPE
- CREATE_TOKEN_CONTENT_TYPE
- CREATE_TOKEN_PATH
- DEFAULT_AUTH_BASE_URL
- DEFAULT_BASE_URL
- DEFAULT_CONTENT_TYPE
- DEFAULT_REQUEST_TIMEOUT
- ENV_BASE_URL_KEY
- REFRESH_TOKEN_GRANT_TYPE
- UPDATE_TOKEN_CONTEXT_PATH
- VALID_ACCESS_TOKEN_SCOPES
Attributes
access_token_scope[RW]
api_subscription_key[RW]
auth_base_url[RW]
auth_with_client_credentials[RW]
base_url[RW]
client_id[RW]
client_secret[RW]
location_id[RW]
refresh_token[RW]
request_timeout[RW]
temp_access_token[RW]
temp_access_token_expires_at[RW]
token_store[RW]
token_store_callback_method[RW]
Public Class Methods
new(options = {})
click to toggle source
# File lib/booker/client.rb, line 29 def initialize(options = {}) options.each { |key, value| send(:"#{key}=", value) } self.request_timeout ||= DEFAULT_REQUEST_TIMEOUT self.base_url ||= get_base_url self.auth_base_url ||= ENV['BOOKER_API_BASE_URL'] || DEFAULT_AUTH_BASE_URL self.client_id ||= ENV['BOOKER_CLIENT_ID'] self.client_secret ||= ENV['BOOKER_CLIENT_SECRET'] self.api_subscription_key ||= ENV['BOOKER_API_SUBSCRIPTION_KEY'] if self.auth_with_client_credentials.nil? self.auth_with_client_credentials = ENV['BOOKER_API_AUTH_WITH_CLIENT_CREDENTIALS'] == 'true' end if self.temp_access_token.present? begin self.temp_access_token_expires_at = token_expires_at(self.temp_access_token) self.access_token_scope = token_scope(self.temp_access_token) rescue JWT::ExpiredSignature => ex raise ex unless self.auth_with_client_credentials || self.refresh_token.present? end end if self.access_token_scope.blank? self.access_token_scope = VALID_ACCESS_TOKEN_SCOPES.first elsif !self.access_token_scope.in?(VALID_ACCESS_TOKEN_SCOPES) raise ArgumentError, "access_token_scope must be one of: #{VALID_ACCESS_TOKEN_SCOPES.join(', ')}" end end
Public Instance Methods
access_token()
click to toggle source
# File lib/booker/client.rb, line 194 def access_token (self.temp_access_token && !temp_access_token_expired?) ? self.temp_access_token : get_access_token end
access_token_response()
click to toggle source
# File lib/booker/client.rb, line 226 def access_token_response body = { grant_type: self.auth_with_client_credentials ? CLIENT_CREDENTIALS_GRANT_TYPE : REFRESH_TOKEN_GRANT_TYPE, client_id: self.client_id, client_secret: self.client_secret, scope: self.access_token_scope } body[:refresh_token] = self.refresh_token if body[:grant_type] == REFRESH_TOKEN_GRANT_TYPE options = { headers: { 'Content-Type' => CREATE_TOKEN_CONTENT_TYPE, 'Ocp-Apim-Subscription-Key' => self.api_subscription_key }, body: body.to_query, timeout: 30 } url = "#{self.auth_base_url}#{CREATE_TOKEN_PATH}" begin handle_errors! url, options, HTTParty.post(url, options), false rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded # retry once sleep 1 handle_errors! url, options, HTTParty.post(url, options), false end end
delete(path, params=nil, body=nil, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 83 def delete(path, params=nil, body=nil, booker_model=nil) booker_resources = get_booker_resources(:delete, path, params, body.to_json, booker_model) build_resources(booker_resources, booker_model) end
full_url(path)
click to toggle source
# File lib/booker/client.rb, line 159 def full_url(path) uri = URI(path) uri.scheme ? path : "#{self.base_url}#{path}" end
get(path, params, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 59 def get(path, params, booker_model=nil) booker_resources = get_booker_resources(:get, path, params, nil, booker_model) build_resources(booker_resources, booker_model) end
get_access_token()
click to toggle source
# File lib/booker/client.rb, line 204 def get_access_token unless self.auth_with_client_credentials || self.refresh_token raise ArgumentError, 'Cannot get new access token without auth_with_client_credentials or a refresh_token' end resp = access_token_response token = resp.parsed_response['access_token'] raise Booker::InvalidApiCredentials.new(response: resp) if token.blank? if self.auth_with_client_credentials && self.location_id self.temp_access_token = get_location_access_token(token, self.location_id) else self.temp_access_token = token end self.temp_access_token_expires_at = token_expires_at(self.temp_access_token) update_token_store self.temp_access_token end
get_base_url()
click to toggle source
# File lib/booker/client.rb, line 55 def get_base_url ENV[self.class::ENV_BASE_URL_KEY] || self.class::DEFAULT_BASE_URL end
get_booker_resources(http_method, path, params=nil, body=nil, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 128 def get_booker_resources(http_method, path, params=nil, body=nil, booker_model=nil) http_options = request_options(params, body) url = full_url(path) puts "BOOKER REQUEST: #{http_method} #{url} #{http_options}" if ENV['BOOKER_API_DEBUG'] == 'true' # Allow it to retry the first time unless it is an authorization error begin response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options)) rescue Booker::Error, Net::ReadTimeout => ex if ex.is_a? Booker::InvalidApiCredentials raise ex else sleep 1 response = nil # Force a retry (see logic below) end end unless response_is_error?(response, http_method) return results_from_response(response, booker_model) end # Retry on blank responses (happens in certain v4 API methods in lieu of an actual error) response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options)) unless response_is_error?(response, http_method) return results_from_response(response, booker_model) end # Raise if response is still blank raise Booker::Error.new(url: url, request: http_options, response: response) end
get_location_access_token(existing_token, location_id)
click to toggle source
# File lib/booker/client.rb, line 254 def get_location_access_token(existing_token, location_id) options = { headers: { 'Accept' => 'application/json', 'Authorization' => "Bearer #{existing_token}", 'Ocp-Apim-Subscription-Key' => self.api_subscription_key }, query: { locationId: location_id }, timeout: 30 } url = "#{self.auth_base_url}#{UPDATE_TOKEN_CONTEXT_PATH}" begin resp = handle_errors! url, options, HTTParty.post(url, options), false rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded # retry once sleep 1 resp = handle_errors! url, options, HTTParty.post(url, options), false end resp.parsed_response end
handle_errors!(url, request, response, retry_unauthorized=true)
click to toggle source
# File lib/booker/client.rb, line 164 def handle_errors!(url, request, response, retry_unauthorized=true) puts "BOOKER RESPONSE: #{response}" if ENV['BOOKER_API_DEBUG'] == 'true' error_class = API_GATEWAY_ERRORS[response.code] begin raise error_class.new(url: url, request: request, response: response) if error_class rescue Booker::InvalidApiCredentials => ex raise ex unless response.code == 401 && retry_unauthorized get_access_token return nil end ex = Booker::Error.new(url: url, request: request, response: response) if ex.error.present? || !response.success? case ex.error when 'invalid_client' raise Booker::InvalidApiCredentials.new(url: url, request: request, response: response) when 'invalid access token' get_access_token return nil else raise ex end end response end
paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true)
click to toggle source
# File lib/booker/client.rb, line 89 def paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true) page_size = params[:PageSize] page_number = params[:PageNumber] if page_size.nil? || page_size < 1 || page_number.nil? || page_number < 1 || !params[:UsePaging] raise ArgumentError, 'params must include valid PageSize, PageNumber and UsePaging' end puts "fetching #{path} with #{params.except(:access_token)}. #{fetched.length} results so far." begin results = self.send(method, path, params, model) rescue Net::ReadTimeout results = nil end unless results.is_a?(Array) error_msg = "Result from paginated request to #{path} with params: #{params} is not a collection" raise Booker::MidPaginationError.new(message: error_msg, error_occurred_during_params: params, results_fetched_prior_to_error: fetched) end fetched.concat(results) results_length = results.length if fetch_all if results_length > 0 # TODO (#111186744): Add logging to see if any pages with less than expected data (as seen in the /appointments endpoint) new_params = params.deep_dup new_params[:PageNumber] = page_number + 1 paginated_request(method: method, path: path, params: new_params, model: model, fetched: fetched) else fetched end else results end end
patch(path, data, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 77 def patch(path, data, booker_model=nil) booker_resources = get_booker_resources(:patch, path, nil, data.to_json, booker_model) build_resources(booker_resources, booker_model) end
post(path, data, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 65 def post(path, data, booker_model=nil) booker_resources = get_booker_resources(:post, path, nil, data.to_json, booker_model) build_resources(booker_resources, booker_model) end
put(path, data, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 71 def put(path, data, booker_model=nil) booker_resources = get_booker_resources(:put, path, nil, data.to_json, booker_model) build_resources(booker_resources, booker_model) end
update_token_store()
click to toggle source
# File lib/booker/client.rb, line 198 def update_token_store if self.token_store.present? && self.token_store_callback_method.present? self.token_store.send(self.token_store_callback_method, self.temp_access_token, self.temp_access_token_expires_at) end end
Private Instance Methods
build_resources(resources, booker_model)
click to toggle source
# File lib/booker/client.rb, line 297 def build_resources(resources, booker_model) return resources if booker_model.nil? if resources.is_a? Hash booker_model.from_hash(resources) elsif resources.is_a? Array booker_model.from_list(resources) else resources end end
decoded_token_info(token)
click to toggle source
# File lib/booker/client.rb, line 348 def decoded_token_info(token) JWT.decode(token, nil, false, verify_not_before: false)[0] end
nil_or_empty_hash?(obj)
click to toggle source
# File lib/booker/client.rb, line 336 def nil_or_empty_hash?(obj) obj.nil? || (obj.is_a?(Hash) && obj.blank?) end
request_options(query=nil, body=nil)
click to toggle source
# File lib/booker/client.rb, line 280 def request_options(query=nil, body=nil) options = { # Headers must use stringified keys due to how they are transformed in some Net::HTTP versions headers: { 'Content-Type' => DEFAULT_CONTENT_TYPE, 'Accept' => DEFAULT_CONTENT_TYPE, 'Authorization' => "Bearer #{access_token}", 'Ocp-Apim-Subscription-Key' => self.api_subscription_key }, timeout: self.request_timeout } options[:body] = body if body.present? options[:query] = query if query.present? options end
response_is_error?(response, http_method)
click to toggle source
# File lib/booker/client.rb, line 330 def response_is_error?(response, http_method) return false if (http_method == :delete) && (response.try(:code) == 204) response.nil? || nil_or_empty_hash?(response.parsed_response) end
results_from_response(response, booker_model=nil)
click to toggle source
# File lib/booker/client.rb, line 313 def results_from_response(response, booker_model=nil) parsed_response = response.parsed_response return parsed_response unless parsed_response.is_a?(Hash) return parsed_response['Results'] unless parsed_response['Results'].nil? if booker_model response_results_key = booker_model.response_results_key return parsed_response[response_results_key] unless parsed_response[response_results_key].nil? pluralized = response_results_key.pluralize return parsed_response[pluralized] unless parsed_response[pluralized].nil? end parsed_response end
temp_access_token_expired?()
click to toggle source
# File lib/booker/client.rb, line 309 def temp_access_token_expired? self.temp_access_token_expires_at.nil? || self.temp_access_token_expires_at <= Time.now end
token_expires_at(token)
click to toggle source
# File lib/booker/client.rb, line 340 def token_expires_at(token) Time.at(decoded_token_info(token)['exp']) end
token_scope(token)
click to toggle source
# File lib/booker/client.rb, line 344 def token_scope(token) decoded_token_info(token)['scope'] end