Packages

Toolbox that help to do trivial things faster. I have more free time for life

Current section

Files

Jump to
magik lib json_view.ex
Raw

lib/json_view.ex

defmodule Magik.JsonView do
defmacro __using__(opts \\ []) do
fields = Keyword.get(opts, :fields, [])
custom_fields = Keyword.get(opts, :custom_fields, [])
after_render = Keyword.get(opts, :after_render)
quote do
def render_json(struct, fields, custom_fields, relationships) do
data =
Magik.JsonView.render_json(struct, __MODULE__,
fields: unquote(fields) ++ fields,
custom_fields: unquote(custom_fields) ++ custom_fields,
relationships: relationships
)
if is_function(unquote(after_render)) do
apply(unquote(after_render), [data])
else
data
end
end
def render_view(struct, view, template) do
Magik.JsonView.render_template(struct, view, template)
end
end
end
@moduledoc """
Render a struct to a map with given options
- `fields`: which fields are extract directly from struct
- `custom_fields`: which fields are render using custom `render_field/2` function
- `relationships`: a list of {field, view_module} defines which fields are rendered using another view
Here is a sample view
defmodule MyApp.PostView do
use JsonView
@fields [:title, :content, :excerpt, :cover]
@custom_fields [:like_count]
@relationships [author: MyApp.AuthorView]
def render("post.json", %{post: post}) do
# 1st way if `use JsonView`
render_json(post, @fields, @custom_fields, @relationships)
# 2nd way same as above
JsonView.render_json(post, __MODULE__,
fields: @fields,
custom_fields: @custom_fields,
relationships: @relationships
)
# 3rd render manual
post
|> JsonView.render_fields(@fields)
|> Map.merge(JsonView.render_custom_fields(post, __MODULE__, @custom_fields))
|> Map.merge(JsonView.render_relationships(post, @relationships))
end
def render_field(:like_count, item) do
# load like_count from some where
end
end
"""
def render_json(nil, _, _), do: nil
def render_json(struct, view, opts) when is_list(opts) do
fields = Keyword.get(opts, :fields, [])
custom_fields = Keyword.get(opts, :custom_fields, [])
relationships = Keyword.get(opts, :relationships, [])
struct
|> render_fields(fields)
|> Map.merge(render_custom_fields(struct, view, custom_fields))
|> Map.merge(render_relationships(struct, relationships))
end
def render_fields(structs, fields) do
Map.take(structs, fields)
end
@doc """
Render field with custom render function
View module must defines `render_field/2` function to render each custom field
use JsonView
def render_field(:is_success, item) do
item.state > 3
end
render_custom_fields(struct, __MODULE__, [:is_success])
"""
def render_custom_fields(struct, view \\ nil, fields) do
# if fields is not empty and render_field/2 is not defined, raise exception
fields
|> Enum.map(fn
{field, render_func} when is_function(render_func) ->
{field, render_func.(struct)}
field ->
{field, view.render_field(field, struct)}
end)
|> Enum.into(%{})
end
@doc """
Render relationship field for struct. `relationships` is a list of {field, view} for mapping render.
For each field, call function `View.render()` to render json for relation object.
Example relationships:
relationships = [comments: CommentView, author: UserView]
Result of `render_relationships(post, relationships)` equal to output of below code
%{
comments: CommentView.render_many(comments, CommentView, "comment.json"),
autho: UserView.render_one(author, UserView, "user.json")
}
"""
def render_relationships(struct, relationships) when is_list(relationships) do
Enum.map(relationships, fn {field, view} ->
{field, render_relationship(struct, field, view)}
end)
|> Enum.into(%{})
end
# render a single relationship
def render_relationship(struct, field, {view, template}) do
references = Map.get(struct, field)
render_template(references, view, template)
end
def render_relationship(struct, field, view) do
references = Map.get(struct, field)
name = relationship_name(view)
render_template(references, view, "#{name}.json")
end
def render_template(struct, view, template) do
case struct do
%Ecto.Association.NotLoaded{} ->
nil
%{} ->
Phoenix.View.render_one(struct, view, template)
struct when is_list(struct) ->
Phoenix.View.render_many(struct, view, template)
_ ->
nil
end
end
# get relationship name. Ex: HeraWeb.ProductView -> product
# this value is used to map assign when render relationship
# render_one(product, HeraWeb.ProductView, "product.json")
defp relationship_name(view) do
view
|> Module.split()
|> Enum.map(&Phoenix.Naming.underscore/1)
|> List.last()
|> String.trim_trailing("_view")
end
end