Skip to content

Instantly share code, notes, and snippets.

@moiristo
Last active March 9, 2021 16:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moiristo/d7e9463afd8b8ffa99ac4ab81bb1e617 to your computer and use it in GitHub Desktop.
Save moiristo/d7e9463afd8b8ffa99ac4ab81bb1e617 to your computer and use it in GitHub Desktop.
BookingExperts client - ruby example
# frozen_string_literal: true
module BookingExperts
class Client
class Error < StandardError; end
class InvalidSignature < Error; end
attr_accessor :token_store
def initialize(token_store = nil)
@site = 'https://api.bookingexperts.nl'
@client_id = 'your app oauth2 UID'
@client_secret = 'your app oauth2 secret'
@redirect_uri = 'your oauth2 redirect URI' # For example: https://myapp.example.com/oauth_callback
@token_store = token_store # interface: store_token(token), fetch_token(oauth2_client)
end
# Builds a signature for the given payload and oauth2 secret
def self.build_signature(payload, client_secret)
"sha256=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), client_secret, payload)}"
end
# Verifies the HTTP_X_BE_SIGNATURE of a request
def self.verify_and_yield_body(request, client_secret)
payload = request.body.read
be_signature = request.get_header('HTTP_X_BE_SIGNATURE')
raise(InvalidSignature) if be_signature.blank? || !Rack::Utils.secure_compare(build_signature(payload, client_secret), be_signature)
JSON.parse(payload)
end
# Instantiates an oauth2 client (https://github.com/oauth-xx/oauth2)
def oauth2_client
@oauth2_client ||= OAuth2::Client.new(@client_id, @client_secret, site: @site)
end
# Gets a token for a received authorization code. This will also containt the refresh token and the expire datetime.
def get_token_for_authorization_code!(authorization_code)
oauth2_client.auth_code.get_token(authorization_code, redirect_uri: @redirect_uri)
end
# Processes a received authorization code for a subscription.
# 1) Get the token and refresh token
# 2) Fetch the subscription using the token. This will containt the subscription ID, return URL, administration details
# 3) Store the token and return the subscription
def process_authorization_code!(authorization_code, token_store_resolver:)
token = get_token_for_authorization_code!(authorization_code)
subscription = request_json(:get, '/v3/subscription', token: token)
self.token_store = token_store_resolver.call(subscription['data']['id'])
store_token!(token)
subscription
end
# Stores the token to the token store. Raises an error when no token store has been set
def store_token!(token)
raise Error, 'no token store set!' if token_store.nil?
token_store.store_token!(token)
end
# Fetches the currently active token.
# 1) Get the last persisted token from the token store
# 2) Check if the persisted token is not expired
# 3) When the token has expired, use the refresh token to obtain a new token and store it in the token store
def current_token
raise Error, 'no token store set!' if token_store.nil?
if (token = token_store.fetch_token!(oauth2_client))
if token.expired?
token = token.refresh!
store_token!(token)
end
token
end
end
# Sends a request to the BookingExperts jsonapi
def request(http_method, endpoint, token: current_token, options: {})
raise Error, 'no token available!' if token.nil?
options[:headers] ||= {}
options[:headers]['Accept'] = 'application/vnd.api+json'
options[:headers]['Accept-Language'] = 'nl,en'
token.send(http_method, endpoint, options)
end
# Sends a request to the BookingExperts jsonapi and returns the parsed json (String -> Hash)
def request_json(http_method, endpoint, token: current_token, options: {})
options[:body] = options[:body].to_json if options[:body].is_a?(Hash)
options[:headers] ||= {}
options[:headers]['Content-Type'] ||= 'application/json'
request(http_method, endpoint, token: token, options: options).parsed
end
# Request the complete collection of a resource. Keeps querying the API until no next page link is returned anymore.
# Returns the collection as an array in 'data', returns included resources as an array in 'included'.
def request_all(collection_endpoint, page_size: 100, options: {})
data = Set.new
included = Set.new
request_uri = Addressable::URI.parse(collection_endpoint)
request_uri.query_values = (request_uri.query_values || {}).merge('page[size]' => page_size, 'page[number]' => 1)
current_page_link = request_uri.to_s
loop do
response = request_json(:get, current_page_link, options: options)
data += response['data'].to_a
included += response['included'].to_a
current_page_link = response.dig('links', 'next')
break if current_page_link.nil?
end
{
'data' => data.to_a,
'included' => included.to_a
}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment