Alembic v1.0.0 Alembic.Document

JSON API refers to the top-level JSON structure as a document.

Summary

Types

t()

A JSON API Document

Functions

Converts a JSON object into a JSON API Document, t

Merges the errors from two documents together

Since merge/2 adds the second errors to the beginning of a first document’s errors list, the final merged errors needs to be reversed to maintain the original order

Types

t ::
  %Alembic.Document{data: nil, errors: [Alembic.Error.t], included: nil, links: Alembic.Links.t | nil, meta: Alembic.Meta.t | nil} |
  %Alembic.Document{data: nil, errors: nil, included: nil, links: Alembic.Links.t | nil, meta: Alembic.Meta.t} |
  %Alembic.Document{data: [Alembic.Resource.t] | Alembic.Resource.t, errors: nil, included: [Alembic.Resource.t] | nil, links: Alembic.Links.t | nil, meta: Alembic.Meta.t | nil}

A JSON API Document.

Data

When there are no errors, data are returned in the document and errors are not returned in the document.

FieldIncluded/Excluded/Optional
dataIncluded
errorsExcluded
includedOptional
linksOptional
metaOptional

Errors

When an error occurs, errors are returned in the document and data are not returned in the document.

FieldIncluded/Excluded/Optional
dataExcluded
errorsIncluded
includedExcluded
linksOptional
metaOptional

Meta

JSON API allows a meta only document, in which case data and errors are not returned in the document.

FieldIncluded/Excluded/Optional
dataExcluded
errorsExcluded
includedExcluded
linksOptional
metaIncluded

Functions

from_json(json, error_template)

Converts a JSON object into a JSON API Document, t.

Data documents

Single

An empty single resource can represented as "data": null in encoded JSON, so it comes into from_json as data: nil

iex> Alembic.Document.from_json(
...>   %{ "data" => nil },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: nil
  }
}

A present single can be a resource

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "attributes" => %{
...>         "text" => "First Post!"
...>       },
...>       "id" => "1",
...>       "type" => "post"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :client
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: %Alembic.Resource{
      attributes: %{
        "text" => "First Post!"
      },
      id: "1",
      type: "post"
    }
  }
}

… or a present single can be just a resource identifier

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => %{
...>       "id" => "1",
...>       "type" => "post"
...>     }
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: %Alembic.ResourceIdentifier{
      id: "1",
      type: "post"
    }
  }
}

You notice that whether a JSON object in "data" is treated as a Alembic.Resource.t or Alembic.ResourceIdentifier.t hinges on whether "attributes" or "relationships" is present as those members are only allowed for resources.

Collection

Resources

A collection can be a list of resources

iex> Alembic.Document.from_json( …> %{ …> “data” => [ ...> %{ ...> "attributes" => %{ ...> "text" => "First Post!" ...> }, ...> "id" => "1", ...> "relationships" => %{ ...> "comments" => %{ ...> "data" => [ ...> %{ ...> "id" => "1", ...> "type" => "comment" ...> } ...> ] ...> } ...> }, ...> "type" => "post" ...> } ...> ] …> }, …> %Alembic.Error{ …> meta: %{ …> “action” => :create, …> “sender” => :client …> }, …> source: %Alembic.Source{ …> pointer: “” …> } …> } …> ) {

 :ok,
 %Alembic.Document{
   data: [
     %Alembic.Resource{
       attributes: %{
         "text" => "First Post!"
       },
       id: "1",
       relationships: %{
         "comments" => %Alembic.Relationship{
           data: [
             %Alembic.ResourceIdentifier{
               id: "1",
               type: "comment"
             }
           ]
         }
       },
       type: "post"
     }
   ]
 }

}

With "relationships", a resources collection can optionally have "included" for the attributes for the resource identifiers. If "included" is not given or the "id" and "type" for a resource identifier, then the resource identifier should just be considered a foreign key reference that needs to be fetched with another API query.

iex> Alembic.Document.from_json( …> %{ …> “data” => [ ...> %{ ...> "attributes" => %{ ...> "text" => "First Post!" ...> }, ...> "id" => "1", ...> "relationships" => %{ ...> "comments" => %{ ...> "data" => [ ...> %{ ...> "id" => "1", ...> "type" => "comment" ...> } ...> ] ...> } ...> }, ...> "type" => "post" ...> } ...> ], …> “included” => [ ...> %{ ...> "attributes" => %{ ...> "text" => "First Comment!" ...> }, ...> "id" => "1", ...> "type" => "comment" ...> } ...> ] …> }, …> %Alembic.Error{ …> meta: %{ …> “action” => :create, …> “sender” => :client …> }, …> source: %Alembic.Source{ …> pointer: “” …> } …> } …> ) {

 :ok,
 %Alembic.Document{
   data: [
     %Alembic.Resource{
       attributes: %{
         "text" => "First Post!"
       },
       id: "1",
       relationships: %{
         "comments" => %Alembic.Relationship{
           data: [
             %Alembic.ResourceIdentifier{
               id: "1",
               type: "comment"
             }
           ]
         }
       },
       type: "post"
     }
   ],
   included: [
     %Alembic.Resource{
       attributes: %{
         "text" => "First Comment!"
       },
       id: "1",
       type: "comment"
     }
   ]
 }

}

Resource Identifiers

Or a list of resource identifiers

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => [
...>       %{
...>         "id" => "1",
...>         "type" => "post"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: [
      %Alembic.ResourceIdentifier{
        id: "1",
        type: "post"
      }
    ]
  }
}

Empty

An empty collection can be signified with []. Because there is no type information, it’s not possible to tell whether it is an empty list of Alembic.Resource.t or Alembic.ResourceIdentifier.t.

iex> Alembic.Document.from_json(
...>   %{
...>     "data" => []
...>   },
...>   %Alembic.Error{
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    data: []
  }
}

Errors documents

Errors from the sender must have an "errors" key set to a list of errors.

iex> Alembic.Document.from_json(
...>   %{
...>     "errors" => [
...>       %{
...>         "code" => "1",
...>         "detail" => "There was an error in data",
...>         "id" => "2",
...>         "links" => %{
...>           "about" => %{
...>             "href" => "/errors/2",
...>             "meta" => %{
...>               "extra" => "about meta"
...>             }
...>           }
...>         },
...>         "meta" => %{
...>           "extra" => "error meta"
...>         },
...>         "source" => %{
...>           "pointer" => "/data"
...>         },
...>         "status" => "422",
...>         "title" => "There was an error"
...>       }
...>     ]
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        code: "1",
        detail: "There was an error in data",
        id: "2",
        links: %{
          "about" => %Alembic.Link{
            href: "/errors/2",
            meta: %{
              "extra" => "about meta"
            }
          }
        },
        meta: %{
          "extra" => "error meta"
        },
        source: %Alembic.Source{
          pointer: "/data"
        },
        status: "422",
        title: "There was an error"
      }
    ]
  }
}

Error objects MUST be returned as an array keyed by "errors" in the top level of a JSON API document.

iex> Alembic.Document.from_json(
...>   %{"errors" => "Lots of errors"},
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :error,
  %Alembic.Document{
    errors: [
      %Alembic.Error{
        detail: "`/errors` type is not array",
        meta: %{
          "type" => "array"
        },
        source: %Alembic.Source{
          pointer: "/errors"
        },
        status: "422",
        title: "Type is wrong"
      }
    ]
  }
}

Meta documents

Returned documents can contain just "meta" with neither "data" nor "errors"

iex> Alembic.Document.from_json(
...>   %{
...>     "meta" => %{
...>       "copyright" => "2016"
...>     }
...>   },
...>   %Alembic.Error{
...>     meta: %{
...>       "action" => :create,
...>       "sender" => :server
...>     },
...>     source: %Alembic.Source{
...>       pointer: ""
...>     }
...>   }
...> )
{
  :ok,
  %Alembic.Document{
    meta: %{
      "copyright" => "2016"
    }
  }
}

Incomplete documents

If neither "errors", "data", nor "meta" is present, then the document is invalid and a `Alembic. iex> Alembic.Document.from_json( …> %{}, …> %Alembic.Error{ …> meta: %{ …> “action” => :create, …> “sender” => :server …> }, …> source: %Alembic.Source{ …> pointer: “” …> } …> } …> ) { :error, %Alembic.Document{ errors: [ %Alembic.Error{ detail: “At least one of the following children of `` must be present:\n” <>

                "data\n" <>
                "errors\n" <>
                "meta",
        meta: %{
          "children" => [
            "data",
            "errors",
            "meta"
          ]
        },
        source: %Alembic.Source{
          pointer: ""
        },
        status: "422",
        title: "Not enough children"
      }
    ],
  }
}
merge(first, second)

Specs

merge(%Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, links: term, meta: term}, %Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, links: term, meta: term}) :: %Alembic.Document{data: term, errors: [Alembic.Error.t], included: term, links: term, meta: term}

Merges the errors from two documents together.

The errors from the second document are prepended to the errors of the first document so that the errors as a whole can be reversed with reverse/1

reverse(document)

Since merge/2 adds the second errors to the beginning of a first document’s errors list, the final merged errors needs to be reversed to maintain the original order.

iex> merged = %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.Document.reverse(merged)
%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"
    }
  ]
}