Alembic v2.1.0 Alembic.FromJson behaviour

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.

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

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

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_results.

collectable_value :: list | map | struct

A value that can collect singleton_values.

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.

key :: field | String.t

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

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.

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

from_json_array(json_array, error_template, element_module)

Specs

Converts a JSON array using the element_module or element_from_json function.

The element_module MUST implement the Alembic.FromJson behaviour.

from_parent_json_to_field_result(map)

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 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.
merge(collable_result, singleton_result)

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.

put_key(result, key)

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
reduce(singleton_results, 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.

reverse(collectable_result)

Specs

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
string_from_json(json, error_template)

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

from_json(decoded_json, error_template)

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 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.