# frozen_string_literal: true require 'base64' require 'faraday' require 'faraday-http-cache' require "bootic_client/errors" require 'faraday/net_http_persistent' module BooticClient class Client USER_AGENT = "[BooticClient v#{VERSION}] Ruby-#{RUBY_VERSION} - #{RUBY_PLATFORM}".freeze JSON_MIME = 'application/json'.freeze attr_reader :options def initialize(options = {}, &block) @options = { logging: false, faraday_adapter: [:net_http_persistent], user_agent: USER_AGENT }.merge(options.dup) @options[:cache_store] = @options[:cache_store] || Faraday::HttpCache::MemoryStore.new conn &block if block_given? end def get(href, query = {}, headers = {}) validated_request!(:get, href) do |req| req.headers.update headers req.params.update(query) end end def post(href, payload = {}, headers = {}) validated_request!(:post, href) do |req| req.headers.update headers req.body = JSON.dump(sanitized(payload)) end end def put(href, payload = {}, headers = {}) validated_request!(:put, href) do |req| req.headers.update headers req.body = JSON.dump(sanitized(payload)) end end def patch(href, payload = {}, headers = {}) validated_request!(:patch, href) do |req| req.headers.update headers req.body = JSON.dump(sanitized(payload)) end end def delete(href, _ = {}, headers = {}) validated_request!(:delete, href) do |req| req.headers.update headers end end class SafeCacheSerializer PREFIX = '__booticclient__base64__:'.freeze PREFIX_EXP = %r{^#{PREFIX}}.freeze def self.dump(data) data[:body] = "#{PREFIX}#{Base64.strict_encode64(data[:body])}" if data[:body].is_a?(String) JSON.dump(data) end def self.load(string) data = JSON.load(string) if data['body'] =~ PREFIX_EXP data['body'] = Base64.strict_decode64(data['body'].sub(PREFIX, '')) end data end end private def conn(&block) @conn ||= Faraday.new do |f| cache_options = {serializer: SafeCacheSerializer, shared_cache: false, store: options[:cache_store]} cache_options[:logger] = options[:logger] if options[:logging] f.use :http_cache, **cache_options f.response :logger, options[:logger] if options[:logging] yield f if block_given? f.adapter *Array(options[:faraday_adapter]) end end def request_headers { 'User-Agent' => options[:user_agent], 'Accept' => JSON_MIME, 'Content-Type' => JSON_MIME } end def validated_request!(verb, href, &block) resp = conn.send(verb) do |req| req.url href req.headers.update request_headers yield req if block_given? end raise_if_invalid! resp resp end def raise_if_invalid!(resp) raise ServerError, "Server Error" if resp.status > 499 raise NotFoundError, "Not Found" if resp.status == 404 raise UnauthorizedError, "Unauthorized request" if resp.status == 401 raise AccessForbiddenError, "Access Forbidden" if resp.status == 403 end def sanitized(payload) return payload unless payload.kind_of?(Hash) payload.each_with_object({}) do |(k, v), memo| memo[k] = if v.kind_of?(Hash) sanitized v elsif v.respond_to?(:read) Base64.encode64 v.read else v end end end end end