{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Events API Schema",
"definitions": {
"event_collection": {
"type": "object",
"required": ["events"],
"properties": {
"events": {
"type": "array",
"items": {
"$ref": "#/definitions/event"
}
}
}
},
"event": {
"type": "object",
"required": ["name", "timestamp"],
"properties": {
"name": {
"type": "string"
},
"timestamp": {
"type": ["string", "integer"],
"format": "date-time"
},
"attributes": {
"type": "object"
}
}
}
}
}
defmodule JsonSchema do
@moduledoc ~S"""
A service which validates objects according to types defined
in `schema.json`.
"""
@doc ~S"""
Validates an object by type. Returns a list of {msg, [columns]} tuples
describing any validation errors, or [] if validation succeeded.
"""
def validate(server \\ :json_schema, object, type) do
GenServer.call(server, {:validate, object, type})
end
@doc ~S"""
Returns true if the object is valid according to the specified type,
false otherwise.
"""
def valid?(server \\ :json_schema, object, type) do
[] == validate(server, object, type)
end
@doc ~S"""
Converts the output of `validate/3` into a JSON-compatible structure,
a list of error messages.
"""
def errors_to_json(errors) do
errors |> Enum.map(fn ({msg, _cols}) -> msg end)
end
use GenServer
def init(_) do
schema = File.read!(Application.app_dir(:myapp) <> "/priv/schema.json")
|> Poison.decode!
|> ExJsonSchema.Schema.resolve
{:ok, schema}
end
### FROM DREW:
### This is a more minor quible, but I thought I'd throw it in to try and help:
#
# Calling validate should not be done from inside the GenServer
# Doing so means that there is essentially a single-line queue where ALL
# validate calls are passing through this single synchronous mailbox.
# So if you've got 1,000 phoenix connections/processes all trying to validate
# input, they will wait one by one to validate.
#
# There's no reason for this, as the only thing the GenServer should be responsible
# for is holding the state (the resolved schema). Validating doesn't CHANGE the state.
# So a normal module function can fetch the resolved schema from the GenServer
# and use it in the validate call.
#
def handle_call({:validate, object, type}, _from, schema) do
errors = get_validation_errors(object, type, schema)
|> transform_errors
{:reply, errors, schema}
end
defp get_validation_errors(object, type, schema) do
type_string = type |> to_string
type_schema = schema.schema["definitions"][type_string]
not_a_struct = case object do
%{__struct__: _} -> Map.from_struct(object)
_ -> object
end
string_keyed_object = ensure_key_strings(not_a_struct)
## validate throws a BadMapError on certain kinds of invalid
## input; absorb it (TODO fix ExJsonSchema upstream)
try do
### FROM DREW:
### This call is a big problem:
# type_schema is a %{} (Plain map). It's *NOT* a completely resolved schema (it's no longer a %ExJsonSchema.Schema.Root{}
# This means that it will match on https://github.com/jonasschmidt/ex_json_schema/blob/238f565a62acaf6e304c66c3cca4d80d2ef43d22/lib/ex_json_schema/validator.ex#L23-L25
# and RE-CALL .resolve every time. Exactly what you were trying to avoid
ExJsonSchema.Validator.validate(schema, type_schema, string_keyed_object)
rescue
_ -> [{"Failed validation", []}]
end
end
@doc ~S"""
Makes sure that all the keys in the map are strings and not atoms.
Works on nested data structures.
"""
defp ensure_key_strings(x) do
cond do
is_map x ->
Enum.reduce x, %{}, fn({k,v}, acc) ->
Map.put acc, to_string(k), ensure_key_strings(v)
end
is_list x ->
Enum.map(x, fn (v) -> ensure_key_strings(v) end)
true ->
x
end
end
end