Packages
jsv
0.7.2
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/normalizer.ex
defmodule JSV.Normalizer do
alias JSV.Helpers.Traverse
@moduledoc """
A Normalizer for JSON data structures.
"""
@type json_decoded_form ::
%{optional(String.t()) => json_decoded_form}
| [json_decoded_form]
| String.t()
| number()
| true
| false
| nil
@doc_json_compat """
Returns the given term in a JSON-decoded form without general atoms or structs.\
"""
@doc """
#{@doc_json_compat}
See `normalize/3` for details and options.
### Examples
iex> JSV.Normalizer.normalize(%{name: :joe})
%{"name" => "joe"}
iex> JSV.Normalizer.normalize(%{"name" => :joe})
%{"name" => "joe"}
iex> JSV.Normalizer.normalize(%{"name" => "joe"})
%{"name" => "joe"}
iex> JSV.Normalizer.normalize(%{true: false})
%{"true" => false}
iex> JSV.Normalizer.normalize(%{specials: [true, false, nil]})
%{"specials" => [true, false, nil]}
This function is also used internally to normalize schemas.
iex> JSV.Normalizer.normalize(%JSV.Schema{title: nil, properties: nil})
%{}
iex> JSV.Normalizer.normalize(%JSV.Schema{type: :integer})
%{"type" => "integer"}
iex> JSV.Normalizer.normalize(%JSV.Schema{title: :"My Schema"})
%{"title" => "My Schema"}
Other structs must implement the `JSV.Normalizer.Normalize` protocol.
iex> defimpl JSV.Normalizer.Normalize, for: Range do
iex> def normalize(range), do: Map.from_struct(range)
iex> end
iex> JSV.Normalizer.normalize(1..10)
%{"first" => 1, "last" => 10, "step" => 1}
"""
@spec normalize(term, keyword) :: json_decoded_form
def normalize(term, opts \\ []) do
{normalized, _acc} = normalize(term, opts, [])
normalized
end
@doc """
#{@doc_json_compat} Accepts and returns an accumulator.
### What is _JSON-decoded_ form?
By that we mean that the returned data could have been returned by
`JSON.decode!/1`:
* Only maps, lists, strings, numbers and atoms.
* Structs must implement the `JSV.Normalizer.Normalize` protocol.
* `true`, `false` and `nil` will be kept as-is in all places except for map
keys.
* `true`, `false` and `nil` as map keys will be converted to string.
* Other atoms as values will be passed to the `:on_general_atom` callback (see
options).
* Map keys must only be atoms, strings or numbers and will be converted to
strings.
### Options
* `:on_general_atom` - A callback accepting an atom found in the data and the
accumulator. Must return a JSON-compatible
### Examples
iex> on_general_atom = fn atom, acc ->
...> {"found:\#{atom}", [atom|acc]}
...> end
iex> opts = [on_general_atom: on_general_atom]
iex> acc_in = []
iex> JSV.Normalizer.normalize(%{an_atom: SomeAtom, a_string: "hello"}, opts, acc_in)
{%{"an_atom" => "found:Elixir.SomeAtom", "a_string" => "hello"}, [SomeAtom]}
"""
@spec normalize(term, keyword, term) :: {json_decoded_form, term}
def normalize(term, opts, acc_in) do
on_general_atom =
case Keyword.fetch(opts, :on_general_atom) do
{:ok, f} when is_function(f, 2) -> f
:error -> &default_on_general_atom/2
end
Traverse.postwalk(term, acc_in, fn
{:val, v}, acc when is_binary(v) when is_list(v) when is_map(v) when is_number(v) ->
{v, acc}
{:val, v}, acc when v in [true, false, nil] ->
{v, acc}
{:val, v}, acc when is_atom(v) ->
on_general_atom.(v, acc)
{:val, other}, _acc ->
raise ArgumentError, "invalid value in JSON data: #{inspect(other)}"
{:key, k}, acc when is_binary(k) ->
{k, acc}
{:key, k}, acc when is_atom(k) ->
{Atom.to_string(k), acc}
{:key, k}, acc when is_number(k) ->
{to_string(k), acc}
{:key, other}, _acc ->
raise ArgumentError, "invalid key in JSON data: #{inspect(other)}"
{:struct, %_{} = struct, cont}, acc ->
data = JSV.Normalizer.Normalize.normalize(struct)
cont.(data, acc)
end)
end
@doc """
Default implementation for the `:on_general_atom` option of `normalize/2` and
`normalize/3`. Transforms atoms to strings.
"""
@spec default_on_general_atom(atom, term) :: {String.t(), term}
def default_on_general_atom(atom, acc) do
{Atom.to_string(atom), acc}
end
end