defmodule ExHal do @moduledoc """ Use HAL APIs with ease. ## Example ```elixir iex> doc = ExHal.parse(~s| ...> { "name": "Hello!", ...> "_links": { ...> "self" : { "href": "http://example.com" }, ...> "profile": [{ "href": "http://example.com/special" }, ...> { "href": "http://example.com/normal" }] ...> } ...> } ...> |) %ExHal.Document{headers: [], links: %{"profile" => [%ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/normal", templated: false}, %ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/special", templated: false}], "self" => [%ExHal.Link{name: nil, rel: "self", target: nil, href: "http://example.com", templated: false}]}, properties: %{"name" => "Hello!"}} iex> ExHal.url(doc) {:ok, "http://example.com"} iex> ExHal.fetch(doc, "name") {:ok, "Hello!"} iex> ExHal.fetch(doc, "non-existent") :error iex> ExHal.fetch(doc, "profile") {:ok, [%ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/normal", templated: false}, %ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/special", templated: false}]} iex> ExHal.get_links_lazy(doc, "profile", fn -> [] end) [%ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/normal", templated: false}, %ExHal.Link{name: nil, rel: "profile", target: nil, href: "http://example.com/special", templated: false}] iex> ExHal.get_links_lazy(doc, "alternate", fn -> [] end) [] ``` ExHal can also make requests. Continuing the example above: ```elixir ExHal.follow_link(doc, "profile") {:error, %ExHal.Error{reason: "multiple choices"}} ExHal.follow_link(doc, "nonexistent") {:error, %ExHal.Error{reason: "no such link"}} ExHal.follow_link("self") {:ok, %ExHal.Document{...}} ExHal.follow_link(doc, "profile", pick_volunteer: true) {:ok, %ExHal.Document{...}} ExHal.follow_links(doc, "profile") [{:ok, %ExHal.Document{...}}, {:ok, %ExHal.Document{...}}] ExHal.follow_links(doc, "profile", headers: ["Content-Type": "application/vnd.custom.json+type"]) [{:ok, %ExHal.Document{...}}, {:ok, %ExHal.Document{...}}] ExHal.post(doc, "self", ~s| ...> { "name": "http://example.com/new-thing", ...> "_links": { ...> "self": { "href": "http://example.com/new-thing" } ...> } ...> } ...> |) {:ok, %ExHal.Document{...}} ``` """ alias ExHal.Link, as: Link alias ExHal.Document, as: Document alias ExHal.Error, as: Error @doc """ Returns a new `%ExHal.Document` representing the HAL document provided. """ def parse(hal_str, opts \\ %{}) do parsed = Poison.Parser.parse!(hal_str) Document.from_parsed_hal(parsed, opts) end @doc """ Follows a link in a HAL document. Returns `{:ok, %ExHal.Document{...}}` if request is an error `{:error, %ExHal.Error{...}}` if not """ def follow_link(a_doc, name, opts \\ %{tmpl_vars: %{}, pick_volunteer: false, headers: []}) do opts = Map.new(opts) pick_volunteer? = Map.get opts, :pick_volunteer, false case figure_link(a_doc, name, pick_volunteer?) do {:error, e} -> {:error, e} {:ok, link} -> Link.follow(link, opts) end end @doc """ Follows all links of a particular rel in a HAL document. Returns `[{:ok, %ExHal.Document{...}}, {:error, %ExHal.Error{...}, ...]` """ def follow_links(a_doc, name, opts \\ %{tmpl_vars: %{}, headers: []}) do opts = Map.new(opts) case get_links_lazy(a_doc, name, fn -> :missing end) do :missing -> {:error, %Error{reason: "no such link: #{name}"}} links -> Enum.map(links, fn link -> Link.follow(link, opts) end) end end @doc """ Posts data to the named link in a HAL document. Returns `{:error, %ExHal.Error{...}}` if request is an error `{:ok, %ExHal.Document{...}}` if not """ def post(a_doc, name, body) do case figure_link(a_doc, name, false) do {:error, e} -> {:error, e} {:ok, link} -> Link.post(link, body) end end @doc """ Fetches value of specified property or links whose `rel` matches Returns `{:ok, }` if `name` identifies a property; `{:ok, [%Link{}, ...]}` if `name` identifies a link; `:error` othewise """ def fetch(a_document, name) do case get_lazy(a_document, name, fn -> :error end) do :error -> :error result -> {:ok, result} end end @doc """ Returns link or property of the specified name, or the result of `default_fun` if neither are found. """ def get_lazy(a_doc, name, default_fun) do get_property_lazy(a_doc, name, fn -> get_links_lazy(a_doc, name, default_fun) end ) end @doc """ Returns `` when property exists result of `default_fun` otherwise """ def get_property_lazy(a_doc, prop_name, default_fun) do Map.get_lazy(a_doc.properties, prop_name, default_fun) end @doc """ Returns `[%Link{}...]` when link exists result of `default_fun` otherwise """ def get_links_lazy(a_doc, link_name, default_fun) do Map.get_lazy(a_doc.links, link_name, default_fun) end @doc """ Returns `{:ok, }` `:error` """ def url(a_doc, default_fn \\ fn (_doc) -> :error end) do case ExHal.fetch(a_doc, "self") do :error -> default_fn.(a_doc) {:ok, [link | _]} -> Link.target_url(link) end end defp figure_link(a_doc, name, pick_volunteer?) do case get_links_lazy(a_doc, name, fn -> :missing end) do :missing -> {:error, %Error{reason: "no such link: #{name}"}} (ls = [_|[_|_]]) -> if pick_volunteer? do {:ok, List.first(ls)} else {:error, %Error{reason: "multiple choices"}} end [l] -> {:ok, l} end end end