Add resource framework: Base, CRUD mixins, and shared drafts/attachments/payments concerns
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user