Module: Booqable::HTTP

Included in:
Client
Defined in:
lib/booqable/http.rb

Overview

HTTP request methods for Client

Provides low-level HTTP methods for making requests to the Booqable API. Handles authentication, pagination, rate limiting, and response parsing. All methods support both query parameters and request bodies as appropriate.

Examples:

Making a GET request

client.get("/orders", include: "customer")

Making a POST request with data

client.post("/orders", data: { type: "order", attributes: { name: "New Order" } })

Using pagination

orders = client.paginate("/orders", page: { size: 50 })

Constant Summary collapse

CONVENIENCE_HEADERS =

Headers that can be passed as top-level options for convenience

Set.new(i[accept content_type user_agent])

Instance Method Summary collapse

Instance Method Details

#agentSawyer::Agent

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get or create the Sawyer agent for API requests

Returns a memoized Sawyer::Agent configured with the API endpoint, serializer, and optional logging. Sawyer handles the low-level HTTP communication and response parsing.

Returns:

  • (Sawyer::Agent)

    HTTP agent instance



252
253
254
255
256
257
# File 'lib/booqable/http.rb', line 252

def agent
  @agent ||= Sawyer::Agent.new(api_endpoint,
                               sawyer_options) do |agent|
     agent.response :logger, logger, bodies: true if logger
  end
end

#api_endpointString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get the full API endpoint URL

Constructs the complete API endpoint URL from the configured company, domain, protocol, and API version. Validates that the API version is supported and that a company is specified.

Returns:

  • (String)

    Complete API endpoint URL

Raises:



365
366
367
368
369
370
371
# File 'lib/booqable/http.rb', line 365

def api_endpoint
  @api_endpoint ||= begin
                      raise UnsupportedAPIVersion unless %w[4 boomerang].include?(api_version.to_s)
                      raise CompanyRequired if company_id.nil? || company_id.empty?
                      Addressable::URI.join("#{api_protocol}://#{company_id}.#{api_domain}/", "api/", api_version.to_s).to_s
                    end
end

#default_headersHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get default HTTP headers for requests

Returns the standard headers that should be included with every API request, including content type, accept headers, and user agent.

Returns:

  • (Hash)

    Default headers hash



191
192
193
194
195
196
197
# File 'lib/booqable/http.rb', line 191

def default_headers
  {
    accept: default_media_type,
    content_type: default_media_type,
    user_agent: user_agent
  }
end

#delete(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP delete request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Body and header params for request

Returns:

  • (Sawyer::Resource)


61
62
63
# File 'lib/booqable/http.rb', line 61

def delete(url, options = {})
  request :delete, url, options
end

#faradayFaraday::Connection

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get or create the Faraday connection instance

Returns a memoized Faraday connection configured with the appropriate middleware stack, authentication, and connection options.

Returns:

  • (Faraday::Connection)

    HTTP connection instance



180
181
182
# File 'lib/booqable/http.rb', line 180

def faraday
  @faraday ||= Faraday.new(faraday_options)
end

#faraday_builderFaraday::RackBuilder

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get or create the Faraday middleware builder

Creates a middleware stack with authentication and optionally removes retry middleware based on configuration. The builder is memoized for performance.

Returns:

  • (Faraday::RackBuilder)

    Middleware builder instance



234
235
236
237
238
239
240
241
242
# File 'lib/booqable/http.rb', line 234

def faraday_builder
  @faraday_builder ||= @middleware.dup.tap do |builder|
    inject_auth_middleware(builder)
    # Remove retry middleware if no_retries is enabled
    if no_retries
      builder.handlers.delete_if { |handler| handler.klass == Faraday::Retry::Middleware }
    end
  end
end

#faraday_optionsHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get Faraday connection options

Builds the configuration hash for the Faraday connection including URL, middleware builder, proxy settings, and SSL verification options.

Returns:

  • (Hash)

    Faraday connection options



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/booqable/http.rb', line 206

def faraday_options
  opts = connection_options ||  { headers: default_headers }

  opts[:url] = api_endpoint
  opts[:builder] = faraday_builder
  opts[:proxy] = proxy if proxy

  if opts[:ssl].nil?
    opts[:ssl] = { verify_mode: @ssl_verify_mode } if @ssl_verify_mode
  else
    verify = connection_options[:ssl][:verify]
    opts[:ssl] = {
      verify: verify,
      verify_mode: verify == false ? 0 : @ssl_verify_mode
    }
  end

  opts
end

#get(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP GET request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Query and header params for request

Returns:

  • (Sawyer::Resource)


25
26
27
# File 'lib/booqable/http.rb', line 25

def get(url, options = {})
  request :get, url, options
end

#head(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP head request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Query and header params for request

Returns:

  • (Sawyer::Resource)


70
71
72
# File 'lib/booqable/http.rb', line 70

def head(url, options = {})
  request :head, url, options
end

#last_responseSawyer::Response?

Response for the last HTTP request

Returns:

  • (Sawyer::Response, nil)

    Last response object or nil if no request was made



124
125
126
# File 'lib/booqable/http.rb', line 124

def last_response
  @last_response
end

#last_response_bodyHash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get the decoded body of the last response

Parses the raw response body from the last HTTP request using the JSON:API serializer. Returns nil if no response is available.

Returns:

  • (Hash, nil)

    Parsed response body or nil



308
309
310
# File 'lib/booqable/http.rb', line 308

def last_response_body
  sawyer_serializer.decode(@last_response.body) if @last_response
end

#loggerLogger?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get logger instance for debug output

Creates a logger that outputs to STDOUT with DEBUG level when debug mode is enabled. Returns nil when debug is disabled.

Returns:

  • (Logger, nil)

    Logger instance or nil if debug is disabled



266
267
268
269
270
# File 'lib/booqable/http.rb', line 266

def logger
  @logger ||= Logger.new(STDOUT).tap do |logger|
    logger.level = Logger::DEBUG
  end if debug?
end

#normalized_path(path) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Normalize a path to ensure it is a valid URL path

Removes leading slashes and normalizes the path to prevent directory traversal attacks and ensure consistent formatting.

Parameters:

  • path (String)

    The path to normalize

Returns:

  • (String)

    Normalized path without leading slash



116
117
118
119
# File 'lib/booqable/http.rb', line 116

def normalized_path(path)
  relative_path = path.to_s.sub(%r{^/}, "") # Remove leading slash
  Addressable::URI.parse(relative_path.to_s).normalize.to_s
end

#paginate(url, options = {}) ⇒ Sawyer::Resource

Make a paginated request to the Booqable API.

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Query and header params for request

  • block (Block)

    Block to perform the data concatination of the multiple requests. The block is called with two parameters, the first contains the contents of the requests so far and the second parameter contains the latest response.

Returns:

  • (Sawyer::Resource)


137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/booqable/http.rb', line 137

def paginate(url, options = {})
  if @per_page || @auto_paginate
    options[:page] ||= {}
    options[:page][:size] ||= @per_page || (@auto_paginate ? 25 : nil)
    options[:page][:number] ||= 1
    options[:stats] ||= { total: "count" } # otherwise we don't get the total count in the response
  end

  data = request(:get, url, options)[:data]

  if @auto_paginate && total_present_in_stats?
    # While there are more results to fetch, and we have not hit the rate limit
    while total_count = last_response_body[:meta][:stats][:total][:count] > data.length && rate_limit.remaining > 0
      options[:page][:number] = options[:page][:number] + 1

      request(:get, url, options.dup)

      data.concat(@last_response.data[:data]) if @last_response.data[:data].is_a?(Array)
    end
  end

  data
end

#parse_options_with_convenience_headers(options) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse options and extract convenience headers

Processes request options to extract convenience headers (like accept, content_type, user_agent) from the top level and moves them to the headers hash for proper HTTP header handling.

Parameters:

  • options (Hash)

    Request options that may contain convenience headers

Returns:

  • (Hash)

    Processed options with headers properly organized



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/booqable/http.rb', line 321

def parse_options_with_convenience_headers(options)
  headers = options.delete(:headers) || {}

  CONVENIENCE_HEADERS.each do |h|
    if header = options.delete(h)
      headers[h] = header
    end
  end

  query = options.delete(:query) || {}

  opts = { query: options }
  opts[:query].merge!(query) if query.is_a?(Hash)
  opts[:headers] = headers unless headers.empty?
  opts
end

#patch(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP patch request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Body and header params for request

Returns:

  • (Sawyer::Resource)


52
53
54
# File 'lib/booqable/http.rb', line 52

def patch(url, options = {})
  request :patch, url, options
end

#post(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP post request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Body and header params for request

Returns:

  • (Sawyer::Resource)


34
35
36
# File 'lib/booqable/http.rb', line 34

def post(url, options = {})
  request :post, url, options
end

#put(url, options = {}) ⇒ Sawyer::Resource

Make a HTTP put request

Parameters:

  • url (String)

    The path, relative to #api_endpoint

  • options (Hash) (defaults to: {})

    Body and header params for request

Returns:

  • (Sawyer::Resource)


43
44
45
# File 'lib/booqable/http.rb', line 43

def put(url, options = {})
  request :put, url, options
end

#rate_limitRateLimit

Get rate limit information from the last response

Extracts rate limiting information from the HTTP headers of the most recent API response. This includes remaining requests, reset time, and limit information.

Returns:

  • (RateLimit)

    Rate limit information object

See Also:



169
170
171
# File 'lib/booqable/http.rb', line 169

def rate_limit
  RateLimit.from_response(@last_response)
end

#request(method, path, data, options = {}) ⇒ Sawyer::Resource

Make a HTTP request to the Booqable API

Low-level request method that handles authentication, error handling, and response processing. All other HTTP methods delegate to this method.

Examples:

Making a custom request

client.request(:get, "/orders", { include: "customer" })

Parameters:

  • method (Symbol)

    HTTP method (e.g. :get, :post, :put, :delete)

  • path (String)

    API endpoint path (e.g. “/products”), relative to #api_endpoint

  • data (Hash, String)

    Request body data (JSON or form-encoded)

  • options (Hash) (defaults to: {})

    Additional request options (headers, etc.)

Returns:

  • (Sawyer::Resource)

    Response object with data and metadata

Raises:



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/booqable/http.rb', line 88

def request(method, path, data, options = {})
  if data.is_a?(Hash) && options.empty?
    data[:headers] = default_headers.merge(data.delete(:headers) || {})

    if accept = data.delete(:accept)
      data[:headers][:accept] = accept
    end

    options = data.dup
  end

  options = parse_options_with_convenience_headers(options) if [ :get, :head ].include?(method)

  @last_response = response = agent.call(method, normalized_path(path), data, options)
  response_data_with_correct_encoding(response)
rescue Booqable::Error => e
  @last_response = nil
  raise e
end

#response_data_with_correct_encoding(response) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Ensure response data has correct character encoding

Reads the charset from the Content-Type header and forces the correct encoding on string response data. Returns the response data unchanged if no charset is specified or data is not a string.

Parameters:

  • response (Sawyer::Response)

    HTTP response object

Returns:

  • (Object)

    Response data with correct encoding



347
348
349
350
351
352
353
# File 'lib/booqable/http.rb', line 347

def response_data_with_correct_encoding(response)
  content_type = response.headers.fetch("content-type", "")
  return response.data unless content_type.include?("charset") && response.data.is_a?(String)

  reported_encoding = content_type.match(/charset=([^ ]+)/)[1]
  response.data.force_encoding(reported_encoding)
end

#sawyer_optionsHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get configuration options for Sawyer agent

Returns the configuration hash for the Sawyer agent including the Faraday connection, link parser, and JSON:API serializer.

Returns:

  • (Hash)

    Sawyer agent configuration options



279
280
281
282
283
284
285
286
287
# File 'lib/booqable/http.rb', line 279

def sawyer_options
  {
    faraday: faraday,
    # simple link parser
    link_parser: Sawyer::LinkParsers::Simple.new,
    # use our own JSON API serializer
    serializer: sawyer_serializer
  }
end

#sawyer_serializerBooqable::JsonApiSerializer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get the JSON:API serializer for Sawyer

Returns the configured JSON:API serializer that handles encoding and decoding of request/response bodies according to the JSON:API specification.

Returns:



297
298
299
# File 'lib/booqable/http.rb', line 297

def sawyer_serializer
  Booqable::JsonApiSerializer.any_json
end

#total_present_in_stats?Boolean

Checks if the total count is present in the last response stats.

Returns:

  • (Boolean)

    true if total count is present, false otherwise



376
377
378
379
380
381
# File 'lib/booqable/http.rb', line 376

def total_present_in_stats?
  last_response_body &&
    last_response_body[:meta] &&
    last_response_body[:meta][:stats] &&
    last_response_body[:meta][:stats][:total]
end