Alembic v2.1.0 Alembic.FromJson behaviour
JSON objects that have constrained members in the JSON API format are represented as
struct
s. In order to convert plain, decoded JSON in map
s and list
s, from_json/2
can be implemented by a
module to convert to its struct
.
Summary
Types
The action that generated the Alembic.json
. :create
and :update
allow additional formats
A result that can collect singleton_result
s
A value that can collect singleton_value
s
Tagged-tuple returned when an error has occured in from_json/2
The name of a field in a struct
A result that has been tagged with the field
in a struct to which it should be put when merged to the
collectable_ok
A key in a map or struct output from from_json/2
A result that has been tagged with the key
in a struct or map to which it should be put when merged to the
collectable_ok
Whether the :client
or :server
sent the json
. The :client
is allowed more formats on :create
and :update
action
A result that can be merged into a collectable_ok
whose collectable_value
is a list
A single value that can be merged into a collective_value
Functions
Converts a JSON array using the element_module
or element_from_json
function
Converts the member of a json object into a field_result
that can be merged into a struct
Merges the singleton_result
into the collectable_result
Add key
to singleton_ok
tuple, otherwise, does nothing
Reduces singleton_results
into an Alembic.FromJson.collectable_result
Ensures that json
is a String.t
Callbacks
Takes decoded JSON, such as from Poison.decode/1
, and validates it for format and converts it to struct
Types
action :: :create | :delete | :fetch | :update
The action that generated the Alembic.json
. :create
and :update
allow additional formats.
collectable_ok :: {:ok, collectable_value}
A result that can collect singleton_result
s.
collectable_value :: list | map | struct
A value that can collect singleton_value
s.
error :: {:error, Alembic.Document.t}
Tagged-tuple returned when an error has occured in from_json/2
.
The format errors are in the errors section of the Alembic.Document.t
, which can be sent back to
sender of the original JSON API document so they can correct the errors.
field :: atom
The name of a field in a struct
field_ok :: {:ok, {field, singleton_value}}
A result that has been tagged with the field
in a struct to which it should be put when merged to the
collectable_ok
.
field_result :: field_ok | error
key_ok :: {:ok, {key, singleton_value}}
A result that has been tagged with the key
in a struct or map to which it should be put when merged to the
collectable_ok
.
key_result :: key_ok | error
sender :: :client | :server
Whether the :client
or :server
sent the json
. The :client
is allowed more formats on :create
and :update
action
singleton_ok :: {:ok, singleton_value}
A result that can be merged into a collectable_ok
whose collectable_value
is a list
.
singleton_value ::
[struct] |
map |
nil |
String.t |
struct
A single value that can be merged into a collective_value
.
Functions
Specs
from_json_array(Alembic.json, Alembic.Error.t, module) ::
{:ok, [singleton_value]} |
error
from_json_array(Alembic.json, Alembic.Error.t, (Alembic.json, Alembic.Error.t -> singleton_result)) ::
{:ok, [singleton_value]} |
error
Converts a JSON array using the element_module
or element_from_json
function.
The element_module
MUST implement the Alembic.FromJson
behaviour.
Specs
from_parent_json_to_field_result(%{parent: %{json: Alembic.json_object, error_template: Alembic.Error.t}, member: %{name: String.t, from_json: (Alembic.json, Origin.t -> singleton_result)} | %{name: String.t, module: module}, field: atom}) ::
field_result |
error |
:error
Converts the member of a json object into a field_result
that can be merged into a struct.
Examples
No Member
If there is no member, then :error will be returned
iex> Alembic.FromJson.from_parent_json_to_field_result(
...> %{
...> field: :data,
...> member: %{
...> module: Alembic.ResourceLinkage,
...> name: "data"
...> },
...> parent: %{
...> json: %{},
...> error_template: %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/relationships/author"
...> }
...> }
...> }
...> }
...> )
:error
If there is no member, and the :member
map contains required: true
, then an error will be returned that the member
is missing
iex> Alembic.FromJson.from_parent_json_to_field_result(
...> %{
...> field: :id,
...> member: %{
...> from_json: &Alembic.FromJson.string_from_json/2,
...> name: "id",
...> required: true
...> },
...> parent: %{
...> json: %{},
...> error_template: %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/relationships/author"
...> }
...> }
...> }
...> }
...> )
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
detail: "`/data/relationships/author/id` is missing",
meta: %{
"child" => "id"
},
source: %Alembic.Source{
pointer: "/data/relationships/author"
},
status: "422",
title: "Child missing"
}
]
}
}
nil
member value
If there is a member, but it’s value is nil
(meaning it was null
in the unparsed JSON) then nil
will be passed
to the member_module
’s from_json/2
callback. In most cases, the implementation should return {:ok, nil}
, which
will be tagged with the field_name
.
iex> Alembic.FromJson.from_parent_json_to_field_result(
...> %{
...> field: :links,
...> member: %{
...> module: Alembic.Links,
...> name: "links"
...> },
...> parent: %{
...> json: %{"links" => nil},
...> error_template: %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/relationships/author"
...> }
...> }
...> }
...> }
...> )
{:ok, {:links, nil}}
Error on member
Any errors from member_module
’s from_json/2
will be returned, but with the origin set to parent_origin
iex> Alembic.FromJson.from_parent_json_to_field_result(
...> %{
...> field: :links,
...> member: %{
...> module: Alembic.Links,
...> name: "links"
...> },
...> parent: %{
...> json: %{"links" => []},
...> error_template: %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/relationships/author"
...> }
...> }
...> }
...> }
...> )
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
detail: "`/data/relationships/author/links` type is not links object",
meta: %{
"type" => "links object"
},
source: %Alembic.Source{
pointer: "/data/relationships/author/links"
},
status: "422",
title: "Type is wrong"
}
]
}
}
Returns
{:ok, {field_name, value}}
- a value (converted bymember_module
’sfrom_json/2
) tagged with thefield_name
, so that it can be passed tomerge/2
.{:error, %Alembic.Document{errors: [Alembic.Error.t]}}
- an error frommember_module
’sfrom_json/2
.:error
if themember_name
isn’t inparent_json
at all. This is to help distinguish no member fromnil
member values, as JSON API allows fornull
members in the case of empty to-one relationships.
Specs
merge({:ok, list}, {:ok, singleton_value}) :: {:ok, list}
merge({:ok, map}, {:ok, {key, singleton_value}}) :: {:ok, map}
merge({:ok, struct}, {:ok, field, singleton_value}) :: {:ok, struct}
merge(collectable_ok, error) :: error
merge(error, key_ok) :: error
merge(error, error) :: error
Merges the singleton_result
into the collectable_result
.
collectable_ok
If the collectable_result
is a collectable_ok
, then the singleton_result
controls whether another
collectable_ok
or error
is produced.
error
If the singleton_result
is an error, then it becomes the merged result
iex> collectable_ok = {:ok, ["One"]}
iex> error = {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/1"
...> }
...> }
...> ]
...> }
...> }
...> merged_result = Alembic.FromJson.merge(collectable_ok, error)
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
source: %Alembic.Source{
pointer: "/data/1"
}
}
]
}
}
iex> merged_result == error
true
field_ok
If the the result being merged in is for a field, then the field in the struct in collectable_ok
is updated with the
value from the field_ok
.
iex> collectable_ok = {:ok, %Alembic.Link{}}
iex> Alembic.FromJson.merge(
...> collectable_ok,
...> {:ok, {:href, "http://example.com"}}
...> )
{
:ok,
%Alembic.Link{
href: "http://example.com"
}
}
key_ok
If the result being merged is for a key, then the key in the map in collectable_ok
is updated with the value from
key_ok
iex> collectable_ok = {:ok, %{}}
iex> Alembic.FromJson.merge(
...> collectable_ok,
...> {
...> :ok,
...> {
...> "link_object",
...> %Alembic.Link{
...> href: "http://example.com",
...> meta: %{
...> "last_updated_on" => "2015-12-21"
...> }
...> }
...> }
...> }
...> )
{
:ok,
%{
"link_object" => %Alembic.Link{
href: "http://example.com",
meta: %{"last_updated_on" => "2015-12-21"}
}
}
}
singleton_ok
If the result being merged is a singleton value, then it is add to the head of collectable_ok
’s list
.
iex> collectable_ok = {:ok, []}
iex> Alembic.FromJson.merge(
...> collectable_ok,
...> {
...> :ok,
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data"
...> }
...> }
...> }
...> )
{
:ok,
[
%Alembic.Error{
source: %Alembic.Source{
pointer: "/data"
}
}
]
}
error
ok
If the current collectable is an error
, then ok results are ignored
iex> error = {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/0"
...> }
...> }
...> ]
...> }
...> }
iex> merged_error = Alembic.FromJson.merge(
...> error,
...> {:ok, {:field, "value"}}
...> )
iex> merged_error == error
true
error
If the collectable is an error
and the singleton_result
is also an error, then the
Alembic.Document.t
are merged so that singleton_result
’s errors appear at the head of merged errors.
iex> error = {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/0"
...> }
...> }
...> ]
...> }
...> }
iex> Alembic.FromJson.merge(
...> error,
...> {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data/1"
...> }
...> }
...> ]
...> }
...> }
...> )
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
source: %Alembic.Source{
pointer: "/data/1"
}
},
%Alembic.Error{
source: %Alembic.Source{
pointer: "/data/0"
}
}
]
}
}
If you want to get the Alembic.Document.t
errors
back in orginal order, use reverse/1
. reduce/2
automatically does the reverse/1
.
Specs
put_key({:ok, value}, key) :: {:ok, {key, value}} when key: String.t | atom, value: singleton_value
put_key(error, key) :: error when key: atom
Add key
to singleton_ok
tuple, otherwise, does nothing.
When result is singleton_ok
, adds the key
iex> Alembic.FromJson.put_key({:ok, %{}}, :data)
{:ok, {:data, %{}}}
But will not add a key if already present
iex> try do
...> Alembic.FromJson.put_key({:ok, {:data, %{}}}, :data)
...> rescue
...> error -> error
...> end
%FunctionClauseError{arity: 2, function: :put_key, module: Alembic.FromJson}
When result is error
, does nothing
iex> result = {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/data"
...> }
...> }
...> ]
...> }
...> }
iex> Alembic.FromJson.put_key(result, :data) == result
true
Specs
reduce([singleton_result] | Enumerable.t, collectable_result) :: collectable_result
Reduces singleton_results
into an Alembic.FromJson.collectable_result
.
If there are any errors in singleton_results, then all the errors are accumulated into a single
Alembic.FromJson.error
.
Since merge/2
adds new singleton_values
to the beginning of a collectable_ok
list or
Alembic.Error.t
s to the beginning of the Alembic.Document.t
errors
, those lists
need to be reversed to maintain original ordering after a series of merge/2
calls.
error
Reverses the Alembic.Document.t
errors
to undo tail prepending done by merge/2
iex> accumulated_error = {
...> :error,
...> %Alembic.Document{
...> errors: [
...> %Alembic.Error{
...> detail: "The index `2` of `/data` is not a resource",
...> source: %Alembic.Source{
...> pointer: "/data/2"
...> },
...> title: "Element is not a resource"
...> },
...> %Alembic.Error{
...> detail: "The index `1` of `/data` is not a resource",
...> source: %Alembic.Source{
...> pointer: "/data/1"
...> },
...> title: "Element is not a resource"
...> }
...> ]
...> }
...> }
iex> Alembic.FromJson.reverse(accumulated_error)
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
detail: "The index `1` of `/data` is not a resource",
source: %Alembic.Source{
pointer: "/data/1"
},
title: "Element is not a resource"
},
%Alembic.Error{
detail: "The index `2` of `/data` is not a resource",
source: %Alembic.Source{
pointer: "/data/2"
},
title: "Element is not a resource"
}
]
}
}
collectable_ok
lists
List are ordered, but collect by prepending, so they need to be reversed
iex> collectable_ok = {:ok, [3..4, 1..2]}
iex> Alembic.FromJson.reverse(collectable_ok)
{:ok, [1..2, 3..4]}
maps
Maps are unordered, so they just pass through
iex> collectable_ok = {:ok, %{"b" => 2, "a" => 1}}
iex> reversed_ok = Alembic.FromJson.reverse(collectable_ok)
{:ok, %{"a" => 1, "b" => 2}}
iex> reversed_ok == collectable_ok
true
Specs
string_from_json(String.t, Alembic.Error.t) :: {:ok, String.t}
string_from_json(nil | true | false | list | float | integer | Alembic.json_object, Alembic.Error.t) :: error
Ensures that json
is a String.t
A string will be returned in an ok
tuple
iex> Alembic.FromJson.string_from_json(
...> "422",
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/errors/0/status"
...> }
...> }
...> )
{:ok, "422"}
A non-string will be returned in an error
tuple where the errors Alembic.Document.t
has a member
type error
iex> Alembic.FromJson.string_from_json(
...> 422,
...> %Alembic.Error{
...> source: %Alembic.Source{
...> pointer: "/errors/0/status"
...> }
...> }
...> )
{
:error,
%Alembic.Document{
errors: [
%Alembic.Error{
detail: "`/errors/0/status` type is not string",
meta: %{
"type" => "string"
},
source: %Alembic.Source{
pointer: "/errors/0/status"
},
status: "422",
title: "Type is wrong"
}
]
}
}
Callbacks
Specs
from_json(decoded_json :: Alembic.json, error_template :: Alembic.Error.t) :: singleton_result
Takes decoded JSON, such as from Poison.decode/1
, and validates it for format and converts it to struct.
Parameters
json
- the decoded JSON fromPoison.decode/1
or some other JSON decoder.error_template
- A prepolated error that includes a pointer for error repointing and meta information that influencing validation. For example, some formats are only accepted on:create
or:update
from the:client
.%Alembic.Error{ meta: %{ "action" => Alembic.FromJson.action, "sender" => Alembic.FromJson.sender }, source: %Alembic.Source{ parameter: nil, pointer: Alembic.json_pointer } }
Returns
{:ok, map | struct}
- A validated type from underAlembic
.{:error, %Alembic.Document{errors: [Alembic.Error.t]}}
- one or more errors was encountered when converting from decoded JSON to a validated JSON API document. The format errors are in the errors section of theAlembic.Document.t
, which can be sent back to sender of the original JSON API document so they can correct the errors.NOTE: the
meta
from the passed inAlembic.Error
should not be in the returnedAlembic.Document.t
’serrors
, as thatmeta
information is an implementation detail and for internal use in recursive calls toAlembic.FromJson.from_json/2
only.