Packages

An Elixir wrapper for the Stories API, by Clockk.com Inc.

Current section

Files

Jump to
stories lib resource.ex
Raw

lib/resource.ex

defmodule Stories.Resource do
@moduledoc """
All Stories requests require authorization headers. These can be set by deafult in your config file:
## Example Config
config :stories,
api_key: "your api key"
The authorization headers can be customized for each request using the following options provided as a keyword list in each resource function:
## Shared Authorization Options
* `:access_token` - Required. Accepts string. Can be set in config
"""
def get_auth_headers() do
case Application.fetch_env(:stories, :acccess_token) do
:error ->
raise(StoriesError, "Error: missing :access_token environment variable")
{:ok, access_token} ->
[{"Authorization", "Bearer #{access_token}"}]
end
end
defmacro __using__(opts) do
api_url =
unless Keyword.get(opts, :api_url_override) do
quote do
defp api_url(options \\ []) do
"https://api.getstories.io/v1"
end
end
end
pagination_data =
unless Keyword.get(opts, :pagination_data_override) do
quote do
defp pagination_data(resource), do: resource
end
end
resource_path =
unless Keyword.get(opts, :resource_path_override) do
quote do
# Default to simply return the resource name as the resource path
defp resource_path() do
resource()
end
end
end
inject =
case Keyword.get(opts, :only) do
nil -> [:get, :list, :create, :update, :delete]
list -> list
end
# list/1 requires get/1
get =
if :get in inject || :list in inject do
quote do
@doc """
Retrieve #{__MODULE__} by id. Accepts a keyword list of options.
returns `%#{__MODULE__}{}`
## Options
* `:id` - The id of the resource you are trying to retrieve. Defaults to empty, giving same behaviour as list/1
* `:get_parameters` - Map of query parameters that you want to include in the query.
"""
def get(options \\ []) do
resource_id = Keyword.get(options, :id)
access_token = Keyword.get(options, :access_token)
url =
"#{api_url(options)}/#{resource_path()}"
|> (fn url ->
# Avoid the trailing "/" character when not necessary
if resource_id do
"#{url}/#{resource_id}"
else
url
end
end).()
|> (fn url ->
case Keyword.get(options, :get_parameters) do
nil ->
url
get_parameters ->
url <> "?" <> URI.encode_query(get_parameters)
end
end).()
case HTTPoison.get(url, Stories.Resource.get_auth_headers()) do
{:ok, %HTTPoison.Response{status_code: 404}} ->
nil
{:ok, response = %HTTPoison.Response{status_code: 200, body: resp_body}} ->
resource = Jason.decode!(resp_body, keys: :atoms)
if Map.has_key?(pagination_data(resource), :links) do
# This means the resource is a list of the resource, in an attribute named after the resource
resource_key = :data
if Application.get_env(:stories, :env, :prod) == :test do
# lets learn about new keys that stories adds to their data objects without crashing in prod
resource[resource_key]
|> Enum.map(&struct!(__MODULE__, &1))
else
resource[resource_key]
|> Enum.map(&struct(__MODULE__, &1))
end
else
struct!(__MODULE__, resource)
end
{:error, %HTTPoison.Error{reason: reason}} ->
raise(StoriesError, "Error: #{inspect(reason)}")
end
end
end
end
list =
if :list in inject do
quote do
defp get_recursive(
options \\ [],
collector \\ [],
page \\ 1,
per_page \\ 100
) do
get_parameters =
case Keyword.get(options, :get_parameters) do
nil ->
%{page: page, per_page: per_page}
get_parameters ->
Map.merge(get_parameters, %{page: page, per_page: per_page})
end
resources = get(options ++ [get_parameters: get_parameters])
if Enum.count(resources) == per_page do
get_recursive(
options,
resources ++ collector,
page + 1,
per_page
)
else
# all pages exhausted for resource. return collection
resources ++ collector
end
end
@doc """
Retrieve list of #{__MODULE__} Stories resource. Shares all options with get/1
returns `[%#{__MODULE__}{}]`
## Options
* `:page` - The page number to use in pagination. Default 1
* `:per_page` - The number of records to return per page. Currently only 50 is supported.
* `:recurse_pages` - Recursively follow each next page and return all records.
* `:get_parameters` - Map of query parameters that you want to include in the query.
"""
def list(options \\ []) do
page = Keyword.get(options, :page, 1)
per_page = Keyword.get(options, :per_page, 50)
if Keyword.get(options, :recurse_pages) do
get_recursive(options, [], page, per_page)
else
get(options ++ [get_parameters: %{page: page, per_page: per_page}])
end
end
end
end
create =
if :create in inject do
quote do
@doc """
Create new #{__MODULE__} resource. Parameters are not validated, but passed along in the request body. Please see Stories API documentation for the appropriate body parameters for your resource.
returns `%#{__MODULE__}{}`
## Params
* `properties` - Map of resource properties to create the resource with. This will be different for each resource based on the specification in Stories API
"""
def create(properties, options \\ []) do
url = "#{api_url(options)}/#{resource_path()}"
body =
case Jason.encode(properties) do
{:ok, body} ->
body
{:error, %Jason.EncodeError{message: message}} ->
raise(
StoriesError,
"Invalid :properties. Was not able to encode to JSON. #{message}"
)
end
case HTTPoison.post(
url,
body,
Stories.Resource.get_auth_headers() ++ [{"Content-Type", "application/json"}]
) do
{:ok, %HTTPoison.Response{body: resp_body}} ->
resource = Jason.decode!(resp_body, keys: :atoms)
struct!(__MODULE__, resource)
{:error, %HTTPoison.Response{status_code: 422, body: body}} ->
{:error, Jason.decode!(body)}
{:error, %HTTPoison.Error{reason: reason}} ->
raise(StoriesError, "Error: #{inspect(reason)}")
end
end
end
end
update =
if :update in inject do
quote do
@doc """
Update given #{__MODULE__} resource. Parameters are not validated, but passed along in the request body. Please see Stories API documentation for the appropriate body parameters for your resource.
returns `%#{__MODULE__}{}`
## Params
* `id` - id of `#{__MODULE__}` resource being updated.
* `changes` - Map of changes to be applied to the resource.
"""
def update(id, changes, options \\ []) do
url = "#{api_url(options)}/#{resource_path()}/#{id}"
body =
case Jason.encode(changes) do
{:ok, body} ->
body
{:error, %Jason.EncodeError{message: message}} ->
raise(
StoriesError,
"Invalid :changes. Was not able to encode to JSON. #{message}"
)
end
case HTTPoison.post(
url,
body,
Stories.Resource.get_auth_headers() ++ [{"Content-Type", "application/json"}]
) do
{:ok, resp = %HTTPoison.Response{body: resource}} ->
resource = Jason.decode!(resource, keys: :atoms)
struct!(__MODULE__, resource)
{:error, %HTTPoison.Response{status_code: 422, body: body}} ->
{:error, Jason.decode!(body)}
{:error, %HTTPoison.Error{reason: reason}} ->
raise(StoriesError, "Error: #{inspect(reason)}")
end
end
end
end
delete =
if :delete in inject do
quote do
@doc """
Delete Stories #{__MODULE__} with provided id.
Will return `:ok` on success, `{:error, 404}` if resource not found, or throw `StoriesError` for implementation errors.
## Params
* `id` - id of #{__MODULE__} resource to delete
"""
def delete(id, options \\ []) do
url = "#{api_url(options)}/#{resource_path()}/#{id}"
case HTTPoison.delete(url, Stories.Resource.get_auth_headers()) do
{:ok, resp} ->
case resp.status_code do
204 ->
:ok
404 ->
{:error, 404}
status_code when status_code > 399 ->
error = Jason.decode!(resp.body, keys: :atoms)
raise(StoriesError, error)
end
{:error, %HTTPoison.Response{status_code: 422, body: body}} ->
{:error, Jason.decode!(body)}
{:error, %HTTPoison.Error{reason: reason}} ->
raise(StoriesError, "Error: #{inspect(reason)}")
end
end
end
end
[api_url, pagination_data, resource_path, get, list, create, update, delete]
end
end