Packages
jsv
0.19.1
0.21.2
0.21.1
0.21.0
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.2
0.15.1
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
retired
0.11.2
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
A JSON Schema Validator with complete support for the latest specifications.
Current section
Files
Jump to
Current section
Files
lib/jsv/ref.ex
defmodule JSV.Ref do
alias __MODULE__
alias JSV.RNS
@moduledoc """
Representation of a JSON Schema reference (`$ref` or `$dynamicRef`).
"""
defstruct ns: nil, kind: nil, arg: nil, dynamic?: false
@type t :: %__MODULE__{}
@type ns :: binary | :root
@doc """
Creates a new reference from an URL, relative to the given namespace.
If the URL is absolute and its namespace is different from the given
namespace, returns an absolute URL.
"""
@spec parse(binary, ns) :: {:ok, t} | {:error, term}
def parse(url, current_ns) do
do_parse(url, current_ns, false)
end
@doc """
Raising version of the `parse/2` function.
"""
@spec parse!(binary, ns) :: t
def parse!(url, current_ns) do
case parse(url, current_ns) do
{:ok, ref} -> ref
{:error, reason} -> raise ArgumentError, "could not parse $ref: #{inspect(url)}, got: #{inspect(reason)}"
end
end
@doc """
Like `parse/2` but flags the reference as dynamic.
"""
@spec parse_dynamic(binary, ns) :: {:ok, t} | {:error, term}
def parse_dynamic(url, current_ns) do
do_parse(url, current_ns, true)
end
@doc """
Raising version of the `parse_dynamic/2` function.
"""
@spec parse_dynamic!(binary, ns) :: t
def parse_dynamic!(url, current_ns) do
case parse_dynamic(url, current_ns) do
{:ok, ref} -> ref
{:error, reason} -> raise ArgumentError, "could not parse $dynamicRef: #{inspect(url)}, got: #{inspect(reason)}"
end
end
@doc """
Creates a new pointer reference from a list of path segments.
The segments can be strings or integers, representing the path components
of a JSON pointer.
## Examples
iex> JSV.Ref.pointer(["properties", "name"], :root)
{:ok, %JSV.Ref{ns: :root, kind: :pointer, arg: ["properties", "name"], dynamic?: false}}
iex> JSV.Ref.pointer(["items", 0], :root)
{:ok, %JSV.Ref{ns: :root, kind: :pointer, arg: ["items", 0], dynamic?: false}}
"""
@spec pointer([binary | integer], ns) :: {:ok, t}
def pointer(segments, ns) when is_list(segments) do
{:ok, pointer!(segments, ns)}
end
@doc """
Creates a new pointer reference from a list of path segments.
Raising version of the `pointer/2` function.
"""
@spec pointer!([binary | integer], ns) :: t
def pointer!(segments, ns) when is_list(segments) do
%Ref{ns: ns, kind: :pointer, arg: segments, dynamic?: false}
end
defp do_parse(url, current_ns, dynamic?) do
uri = URI.parse(url)
{kind, arg} = parse_fragment(uri.fragment)
dynamic? = dynamic? and kind == :anchor
with {:ok, ns} <- RNS.derive(current_ns, url) do
{:ok, %Ref{ns: ns, kind: kind, arg: arg, dynamic?: dynamic?}}
end
end
defp parse_fragment(nil) do
{:top, []}
end
defp parse_fragment("") do
{:top, []}
end
defp parse_fragment("/") do
{:top, []}
end
defp parse_fragment("/" <> path) do
{:pointer, parse_pointer(path)}
end
defp parse_fragment(anchor) do
{:anchor, anchor}
end
defp parse_pointer(raw_docpath) do
raw_docpath |> String.split("/") |> Enum.map(&parse_pointer_segment/1)
end
defp parse_pointer_segment(string) do
unescape_json_pointer(string)
end
defp unescape_json_pointer(str) do
str
|> String.replace("~1", "/")
|> String.replace("~0", "~")
|> URI.decode()
end
@doc """
Encodes the given string as a JSON representation of a JSON pointer, that is
with `~` as `~0` and `/` as `~1`.
"""
@spec escape_json_pointer(binary | iodata()) :: binary
def escape_json_pointer(str) when is_binary(str) do
str
|> String.replace("~", "~0")
|> String.replace("/", "~1")
end
def escape_json_pointer(str) do
str
|> IO.iodata_to_binary()
|> escape_json_pointer()
end
end
defimpl Inspect, for: JSV.Ref do
alias JSV.Ref
# credo:disable-for-next-line Credo.Check.Readability.Specs
def inspect(ref, opts) do
do_inspect(ref, opts)
end
defp do_inspect(%Ref{dynamic?: true, kind: :anchor, ns: ns, arg: anchor}, _) do
"JSV.Ref.parse_dynamic!(#{inspect("##{anchor}")}, #{inspect(ns)})"
end
defp do_inspect(%Ref{kind: :top, ns: ns}, _) do
"JSV.Ref.parse!(\"\", #{inspect(ns)})"
end
defp do_inspect(%Ref{kind: :anchor, ns: ns, arg: anchor}, _) do
"JSV.Ref.parse!(#{inspect("##{anchor}")}, #{inspect(ns)})"
end
defp do_inspect(%Ref{kind: :pointer, ns: ns, arg: path}, _) do
"JSV.Ref.parse!(#{inspect(pointer_url(path))}, #{inspect(ns)})"
end
defp pointer_url(path) do
IO.iodata_to_binary(["#/", Enum.map_intersperse(path, "/", &to_pointer_segment/1)])
end
defp to_pointer_segment(segment) when is_integer(segment) do
segment
|> Integer.to_string()
|> to_pointer_segment()
end
defp to_pointer_segment(segment) when is_binary(segment) do
segment
|> Ref.escape_json_pointer()
|> URI.encode(&URI.char_unreserved?/1)
end
end
defimpl String.Chars, for: JSV.Ref do
alias JSV.Ref
# credo:disable-for-next-line Credo.Check.Readability.Specs
def to_string(ref) do
%Ref{ns: ns, kind: kind, arg: arg} = ref
IO.iodata_to_binary([ns_to_prefix(ns), fragment_to_ref_suffix(kind, arg)])
end
defp ns_to_prefix(:root) do
""
end
defp ns_to_prefix(ns) when is_binary(ns) do
ns
end
defp fragment_to_ref_suffix(:top, _) do
""
end
defp fragment_to_ref_suffix(:anchor, anchor) when is_binary(anchor) do
["#", anchor]
end
defp fragment_to_ref_suffix(:pointer, path) when is_list(path) do
["#/", Enum.map_intersperse(path, "/", &to_pointer_segment/1)]
end
defp to_pointer_segment(segment) when is_integer(segment) do
segment
|> Integer.to_string()
|> to_pointer_segment()
end
defp to_pointer_segment(segment) when is_binary(segment) do
segment
|> Ref.escape_json_pointer()
|> URI.encode(&URI.char_unreserved?/1)
end
end