module Procore::Requestable

Module which defines HTTP verbs GET, POST, PUT, PATCH and DELETE. Is included in Client. Has support for Idempotency Tokens on POST and PATCH.

@example Using get:

client.get("my_open_items", per_page: 5)

@example Using post:

client.post("projects", name: "New Project")

Constants

HTTP_EXCEPTIONS

Public Instance Methods

delete(path, version: nil, query: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param query [Hash] Query options to pass along with the request @option options [String] :company_id

@example Usage

client.delete("users/1", query: {}, options: {})

@return [Response]

# File lib/procore/requestable.rb, line 226
def delete(path, version: nil, query: {}, options: {})
  full_path = full_path(path, version)

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "DELETE",
    headers: headers(options),
    query: query.to_s,
  )

  with_response_handling do
    RestClient::Request.execute(
      method: :delete,
      url: full_path,
      headers: headers.merge(params: query),
      timeout: Procore.configuration.timeout,
    )
  end
end
get(path, version: nil, query: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param query [Hash] Query options to pass along with the request @option options [Hash] :company_id

@example Usage

client.get("my_open_items", query: { per_page: 5, filter: {} })

@return [Response]

# File lib/procore/requestable.rb, line 30
def get(path, version: nil, query: {}, options: {})
  full_path = full_path(path, version)

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "GET",
    query: query.to_s,
  )

  with_response_handling do
    RestClient::Request.execute(
      method: :get,
      url: full_path,
      headers: headers(options).merge(params: query),
      timeout: Procore.configuration.timeout,
    )
  end
end
patch(path, version: nil, body: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param body [Hash] Body parameters to send with the request @param options [Hash] Extra request options @option options [String] :idempotency_token | :company_id

@example Usage

client.patch(
  "users/1",
  body: { name: "Updated" },
  options: { idempotency_token: "key", company_id: 1 },
)

@return [Response]

# File lib/procore/requestable.rb, line 130
def patch(path, version: nil, body: {}, options: {})
  full_path = full_path(path, version)

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "PATCH",
    body: body.to_s,
  )

  with_response_handling(request_body: body) do
    RestClient::Request.execute(
      method: :patch,
      url: full_path,
      payload: payload(body),
      headers: headers(options),
      timeout: Procore.configuration.timeout,
    )
  end
end
post(path, version: nil, body: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param body [Hash] Body parameters to send with the request @param options [Hash] Extra request options @option options [String] :idempotency_token | :company_id

@example Usage

client.post(
  "users",
  body: { name: "New User" },
  options: { idempotency_token: "key", company_id: 1 },
)

@return [Response]

# File lib/procore/requestable.rb, line 64
def post(path, version: nil, body: {}, options: {})
  full_path = full_path(path, version)

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "POST",
    body: body.to_s,
  )

  with_response_handling(request_body: body) do
    RestClient::Request.execute(
      method: :post,
      url: full_path,
      payload: payload(body),
      headers: headers(options),
      timeout: Procore.configuration.timeout,
    )
  end
end
put(path, version: nil, body: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param body [Hash] Body parameters to send with the request @param options [Hash] Extra request options @option options [String] :idempotency_token | :company_id

@example Usage

client.put("dashboards/1/users", body: [1,2,3], options: { company_id: 1 })

@return [Response]

# File lib/procore/requestable.rb, line 95
def put(path, version: nil, body: {}, options: {})
  full_path = full_path(path, version)

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "PUT",
    body: body.to_s,
  )

  with_response_handling(request_body: body) do
    RestClient::Request.execute(
      method: :put,
      url: full_path,
      payload: payload(body),
      headers: headers(options),
      timeout: Procore.configuration.timeout,
    )
  end
end
sync(path, version: nil, body: {}, options: {}) click to toggle source

@param path [String] URL path @param version [String] API version @param body [Hash] Body parameters to send with the request @param options [Hash] Extra request options @option options [String | Integer] :company_id | :batch_size

@example Usage

client.sync(
  "projects/sync",
  body: {
    updates: [
     { id: 1, name: "Update 1" },
     { id: 2, name: "Update 2" },
     { id: 3, name: "Update 3" },
     ...
     ...
     { id: 5055, name: "Update 5055" },
    ]
  },
  options: { batch_size: 500, company_id: 1 },
)

@return [Response]

# File lib/procore/requestable.rb, line 174
def sync(path, version: nil, body: {}, options: {})
  full_path = full_path(path, version)

  batch_size = options[:batch_size] ||
    Procore.configuration.default_batch_size

  if batch_size > 1000
    batch_size = 1000
  end

  Util.log_info(
    "API Request Initiated",
    path: full_path,
    method: "SYNC",
    batch_size: batch_size,
  )

  groups = body[:updates].each_slice(batch_size).to_a

  responses = groups.map do |group|
    batched_body = body.merge(updates: group)
    with_response_handling(request_body: batched_body) do
      RestClient::Request.execute(
        method: :patch,
        url: full_path,
        payload: payload(batched_body),
        headers: headers(options),
        timeout: Procore.configuration.timeout,
      )
    end
  end

  Procore::Response.new(
    body: responses.reduce({}) do |combined, response|
      combined.deep_merge(response.body) { |_, v1, v2| v1 + v2 }
    end.to_json,
    headers: responses.map(&:headers).inject({}, &:deep_merge),
    code: 200,
    request: responses.last&.request,
    request_body: body,
  )
end

Private Instance Methods

full_path(path, version) click to toggle source
# File lib/procore/requestable.rb, line 372
def full_path(path, version)
  version ||= options[:default_version]
  if version == "vapid"
    File.join(base_api_path, "vapid", path)
  elsif /\Av\d+\.\d+\z/.match?(version)
    File.join(base_api_path, "rest", version, path)
  else
    raise Procore::InvalidRequestError.new "#{version} is an invalid Procore API version"
  end
end
headers(options = {}) click to toggle source
# File lib/procore/requestable.rb, line 341
def headers(options = {})
  {
    "Accepts" => "application/json",
    "Authorization" => "Bearer #{access_token}",
    "Content-Type" => "application/json",
    "Procore-Sdk-Version" => Procore::VERSION,
    "Procore-Sdk-Language" => "ruby",
    "User-Agent" => Procore.configuration.user_agent,
  }.tap do |headers|
    if options[:idempotency_token]
      headers["Idempotency-Token"] = options[:idempotency_token]
    end

    if options[:company_id]
      headers["procore-company-id"] = options[:company_id]
    end
  end
end
multipart?(body) click to toggle source
# File lib/procore/requestable.rb, line 368
def multipart?(body)
  RestClient::Payload::has_file?(body)
end
payload(body) click to toggle source
# File lib/procore/requestable.rb, line 360
def payload(body)
  if multipart?(body)
    body
  else
    body.to_json
  end
end
with_response_handling(request_body: nil) { || ... } click to toggle source
# File lib/procore/requestable.rb, line 249
def with_response_handling(request_body: nil)
  request_start_time = Time.now
  retries = 0

  begin
    result = yield
  rescue *HTTP_EXCEPTIONS => e
    if retries <= Procore.configuration.max_retries
      retries += 1
      sleep 1.5**retries
      retry
    else
      raise APIConnectionError.new(
        "Cannot connect to the Procore API. Double check your timeout "    \
        "settings to ensure requests are not being cancelled before they " \
        "can complete. Try setting the timeout and max_retries to larger " \
        "values.",
      ), e
    end
  rescue RestClient::ExceptionWithResponse => e
    result = e.response
  end

  response = Procore::Response.new(
    body: result.body,
    headers: result.headers,
    code: result.code,
    request: result.request,
    request_body: request_body,
  )

  case result.code
  when 200..299
    Util.log_info(
      "API Request Finished ",
      path: result.request.url,
      status: result.code.to_s,
      duration: "#{((Time.now - request_start_time) * 1000).round(0)}ms",
      request_id: result.headers["x-request-id"],
    )
  else
    Util.log_error(
      "API Request Failed",
      path: result.request.url,
      status: result.code.to_s,
      duration: "#{((Time.now - request_start_time) * 1000).round(0)}ms",
      request_id: result.headers["x-request-id"],
      retries: retries,
    )
  end

  case result.code
  when 200..299
    response
  when 401
    raise Procore::AuthorizationError.new(
      "The request failed because you lack the correct credentials to "    \
      "access the target resource",
      response: response,
    )
  when 403
    raise Procore::ForbiddenError.new(
      "The request failed because you lack the required permissions",
      response: response,
    )
  when 404
    raise Procore::NotFoundError.new(
      "The URI requested is invalid or the resource requested does not "   \
      "exist.",
      response: response,
    )
  when 400, 422
    raise Procore::InvalidRequestError.new(
      "Bad Request.",
      response: response,
    )
  when 429
    raise Procore::RateLimitError.new(
      "You have surpassed the max number of requests for an hour. Please " \
      "wait until your limit resets.",
      response: response,
    )
  else
    raise Procore::ServerError.new(
      "Something is broken. This is usually a temporary error - Procore "  \
      "may be down or this endpoint may be having issues. Check "          \
      "http://status.procore.com for any known or ongoing issues.",
      response: response,
    )
  end
end