Current section

Files

Jump to
phoenix_swagger lib validator.ex
Raw

lib/validator.ex

defmodule PhoenixSwagger.Validator do
@moduledoc """
The PhoenixSwagger.Validator module provides converter of
swagger schema to ex_json_schema structure for further validation.
There are two main functions:
* parse_swagger_schema/1
* validate/2
Before `validate/2` will be called, a swagger schema should be parsed
for futher validation with the `parse_swagger_schema/1`. This function
takes path to a swagger schema and returns it in ex_json_schema format.
During execution of the `parse_swagger_schema/1` function, it creates
the `validator_table` ets table and stores associative key/value there.
Where `key` is an API path of a resource and `value` is input parameters
of a resource.
To validate of a parsed swagger schema, the `validate/1` should be used.
For more information, see more in ./phoenix_swagger/tests/ directory.
"""
@doc """
The `parse_swagger_schema/1` takes path to a swagger schema, parses it
into ex_json_schema format and store to the `validator_table` ets
table.
Usage:
iex(1)> parse_swagger_schema("my_json_spec.json")
[{"/person", %{'__struct__' => 'Elixir.ExJsonSchema.Schema.Root',
location => root,
refs => %{},
schema => %{
"properties" => %{
"name" => %{"type" => "string"},
"age" => %{"type" => "integer"}
}
}
}
}]
"""
@table_owner TableOwner
def parse_swagger_schema(spec) do
schema = File.read(spec) |> elem(1) |> Poison.decode() |> elem(1)
# get rid from all keys besides 'paths' and 'definitions' as we
# need only in these fields for validation
schema = Enum.reduce(schema, %{}, fn(map, acc) ->
{key, val} = map
if key in ["paths", "definitions"] do
Map.put_new(acc, key, val)
else
acc
end
end)
# parse swagger schema
schema = Enum.map(schema["paths"], fn({path, data}) ->
parameters = data[Map.keys(data) |> List.first]["parameters"]
# we may have a request without parameters, so nothing to validate
# in this case
if parameters == nil do
[]
else
# Let's go through requests parameters from swagger scheme
# and collect it into json schema properties.
properties = Enum.reduce(parameters, %{}, fn(parameter, acc) ->
Map.merge(acc, get_property_type(schema, parameter, "", acc))
end)
schema_object = %{"type" => "object", "properties" => properties, "definitions" => schema["definitions"]}
GenServer.cast(@table_owner, {:insert, path, ExJsonSchema.Schema.resolve(schema_object)})
{path, ExJsonSchema.Schema.resolve(schema_object)}
end
end) |> List.flatten
schema
end
@doc """
The `validate/2` takes a resource path and input parameters
of this resource.
Returns `:ok` in a case when parameters are valid for the
given resource or:
* {:error, :path_not_exists} in a case when path is not
exists in the validator table;
* {:error, error_message, path} in a case when at least
one parameter is not valid for the given resource.
"""
def validate(path, params) do
case GenServer.call(@table_owner, {:lookup, path}) do
[] ->
{:error, :path_not_exists}
[{_, schema}] ->
case ExJsonSchema.Validator.validate(schema, params) do
:ok ->
:ok
{:error, [{error, path}]} ->
{:error, error, path}
end
end
end
@doc false
defp get_property_type(schema, parameter, property_name, acc) do
{properties, has_properties?} = get_properties(schema, parameter, parameter["schema"] || parameter)
if has_properties? do
collect_properties(schema, parameter, properties, property_name)
else
Map.put_new(acc || %{}, parameter["name"], %{"type" => parameter["type"]})
end
end
@doc false
defp get_properties(schema, parameter, ref) do
if ref["$ref"] != nil do
[_, definition, path] = String.split(ref["$ref"], "/")
props = schema[definition][path]["properties"]
{props, props != nil}
else
if parameter["properties"] != nil do
{parameter["properties"], true}
else
props = parameter["schema"]["properties"]
{props, props != nil}
end
end
end
@doc false
defp collect_properties(schema, parameter, properties, property_name) do
Enum.reduce(properties, %{}, fn({property, type}, ref_acc) ->
if type["$ref"] != nil do
[_, definition, path] = String.split(type["$ref"], "/")
type = schema[definition][path]["type"]
if type == "object" or type == "array" do
Map.put_new(ref_acc, path, %{"type" => type, "$ref" => "#/definitions/" <> path})
else
Map.put_new(ref_acc, property, %{"type" => schema[definition][path]["type"]})
end
else
if ref_acc[parameter["name"]] == nil do
if parameter["name"] == nil do
map = Map.put_new(%{}, property, %{"type" => type["type"]})
ref_acc = if ref_acc[property_name] == nil do
Map.put_new(ref_acc, property_name, map)
else
Map.put(ref_acc, property_name, Map.merge(ref_acc[property_name] || %{}, map))
end
ref_acc
else
ref_acc = Map.put_new(ref_acc, property, %{"type" => type["type"]})
ref_acc
end
else
map = Map.put_new(ref_acc[parameter["name"]], property, %{"type" => type["type"]})
ref_acc = Map.delete(ref_acc, parameter["name"])
|> Map.put_new(parameter["name"], map)
end
end
end)
end
end