Packages

Implementation of RFC 6901 which defines a string syntax for identifying a specific value within a JSON document

Current section

Files

Jump to
json_pointer lib json_pointer.ex
Raw

lib/json_pointer.ex

defmodule JSONPointer do
@moduledoc """
Implementation of [RFC 6901](https://tools.ietf.org/html/rfc6901)
which defines a string syntax for identifying a specific value within a JSON document.
## Usage
*Preleminary note: the actual parsing of the JSON document is outside of the scope of this library,
feel free to select on the several libraries available.*
The `resolve/2` and `resolve!/2` functions expect to receive documents in the form of nested maps and lists,
as produced by most JSON parsers.
iex> document = %{
...> "key" => "value",
...> "list" => [1, 2, 3],
...> "deeply" => %{"nested" => %{"values" => [%{"x" => 1}, %{"x" => 2}]}}
...> }
iex> JSONPointer.resolve(document, "/key")
{:ok, "value"}
iex> JSONPointer.resolve(document, "/list/1")
{:ok, 2}
iex> JSONPointer.resolve(document, "/deeply/nested/values/0")
{:ok, %{"x" => 1}}
iex> JSONPointer.resolve(document, "/deeply/nested/values/1/x")
{:ok, 2}
iex> JSONPointer.resolve(document, "/list/4")
{:error, "index 4 out of bounds in [1, 2, 3]"}
"""
@type json_object :: map
@type json_value :: nil | true | false | list | float | integer | String.t | json_object
@doc """
Escapes a string in order to be usable as reference token.
iex> JSONPointer.escape("/esc~aped")
"~1esc~0aped"
"""
@spec escape(String.t) :: String.t
def escape(string) do
string
|> String.replace("~", "~0")
|> String.replace("/", "~1")
end
@doc """
Unescapes a reference token and returns the original string
iex> JSONPointer.unescape("~1esc~0aped")
"/esc~aped"
"""
@spec unescape(String.t) :: String.t
def unescape(string) do
string
|> String.replace("~1", "/")
|> String.replace("~0", "~")
end
@doc """
Resolves a JSON Pointer `expr` against the given `document` and
returns the referenced value.
Raises an `ArgumentError` exception if an error occurs
iex> JSONPointer.resolve!(%{"key" => "value"}, "/key")
"value"
iex> JSONPointer.resolve!(%{"key" => "value"}, "/bogus")
** (ArgumentError) reference token not found: "bogus"
"""
@spec resolve!(json_object, String.t) :: json_value | no_return
def resolve!(document, expr) do
case resolve(document, expr) do
{:ok, value}
-> value
{:error, message}
-> raise ArgumentError, message
end
end
@doc """
Resolves a JSON Pointer `expr` against the given `document` and
returns `{:ok, value}` on success and `{:error, message}` otherwise.
iex> JSONPointer.resolve(%{"key" => "value"}, "/key")
{:ok, "value"}
iex> JSONPointer.resolve(%{"key" => "value"}, "/bogus")
{:error, "reference token not found: \\"bogus\\""}
"""
@spec resolve(json_object, String.t) :: {:ok, json_value} | {:error, String.t}
def resolve(document, ""), do: {:ok, document}
def resolve(document, _expr = <<"/", expr::binary>>), do: do_resolve(document, String.split(expr, "/"))
def resolve(_document, _expr), do: {:error, "must contain zero or more reference token, each prefixed with \"/\""}
#
# Private interface
#
defp do_resolve(document, []), do: {:ok, document}
defp do_resolve(document, [token|rest]) do
token = unescape(token)
cond do
is_numeric?(token)
-> do_resolve_array_index(document, token, rest)
token
-> do_resolve_token(document, token, rest)
end
end
defp do_resolve_token(nil, token, _), do: {:error, "reference token not found: #{inspect token}"}
defp do_resolve_token(document, token, _) when is_list(document), do: {:error, "invalid array index: #{inspect token}"}
defp do_resolve_token(document, token, rest) do
case Dict.fetch(document, token) do
:error
-> {:error, "reference token not found: #{inspect token}"}
{:ok, value}
-> do_resolve(value, rest)
end
end
defp do_resolve_array_index(nil, token, _rest), do: {:error, "cannot find index #{token} on nil"}
defp do_resolve_array_index(_document, index = <<"0", _, _::binary>>, _rest), do: {:error, "index with leading zeros not allowed: #{inspect index}"}
defp do_resolve_array_index(document, token, rest) do
index = Integer.parse(token) |> elem(0)
case get_item_at_index(document, index) do
:error
-> {:error, "index #{index} out of bounds in #{inspect document}"}
{:ok, value}
-> do_resolve(value, rest)
end
end
defp is_numeric?(token), do: Regex.match?(~r/\A\d+\Z/, token)
defp get_item_at_index([item|_], 0), do: {:ok, item}
defp get_item_at_index([], _index), do: :error
defp get_item_at_index([_|rest], index), do: get_item_at_index(rest, index-1)
end