Current section
Files
Jump to
Current section
Files
lib/jsonapi_plug.ex
defmodule JSONAPIPlug do
@moduledoc """
JSONAPIPlug context
This defines a struct for storing configuration and request data. `JSONAPIPlug.Plug` populates
its attributes by means of a number of other plug modules used to parse and validate requests
and stores it in the `Plug.Conn` private assings under the `jsonapi_plug` key.
"""
alias JSONAPIPlug.{Document, Normalizer, Resource}
alias JSONAPIPlug.Document.ResourceObject
alias Plug.Conn
@typedoc "String case"
@type case :: :camelize | :dasherize | :underscore
@typedoc "JSONAPIPlug context"
@type t :: %__MODULE__{
allowed_includes: keyword(keyword()),
config: Keyword.t(),
base_url: String.t(),
fields: term(),
filter: term(),
include: term(),
page: term(),
params: Conn.params(),
resource: Resource.t(),
sort: term()
}
defstruct allowed_includes: nil,
base_url: nil,
config: nil,
fields: nil,
filter: nil,
include: nil,
page: nil,
params: nil,
resource: nil,
sort: nil
@doc """
JSON:API MIME type
Returns the JSON:API MIME type.
"""
@spec mime_type :: String.t()
def mime_type, do: "application/vnd.api+json"
@doc """
Render JSON:API response
Renders the JSON:API response for the specified Resource.
"""
@spec render(
Conn.t(),
Resource.t() | [Resource.t()] | nil,
Document.links() | nil,
Document.meta() | nil,
Resource.options()
) ::
Document.t() | no_return()
def render(
conn,
resource_or_resources \\ nil,
links \\ nil,
meta \\ nil,
options \\ []
) do
Normalizer.normalize(conn, resource_or_resources, meta, links, options)
|> Document.serialize()
end
@doc """
Generate relationships link
Generates the relationships link for a resource.
"""
@spec url_for_relationship(
Resource.t() | [Resource.t()],
Conn.t() | nil,
ResourceObject.type()
) ::
String.t()
def url_for_relationship(resource_or_resources, conn, relationship_type) do
Enum.join([url_for(resource_or_resources, conn), "relationships", relationship_type], "/")
end
@doc """
Generates the resource link
Generates the resource link for a resource.
"""
@spec url_for(Resource.t() | [Resource.t()] | nil, Conn.t() | nil) :: String.t()
def url_for(
resources,
%Conn{private: %{jsonapi_plug: %__MODULE__{} = jsonapi_plug}}
)
when is_nil(resources) or is_list(resources),
do: jsonapi_plug.base_url
def url_for(
resource,
%Conn{private: %{jsonapi_plug: %__MODULE__{} = jsonapi_plug}}
) do
Enum.join(
[
jsonapi_plug.base_url,
Map.get(resource, Resource.id_attribute(resource)) |> to_string()
],
"/"
)
end
@doc """
Recase resource fields
Changes the case of resource field names to the specified case, ignoring underscores
or dashes that are not between letters/numbers.
## Examples
iex> recase("top_posts", :camelize)
"topPosts"
iex> recase(:top_posts, :camelize)
"topPosts"
iex> recase("_top_posts", :camelize)
"_topPosts"
iex> recase("_top__posts_", :camelize)
"_top__posts_"
iex> recase("", :camelize)
""
iex> recase("top_posts", :dasherize)
"top-posts"
iex> recase("_top_posts", :dasherize)
"_top-posts"
iex> recase("_top__posts_", :dasherize)
"_top__posts_"
iex> recase("top-posts", :underscore)
"top_posts"
iex> recase(:top_posts, :underscore)
"top_posts"
iex> recase("-top-posts", :underscore)
"-top_posts"
iex> recase("-top--posts-", :underscore)
"-top--posts-"
iex> recase("corgiAge", :underscore)
"corgi_age"
"""
@spec recase(Resource.field_name() | String.t(), case()) :: String.t()
def recase(field, case) when is_atom(field) do
field
|> to_string()
|> recase(case)
end
def recase("", :camelize), do: ""
def recase(field, :camelize) do
[h | t] =
Regex.split(~r{(?<=[a-zA-Z0-9])[-_](?=[a-zA-Z0-9])}, field)
|> Enum.filter(&(&1 != ""))
Enum.join([String.downcase(h) | Enum.map(t, &String.capitalize/1)])
end
def recase(field, :dasherize) do
String.replace(field, ~r/([a-zA-Z0-9])_([a-zA-Z0-9])/, "\\1-\\2")
end
def recase(field, :underscore) do
field
|> String.replace(~r/([a-zA-Z\d])-([a-zA-Z\d])/, "\\1_\\2")
|> String.replace(~r/([a-z\d])([A-Z])/, "\\1_\\2")
|> String.downcase()
end
@attribute_schema [
name: [
doc: "Maps the resource attribute name to the given key.",
type: :atom
],
serialize: [
doc: "Controls wether the attribute is serialized in responses.",
type: :boolean,
default: true
],
deserialize: [
doc: "Controls wether the attribute is deserialized in requests.",
type: :boolean,
default: true
]
]
@relationship_schema [
name: [
doc: "Maps the resource relationship name to the given key.",
type: :atom
],
many: [
doc: "Specifies a to many relationship.",
type: :boolean,
default: false
],
resource: [
doc: "Specifies the resource to be used to serialize the relationship",
type: :atom,
required: true
]
]
@schema NimbleOptions.new!(
attributes: [
doc:
"Resource attributes. This will be used to (de)serialize requests/responses:\n\n" <>
NimbleOptions.docs(@attribute_schema, nest_level: 1),
type:
{:or,
[
{:list, :atom},
{:keyword_list, [*: [type: [keyword_list: [keys: @attribute_schema]]]]}
]},
default: []
],
id_attribute: [
doc: "Attribute on your data to be used as the JSON:API resource id.",
type: :atom,
default: :id
],
relationships: [
doc:
"Resource relationships. This will be used to (de)serialize requests/responses\n\n" <>
NimbleOptions.docs(@relationship_schema, nest_level: 1),
type: :keyword_list,
keys: [*: [type: :non_empty_keyword_list, keys: @relationship_schema]],
default: []
],
type: [
doc: "Resource type. To be used as the JSON:API resource type value",
type: :string,
required: true
]
)
@doc false
def resource_options_schema, do: @schema
end