defmodule ExChargebee.Resource do
@moduledoc """
A macro for generating simple chargebee http interfaces.
Chargebee's API structure is generally consistent, but some endpoints are
decidedly atypical.
Typically Endpoints support the following Standard Operations:
- create (POST /resource) (mostly deprecated or internal)
- list (GET /resource)
- retrieve (GET /resource/:id)
- update (POST /resource/:id)
- delete (POST /resource/:id/delete)
To disable any of these operations, pass a list of operations to the `:stdops`
option.
Some endpoints support additional operations. To add these operations, pass a
list of operations to the `:get_operations`, `:post_operations`,
`:list_operations`, or `:post_root_operations` options.
Types of operations:
- Get Operations (GET /resource/:id/:operation)
- Post Operations (POST /resource/:id/:operation)
- List Operations (GET /resource/:operation)
- Post Root Operations (POST /resource/:operation)
A better name for "Post Root Operations" would be "Create Operations", but
that would be confusing because of the `create` operation.
Example:
```elixir
defmodule ExChargebee.Subscription do
use ExChargebee.Resource,
post_operations: [
:add_charge_at_term_end,
:cancel_for_items,
:change_term_end,
:charge_future_renewals,
:delete,
:edit_advance_invoice_schedule,
:import_contract_term,
:import_for_items,
:override_billing_profile,
:pause,
:reactivate,
:regenerate_invoice,
:remove_advance_invoice_schedule,
:remove_coupons,
:remove_scheduled_cancellation,
:remove_scheduled_changes,
:remove_scheduled_pause,
:remove_scheduled_resumption,
:resume,
:retrieve_advance_invoice_schedule,
:subscription_for_items,
:update_for_items
],
get_operations: [:contract_terms, :discounts, :retrieve_with_scheduled_changes]
def import_unbilled_charges(params, opts \\ []) do
post_resource("import_unbilled_charges", "/import_unbilled_charges", params, opts)
end
end
```
"""
alias ExChargebee.Interface
defmacro __using__(opts \\ []) do
module = __CALLER__.module
resource = module |> Module.split() |> List.last() |> Macro.underscore()
quote location: :keep do
alias ExChargebee.Interface
import ExChargebee.Resource
@resource __MODULE__
|> Module.split()
|> List.last()
|> Macro.underscore()
@resource_plural Inflex.pluralize(@resource)
defp resource_base_path(path \\ "")
defp resource_base_path("") do
"/#{@resource_plural}"
end
defp resource_base_path("/" <> _rest = path) do
"/#{@resource_plural}#{path}"
end
defp resource_base_path(path) do
"/#{@resource_plural}/#{path}"
end
defp resource_path(id, path \\ "")
defp resource_path(id, "") do
encoded_id = id |> to_string |> URI.encode()
"#{resource_base_path()}/#{encoded_id}"
end
defp resource_path(id, "/" <> _rest = path) do
encoded_id = id |> to_string |> URI.encode()
"#{resource_base_path()}/#{encoded_id}#{path}"
end
defp resource_path(id, path) do
encoded_id = id |> to_string |> URI.encode()
"#{resource_base_path()}/#{encoded_id}/#{path}"
end
# expose operations for testing
@spec operations() :: Keyword.t()
def operations do
Keyword.take(unquote(opts), [
:get_operations,
:post_operations,
:list_operations,
:post_root_operations,
:stdops
])
|> Keyword.update(:list_operations, [], &Enum.map(&1, fn v -> :"list_#{v}" end))
|> Keyword.update(:list_root_operations, [], &Enum.map(&1, fn v -> :"list_#{v}" end))
end
def operations(group) do
Keyword.get(operations(), group, [])
end
unquote(define_stdops(opts, resource))
unquote(define_operations(opts, resource))
end
end
defp define_operations(opts, resource) do
get_operations = Keyword.get(opts, :get_operations, [])
post_operations = Keyword.get(opts, :post_operations, [])
list_operations = Keyword.get(opts, :list_operations, [])
list_root_operations = Keyword.get(opts, :list__root_operations, [])
post_root_operations = Keyword.get(opts, :post_root_operations, [])
quote location: :keep do
unquote(generate_operations(get_operations, :get, resource))
unquote(generate_operations(post_operations, :post, resource))
unquote(generate_operations(list_operations, :list, resource))
unquote(generate_operations(list_root_operations, :list_root, resource))
unquote(generate_operations(post_root_operations, :create, resource))
end
end
defp define_stdops(opts, resource) do
stdops =
Keyword.get(opts, :stdops, [
:create,
:retrieve,
:list,
:update,
:delete
])
if stdops && is_list(stdops) do
for op <- Enum.uniq(stdops) do
defstdop(op, resource)
end
end
end
def generate_operations(operations, :list_root, resource) do
for operation <- operations do
operation_name = :"list_#{operation}"
quote location: :keep do
@doc """
Returns a list of #{@resource_plural} #{unquote(operation)}.
Pagination is handled automatically.
For more information see [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{Inflex.pluralize(unquote(resource))}##{unquote(operation_name)})
"""
@spec unquote(operation_name)(map(), keyword()) :: [map()] | nil
def unquote(operation_name)(params \\ %{}, opts \\ []) do
if Keyword.get(opts, :paginate, true) do
unquote(operation)
|> resource_base_path()
|> Interface.stream_list(params, opts, unquote(resource))
|> Enum.to_list()
else
unquote(operation)
|> resource_base_path()
|> Interface.get(params, opts)
|> Map.update("list", [], &Map.get(&1, unquote(resource)))
end
end
end
end
end
def generate_operations(operations, :list, resource) do
for operation <- operations do
operation_name = :"list_#{operation}"
quote location: :keep do
@doc """
Returns a list of #{@resource_plural} #{unquote(operation)}.
Pagination is handled automatically.
For more information see [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{Inflex.pluralize(unquote(resource))}##{unquote(operation_name)})
"""
@spec unquote(operation_name)(String.t(), map(), keyword()) :: [map()] | nil
def unquote(operation_name)(resource_id, params \\ %{}, opts \\ []) do
path = resource_path(resource_id, unquote(operation))
resource_name = Inflex.singularize(unquote(operation))
if Keyword.get(opts, :paginate, true) do
path
|> Interface.stream_list(params, opts, resource_name)
|> Enum.to_list()
else
path
|> Interface.get(params, opts)
|> Map.update("list", [], &Map.get(&1, resource_name))
end
end
end
end
end
def generate_operations(operations, :create, resource) do
for operation <- operations do
quote location: :keep do
@doc """
Perform a #{unquote(resource)} #{unquote(operation)}.
Find more information in [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{Inflex.pluralize(unquote(resource))}##{unquote(operation)})
"""
@spec unquote(operation)(map(), keyword()) :: map() | nil
def unquote(operation)(params, opts \\ []) do
unquote(operation)
|> resource_base_path()
|> Interface.post(params, opts)
|> Map.get(unquote(resource))
end
end
end
end
def generate_operations(operations, verb, resource) do
for operation <- operations do
{resource, operation} =
case operation do
{resource, operation} -> {resource, operation}
operation -> {resource, operation}
end
quote location: :keep do
@doc """
Perform a #{unquote(operation)} on individual #{unquote(resource)}.
Find more information in [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{Inflex.pluralize(unquote(resource))}##{unquote(operation)})
"""
@spec unquote(operation)(String.t(), map(), keyword()) :: map() | nil
def unquote(operation)(resource_id, params \\ %{}, opts \\ []) do
path = resource_path(resource_id, "/#{unquote(operation)}")
apply(Interface, unquote(verb), [path, params, opts])
|> Map.get(unquote(resource))
end
end
end
end
def defstdop(:retrieve, resource) do
quote location: :keep do
resource_plural = Inflex.pluralize(unquote(resource))
@moduledoc """
An interface for Interacting with #{resource_plural}
For More information see [Chargebee #{unquote(resource)} Documentation](https://apidocs.chargebee.com/docs/api/#{resource_plural})
"""
@spec retrieve(String.t(), keyword()) :: map() | nil
def retrieve(resource_id, opts \\ []) do
resource_path(resource_id, "")
|> Interface.get(opts)
|> Map.get(unquote(resource))
rescue
ExChargebee.NotFoundError -> nil
end
end
end
def defstdop(:list, resource) do
quote location: :keep do
resource_plural = Inflex.pluralize(unquote(resource))
@doc """
Returns a list of #{resource_plural}. Pagination is handled automatically unless the opt `paginate` is set to false.
opts:
- site: the site to use for the request. Defaults to the default site.
- paginate: whether to paginate the results. Defaults to false. If false,
all results will be returned.
"""
@spec list(map(), keyword()) :: [map()] | nil
def list(params \\ %{}, opts \\ []) do
if Keyword.get(opts, :paginate, true) do
stream_list(params, opts)
|> Enum.to_list()
else
resource_base_path()
|> Interface.get(params, opts)
|> Map.update("list", [], &Map.get(&1, unquote(resource)))
end
end
@doc """
Returns a stream of #{resource_plural}. Pagination is handled
automatically as the stream is consumed.
"""
@spec stream_list(map(), keyword()) :: Enumerable.t()
def stream_list(params \\ %{}, opts \\ []) do
Interface.stream_list(resource_base_path(), params, opts, unquote(resource))
end
end
end
def defstdop(:create, resource) do
quote location: :keep do
resource = unquote(resource)
resource_plural = Inflex.pluralize(unquote(resource))
@doc """
Creates a #{resource}.
Find more information in [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{resource_plural}#create_#{resource})
"""
@spec create(map(), keyword()) :: map() | nil
def create(params, opts \\ []) do
resource_base_path()
|> Interface.post(params, opts)
|> Map.get(unquote(resource))
end
end
end
def defstdop(:update, resource) do
quote location: :keep do
resource = unquote(resource)
resource_plural = Inflex.pluralize(unquote(resource))
@doc """
Updates a #{resource}.
Find more information in [the Chargebee Documentation](https://apidocs.chargebee.com/docs/api/#{resource_plural}#update_#{resource})
"""
@spec update(String.t(), map(), keyword()) :: map() | nil
def update(resource_id, params, opts \\ []) do
resource_id
|> resource_path()
|> Interface.post(params, opts)
|> Map.get(unquote(resource))
end
end
end
def defstdop(:delete, resource) do
# quote location: :keep do
ExChargebee.Resource.generate_operations([:delete], :post, resource)
# end
end
def defstdop(other, resource) do
quote location: :keep do
# raise an error if the operation is not supported
raise """
#{inspect(unquote(other))} is not a supported operation for #{unquote(resource)}
"""
end
end
end