Alembic v4.0.0 Alembic.FromJson behaviour View Source

JSON objects that have constrained members in the JSON API format are represented as structs. In order to convert plain, decoded JSON in maps and lists, from_json/2 can be implemented by a module to convert to its struct.

Link to this section Summary

Types

The action that generated the Alembic.json. :create and :update allow additional formats

A result that can collect singleton_results

A value that can collect singleton_values

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

Ensurs that json is an integer

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

Since merge/2 adds new singleton_values to the beginning of a collectable_ok list or Alembic.Error.ts 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

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

Link to this section Types

Link to this type action() View Source
action() :: :create | :delete | :fetch | :update

The action that generated the Alembic.json. :create and :update allow additional formats.

Link to this type collectable_ok() View Source
collectable_ok() :: {:ok, collectable_value()}

A result that can collect singleton_results.

Link to this type collectable_result() View Source
collectable_result() :: collectable_ok() | error()
Link to this type collectable_value() View Source
collectable_value() :: list() | map() | struct()

A value that can collect singleton_values.

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.

The name of a field in a struct

Link to this type field_ok() View Source
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.

Link to this type field_result() View Source
field_result() :: field_ok() | error()

A key in a map or struct output from from_json/2

Link to this type key_ok() View Source
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.

Link to this type key_result() View Source
key_result() :: key_ok() | error()
Link to this type sender() View Source
sender() :: :client | :server

Whether the :client or :server sent the json. The :client is allowed more formats on :create and :update action

Link to this type singleton_ok() View Source
singleton_ok() :: {:ok, singleton_value()}

A result that can be merged into a collectable_ok whose collectable_value is a list.

Link to this type singleton_result() View Source
singleton_result() :: singleton_ok() | error()
Link to this type singleton_value() View Source
singleton_value() :: [struct()] | map() | nil | String.t() | struct()

A single value that can be merged into a collective_value.

Link to this section Functions

Link to this function from_json_array(json_array, error_template, element_module) View Source
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.

Link to this function from_parent_json_to_field_result(options) View Source
from_parent_json_to_field_result(%{
  parent: %{json: Alembic.json_object(), error_template: Alembic.Error.t()},
  member:
    %{
      :from_json => (Alembic.json(), Origin.t() -> singleton_result()),
      :name => String.t(),
      optional(:required) => boolean()
    }
    | %{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 by member_module’s from_json/2) tagged with the field_name, so that it can be passed to merge/2.
  • {:error, %Alembic.Document{errors: [Alembic.Error.t]}} - an error from member_module’s from_json/2.
  • :error if the member_name isn’t in parent_json at all. This is to help distinguish no member from nil member values, as JSON API allows for null members in the case of empty to-one relationships.
Link to this function integer_from_json(integer, error_template) View Source
integer_from_json(integer(), Alembic.Error.t()) :: {:ok, integer()}
integer_from_json(
  nil | true | false | list() | float() | String.t() | Alembic.json_object(),
  Alembic.Error.t()
) :: {:error, Alembic.Document.t()}

Ensurs that json is an integer.

An integer will be returned with an ok tuple

iex> Alembic.FromJson.integer_from_json(
...>   1,
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: "/page/number"
...>     }
...>   }
...> )
{:ok, 1}

A non-integer will be returned in an error tuple where the errors Alembic.Document.t has a member type error.

iex> Alembic.FromJson.integer_from_json(
...>   1.5,
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: "/page/number"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/page/number` type is not integer",
        meta: %{
          "type" => "integer"
        },
        source: %Alembic.Source{
          pointer: "/page/number"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}
Link to this function integer_to_positive_integer(integer, error_template) View Source
integer_to_positive_integer(0, Alembic.Error.t()) :: error()
integer_to_positive_integer(neg_integer(), Alembic.Error.t()) :: error()
integer_to_positive_integer(pos_integer(), Alembic.Error.t()) ::
  {:ok, pos_integer()}

Ensures integer is positive.

If greater than 0, then it’s ok

iex> Alembic.FromJson.integer_to_positive_integer(
...>   1,
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: "/page/number"
...>     }
...>   }
...> )
{:ok, 1}

If it’s 0, then it’s a type error

iex> Alembic.FromJson.integer_to_positive_integer(
...>   0,
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: "/page/number"
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/page/number` type is not positive integer",
        meta: %{
          "type" => "positive integer"
        },
        source: %Alembic.Source{
          pointer: "/page/number"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}
Link to this function merge(collable_result, singleton_result) View Source
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.

Link to this function put_key(result, key) View Source
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
Link to this function reduce(singleton_results, collectable_result) View Source

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.

Link to this function reverse(collectable_result) View Source
reverse(error()) :: error()
reverse({:ok, list()}) :: {:ok, list()}
reverse({:ok, map()}) :: {:ok, map()}

Since merge/2 adds new singleton_values to the beginning of a collectable_ok list or Alembic.Error.ts 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
Link to this function string_from_json(json, error_template) View Source
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"
      }
    ]
  }
}

Link to this section Callbacks

Link to this callback from_json(decoded_json, error_template) View Source
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 from Poison.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 under Alembic.
  • {: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 the Alembic.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 in Alembic.Error should not be in the returned Alembic.Document.t’s errors, as that meta information is an implementation detail and for internal use in recursive calls to Alembic.FromJson.from_json/2 only.