Packages

Library to help with writing concise views in Phoenix REST projects

Current section

Files

Jump to
simple_rest_view lib simple_rest_view.ex
Raw

lib/simple_rest_view.ex

defmodule SimpleRestView do
alias User
@moduledoc """
Module takes care of creating a map which can be converted into json from a schema obtained in database query.\n
Meant to make phoenix views shorter and more concise in REST projects
"""
@adding_keyword :add
@include_timestamps_keyword :include_timestamps
@only_keyword :only
@except_keyword :except
@many_keyword :many
@timestamp_fields [:inserted_at, :updated_at]
@doc """
Function renders all fields of a given schema into a map that can be converted into json.
It supports rendering fields of nested objects if specified with add optional parameter.\n
Optional parameters are:\n
many - true/false\n
only - [:field1, :field2]\n
except - [:field1, :field2]\n
include_timestamps - true/false\n
add - [field_name: {SchemaReference, :field_name}] OR [field_name: {SchemaReference, :field_name, opt}] OR [field_name: (fn schema -> ... end)] OR [field_name: %{custom_field: custom_val, ...}]
## Examples
iex> render_schema(User, %User{id: 1, username: "joe", email: "joe@mail.com", ...})
%{id: 1, username: "joe", email: "joe@mail.com", ...}
iex> render_schema(User, %User{id: 1, username: "joe", email: "joe@mail.con", reviewed: [%Review{...}, %Review{...}]},
except: [:email, :password], add: [avg_rating: (fn user -> get_avg_rating(user.id)),
reviewed: {Review, :reviewed, many: true, only: [:comment]}])
%{id: 1, username: "joe", avg_rating: 10, reviewed: [%{comment: "..."}, %{comment: "..."}]}
iex> render_schema(User, %User{id: 1, username: "joe", email: "joe@mail.com", reviewed: [%Review{..., reviewer: %User{username: "mary", ...}}, %Review{..., reviewer: %User{username: "johnson", ...}}]},
except: [:email, :password], add: [avg_rating: (fn user -> get_avg_rating(user.id)),
reviewed: {Review, :reviewed, many: true, only: [:comment],
add: [reviewed_by: {User, :reviewer, only: [:username]}]}])
%{id: 1, username: "joe", avg_rating: 10,
reviewed: [%{comment: "...", reviewed_by: %{username: "mary"}},
%{comment: "...", reviewed_by: %{username: "johnson"}}]}
"""
def render_schema(schema_ref, schema_map, opt \\ []) do
cond do
opt[@many_keyword] == true -> render_many_schema(schema_ref, schema_map, opt)
true -> render_single_schema(schema_ref, schema_map, opt)
end
end
@doc """
Wraps a map inside of map with a data tag
## Examples
iex> render_wrapper([%{id: 1, username: "joe"}, %{id: 2, username: "mary"}])
%{data: [%{id: 1, username: "joe"}, %{id: 2, username: "mary"}]}
"""
def render_wrapper(data) do
%{data: data}
end
@doc """
Wraps the result with pagination data obtained when using :scrivener library
## Examples
iex> render_paginated_wrapper([%{id: 1, username: "joe"}, %{id: 2, username: "mary"}], %Scrivener.Page{page_number: 1, page_size: 2, total_pages: 5}, except: [:total_entries])
%{data: [%{id: 1, username: "joe"}, %{id: 2, username: "mary"}],
page_number: 1, page_size: 2, total_pages: 5}
"""
def render_paginated_wrapper(data, query_result, opt \\ []) do
%{data: data,
page_number: value_or_nil(query_result, :page_number),
page_size: value_or_nil(query_result, :page_size),
total_entries: value_or_nil(query_result, :total_entries),
total_pages: value_or_nil(query_result, :total_pages)}
|> handle_base_fields(opt)
end
defp render_many_schema(schema_ref, schema_list_of_maps, opt) do
schema_list_of_maps
|> Enum.map(fn schema_map ->
render_single_schema(schema_ref, schema_map, convert_options(schema_map, opt))
end)
end
defp render_single_schema(schema_ref, schema_map, opt) do
converted_opts = convert_options(schema_map, opt)
schema_ref.__schema__(:fields)
|> Enum.map(fn key -> {key, value_or_nil(schema_map, key)} end)
|> Enum.reduce(%{}, fn {key, val}, acc -> Map.put(acc, key, val) end)
|> handle_options(converted_opts)
end
# Function takes care of converting data from current level and all nested levels
# into maps that can be send from server.
# This is where the magic happens!
defp convert_options(schema_map, opt) do
if opt[@adding_keyword] != nil do
opt[@adding_keyword]
|> Enum.reduce(opt, fn adding_options, accumulated_options ->
case adding_options do
# Rendering nested field
{key, {schema_ref, field_atom, inner_options}} ->
renewed_adding_options = handle_rendering_nested_schema(key, schema_ref, schema_map, field_atom, inner_options)
concatenate_options(accumulated_options, renewed_adding_options)
# Rendering nested field without options passed
{key, {schema_ref, field_atom}} ->
renewed_adding_options = handle_rendering_nested_schema(key, schema_ref, schema_map, field_atom)
concatenate_options(accumulated_options, renewed_adding_options)
# Adding field with a custom function
{key, custom_func} when is_function(custom_func) ->
concatenate_options(accumulated_options, [{@adding_keyword, [{key, custom_func.(schema_map)}]}])
_ -> accumulated_options
end
end)
else
opt
end
end
defp concatenate_options(full_options, options_to_concat) do
current_add = full_options[@adding_keyword]
if current_add != nil do
to_add = Keyword.merge(current_add, options_to_concat[@adding_keyword])
{_, new_keyword_list} = Keyword.pop(full_options, @adding_keyword)
new_keyword_list
|> Keyword.put(@adding_keyword, to_add)
else
full_options ++ options_to_concat
end
end
defp handle_rendering_nested_schema(key, schema_ref, schema_map, field_name, options \\ []) do
schema_to_render = value_or_nil(schema_map, field_name)
inner_schema = render_schema(schema_ref, schema_to_render, options)
[{@adding_keyword, [{key, inner_schema}]}]
end
defp value_or_nil(map, key) do
with {:ok, val} <- Map.fetch(map, key) do
val
else _ ->
nil
end
end
# Function takes care of rendering a map based on options passed
# Doesn't handle rendering on nested fields
defp handle_options(map, opt) do
map
|> handle_base_fields(opt)
|> handle_adding_fields(opt[@adding_keyword])
|> handle_timestamps(opt[@include_timestamps_keyword])
end
defp handle_base_fields(map, opt) do
cond do
opt[@only_keyword] -> if is_atom(opt[@only_keyword]), do: map |> Map.take([opt[@only_keyword]]), else: map |> Map.take(opt[@only_keyword])
opt[@except_keyword] -> if is_atom(opt[@except_keyword]), do: map |> Map.drop([opt[@except_keyword]]), else: map |> Map.drop(opt[@except_keyword])
true -> map
end
end
# Functions handle adding fields which get passed in opt parameter.
# It doesn't convert values and doesn't handle adding for nested fields
defp handle_adding_fields(map, {key, value}),
do: map |> Map.put(key, value)
defp handle_adding_fields(map, list_of_key_values) when is_list(list_of_key_values),
do: list_of_key_values |> Enum.reduce(map, fn key_value_pair, acc -> handle_adding_fields(acc, key_value_pair) end)
defp handle_adding_fields(map, _), do: map
defp handle_timestamps(map, true), do: map
defp handle_timestamps(map, false), do: map |> Map.drop(@timestamp_fields)
defp handle_timestamps(map, _), do: handle_timestamps(map, false)
end