180 lines
4.8 KiB
Ruby
180 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Fiken
|
|
# Building blocks for the resource-oriented API surface.
|
|
#
|
|
# A resource is bound to a client and (for company-scoped resources) a company
|
|
# slug. Concrete resources subclass Base and mix in the verb modules they support.
|
|
module Resource
|
|
class Base
|
|
attr_reader :client, :company_slug
|
|
|
|
def initialize(client, company_slug = nil)
|
|
@client = client
|
|
@company_slug = company_slug
|
|
end
|
|
|
|
# The path segment for this resource, e.g. "contacts". Override in subclasses.
|
|
def resource_path
|
|
raise NotImplementedError, "#{self.class} must define #resource_path"
|
|
end
|
|
|
|
def base_path
|
|
if company_slug
|
|
"/companies/#{company_slug}/#{resource_path}"
|
|
else
|
|
"/#{resource_path}"
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def connection
|
|
client.connection
|
|
end
|
|
|
|
def wrap(body)
|
|
Object.new(body)
|
|
end
|
|
|
|
# GET a list endpoint, returning a paginated Collection.
|
|
def get_collection(path, params)
|
|
result = connection.get(path, params)
|
|
Collection.new(result, fetch_page: lambda { |page|
|
|
get_collection(path, params.merge(page: page))
|
|
})
|
|
end
|
|
|
|
# GET a single resource, returning a wrapped Object.
|
|
def get_one(path)
|
|
wrap(connection.get(path).body)
|
|
end
|
|
|
|
# POST to create. Fiken create endpoints typically return 201 with an empty
|
|
# body and a Location header; we return the parsed body when present, else an
|
|
# Object carrying the location and the id parsed from it.
|
|
def post_create(path, attributes = nil)
|
|
build_created(connection.post(path, attributes))
|
|
end
|
|
|
|
def put_one(path, attributes)
|
|
wrap(connection.put(path, attributes).body)
|
|
end
|
|
|
|
def patch_one(path, attributes = nil)
|
|
wrap(connection.patch(path, attributes).body)
|
|
end
|
|
|
|
def delete_path(path)
|
|
connection.delete(path)
|
|
true
|
|
end
|
|
|
|
def build_created(result)
|
|
body = result.body
|
|
return wrap(body) if body.is_a?(Hash) && !body.empty?
|
|
|
|
location = result.headers["Location"]
|
|
wrap("location" => location, "id" => location&.split("/")&.last)
|
|
end
|
|
|
|
def build_file_part(path, io, filename, content_type)
|
|
if path
|
|
Faraday::Multipart::FilePart.new(path, content_type, filename || File.basename(path))
|
|
elsif io
|
|
Faraday::Multipart::FilePart.new(io, content_type, filename)
|
|
else
|
|
raise ArgumentError, "provide path: or io:"
|
|
end
|
|
end
|
|
end
|
|
|
|
module Listable
|
|
def list(**params)
|
|
get_collection(base_path, params)
|
|
end
|
|
alias all list
|
|
end
|
|
|
|
module Findable
|
|
def find(id)
|
|
get_one("#{base_path}/#{id}")
|
|
end
|
|
alias retrieve find
|
|
end
|
|
|
|
module Creatable
|
|
def create(attributes)
|
|
post_create(base_path, attributes)
|
|
end
|
|
end
|
|
|
|
# Update via PUT (used for drafts and other full-replacement endpoints).
|
|
module Updatable
|
|
def update(id, attributes)
|
|
put_one("#{base_path}/#{id}", attributes)
|
|
end
|
|
end
|
|
|
|
# Update via PATCH (used for finalized invoices, projects, time entries, ...).
|
|
module PatchUpdatable
|
|
def update(id, attributes)
|
|
patch_one("#{base_path}/#{id}", attributes)
|
|
end
|
|
end
|
|
|
|
module Deletable
|
|
def delete(id)
|
|
delete_path("#{base_path}/#{id}")
|
|
end
|
|
end
|
|
|
|
# For documents that expose a /drafts sub-resource. The host defines
|
|
# #draft_create_action (the path segment that finalizes a draft).
|
|
module Draftable
|
|
def drafts
|
|
Resources::Concerns::Drafts.new(client, company_slug, base_path, draft_create_action)
|
|
end
|
|
end
|
|
|
|
# For resources that expose /{id}/attachments. Override #attachments_listable?
|
|
# for resources that only allow uploading (e.g. contacts).
|
|
module Attachable
|
|
def attachments(id)
|
|
Resources::Concerns::Attachments.new(
|
|
client, company_slug, "#{base_path}/#{id}", listable: attachments_listable?
|
|
)
|
|
end
|
|
|
|
def attachments_listable?
|
|
true
|
|
end
|
|
end
|
|
|
|
# For documents that expose /{id}/payments (sales, purchases).
|
|
module Payable
|
|
def payments(id)
|
|
Resources::Concerns::Payments.new(client, company_slug, "#{base_path}/#{id}")
|
|
end
|
|
end
|
|
|
|
# POST /<resource>/send. Named #dispatch to avoid overriding Object#send.
|
|
module Sendable
|
|
def dispatch(attributes)
|
|
post_create("#{base_path}/send", attributes)
|
|
end
|
|
end
|
|
|
|
# GET/POST /<resource>/counter.
|
|
module HasCounter
|
|
def counter
|
|
get_one("#{base_path}/counter")
|
|
end
|
|
|
|
def create_counter(attributes = nil)
|
|
post_create("#{base_path}/counter", attributes)
|
|
end
|
|
end
|
|
end
|
|
end
|