Last active
March 9, 2021 16:08
-
-
Save moiristo/d7e9463afd8b8ffa99ac4ab81bb1e617 to your computer and use it in GitHub Desktop.
BookingExperts client - ruby example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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