Packages

ISO/HL7 FHIR Application Server

Current section

Files

Jump to
hl7 lib validation json_schema.ex
Raw

lib/validation/json_schema.ex

defmodule Xema.JsonSchema do
@moduledoc """
Converts a JSON Schema to Xema source.
"""
alias Xema.{
JsonSchema.Validator,
Schema,
SchemaError
}
@type json_schema :: true | false | map
@type opts :: [draft: String.t()]
@drafts ~w(draft4 draft6 draft7)
@schema ~w(
additional_items
additional_properties
property_names
not
if
then
else
contains
items
)a
@schemas ~w(
all_of
any_of
one_of
items
)a
@schema_map ~w(
definitions
pattern_properties
properties
)a
defmacro keywords() do
Schema.keywords()
|> Enum.map(&to_string/1)
|> ConvCase.to_camel_case()
|> List.delete("ref")
|> List.delete("schema")
|> Enum.concat(["$ref", "$id", "$schema"])
end
@doc """
This function converts a JSON Schema in Xema schema source. The argument
`json_schema` is expected as a decoded JSON Schema.
All keys that are not standard JSON Schema keywords have to be known atoms. If
the schema has additional keys that are unknown atoms the option
`atom: :force` is needed. In this case the atoms will be created. This is not
needed for keys expected by JSON Schema (e.g. in properties)
Options:
* `:draft` specifies the draft to check the given JSON Schema. Possible values
are `"draft4"`, `"draft6"`, and `"draft7"`, default is `"draft7"`. If
`:draft` not set and the schema contains `$schema` then the value for
`$schema` is used for this option.
* `:atom` creates atoms for unknown atoms when set to `:force`. This is just
needed for additional JSON Schema keywords.
## Examples
iex> Xema.JsonSchema.to_xema(%{"type" => "integer", "minimum" => 5})
{:integer, [minimum: 5]}
iex> schema = %{
...> "type" => "object",
...> "properties" => %{"foo" => %{"type" => "integer"}}
...> }
iex> Xema.JsonSchema.to_xema(schema)
{:map, [keys: :strings, properties: %{"foo" => :integer}]}
iex> Xema.JsonSchema.to_xema(%{"type" => "integer", "foo" => "bar"}, atom: :force)
{:integer, [foo: "bar"]}
"""
@spec to_xema(json_schema, opts) :: atom | tuple
def to_xema(json_schema, opts \\ []) do
draft = draft(json_schema, opts)
case Validator.validate(draft, json_schema) do
:ok ->
do_to_xema(json_schema, opts)
{:error, :unknown} ->
raise "unknown draft #{inspect(draft)}, has to be one of #{inspect(@drafts)}"
{:error, reason} ->
raise SchemaError, reason
end
end
defp do_to_xema(json, opts) when is_map(json) do
{type, json} = type(json)
cond do
Enum.empty?(json) ->
type
type == :map ->
{:map, json |> Map.put("keys", :strings) |> schema(opts)}
true ->
{type, schema(json, opts)}
end
end
defp do_to_xema(json, _) when is_boolean(json), do: json
defp type(map) do
{type, map} = Map.pop(map, "type", :any)
{type_to_atom(type), map}
end
defp draft(json_schema, opts) when is_map(json_schema) do
draft = Map.get(json_schema, "$schema", "draft7")
Keyword.get(opts, :draft, draft)
end
defp draft(_json_schema, opts), do: Keyword.get(opts, :draft, "draft7")
defp type_to_atom(list) when is_list(list), do: Enum.map(list, &type_to_atom/1)
defp type_to_atom("object"), do: :map
defp type_to_atom("array"), do: :list
defp type_to_atom("null"), do: nil
defp type_to_atom(type) when is_binary(type), do: to_existing_atom(type)
defp type_to_atom(type), do: type
defp schema(json, opts) do
json
|> Enum.map(&rule(&1, opts))
|> Keyword.new()
end
# handles all rules with a regular keyword
def keywordsDefault(key,value,opts) do
key
|> String.trim_leading("$")
|> ConvCase.to_snake_case()
|> to_existing_atom(opts)
|> rule(value, opts)
end
#["propertyNames", "maxProperties", "contentMediaType", "examples", "default",
# "if", "description", "maximum", "id", "items", "then", "additionalItems",
# "definitions", "properties", "patternProperties", "contains", "oneOf", "else",
# "minItems", "caster", "contentEncoding", "pattern", "title",
# "exclusiveMinimum", "enum", "maxLength", "type", "additionalProperties",
# "required", "validator", "keys", "maxItems", "module", "dependencies", "not",
# "anyOf", "multipleOf", "format", "minLength", "const", "allOf", "minimum",
# "exclusiveMaximum", "uniqueItems", "comment", "minProperties", "$ref", "$id",
# "$schema"]
defp rule({"propertyNames" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"maxProperties" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"contentEndoding" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"contentMediaType" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"examples" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"contains" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"default" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"if" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"description" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"id" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"items" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"then" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"additionalItems" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"definitions" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"properties" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"comment" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"module" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"caster" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"pattern" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"title" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"else" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"validator" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"enum" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"oneOf" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"allOf" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"anyOf" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"multipleOf" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"format" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"not" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"dependencies" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"minLength" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"maxLength" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"minItems" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"maxItems" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"exclusiveMinimum" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"exclusiveMaximum" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"const" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"minimum" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"maximum" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"uniqueItems" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"minProperties" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"additionalProperties" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"patternProperties" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"required" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"keys" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"type" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"$ref" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"$id" = key, value}, opts), do: keywordsDefault(key,value,opts)
defp rule({"$schema" = key, value}, opts), do: keywordsDefault(key,value,opts)
# defp rule({key, value}, opts) when key in keywords() do
# key
# |> String.trim_leading("$")
# |> ConvCase.to_snake_case()
# |> to_existing_atom(opts)
# |> rule(value, opts)
# end
# handles all rules without a regular keyword
defp rule({key, value}, opts) when is_binary(key) and is_map(value) do
value =
case schema?(value) do
true -> do_to_xema(value, opts)
false -> schema(value, opts)
end
{to_existing_atom(key, opts), value}
end
defp rule({key, value}, opts), do: {to_existing_atom(key, opts), value}
defp rule(:format, value, _) do
format = value |> ConvCase.to_snake_case()
|> to_existing_atom(maybe: true)
case is_atom(format) do
true -> {:format, format}
false -> {:format, :unsupported}
end
end
defp rule(:dependencies, value, opts) do
value =
Enum.into(value, %{}, fn
{key, value} when is_map(value) -> {key, do_to_xema(value, opts)}
{key, value} -> {key, value}
end)
{:dependencies, value}
end
defp rule(key, value, opts) when key in @schema_map do
{key, Enum.into(value, %{}, fn {key, value} -> {key, do_to_xema(value, opts)} end)}
end
defp rule(key, value, opts) when key in @schemas and is_list(value) do
{key, Enum.map(value, &do_to_xema(&1, opts))}
end
defp rule(key, value, opts) when key in @schema do
{key, do_to_xema(value, opts)}
end
defp rule(key, value, _), do: {key, value}
defp schema?(value) do
value |> Map.keys() |> Enum.any?(fn key -> Enum.member?(keywords(), key) end)
end
defp to_existing_atom(str, opts \\ []) do
case Keyword.get(opts, :atom, :force) do
:existing -> String.to_existing_atom(str)
:force -> String.to_atom(str)
end
rescue
_ ->
case Keyword.get(opts, :maybe, false) do
true ->
str
false ->
reraise SchemaError,
"All additional schema keys must be existing atoms. Missing atom for #{str}",
__STACKTRACE__
end
end
end