Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions rebar.config
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{erl_opts, [debug_info]}.
{deps, [
{nova, "0.12.1"},
{nova, "0.13.7"},
{jesse, "1.8.1"}
]}.

{xref_ignores, [
{nova_json_schemas, load_local_schemas, 0}
{nova_json_schemas, init, 0},
{nova_json_schemas, load_local_schemas, 0},
{nova_json_schemas, pre_request, 4},
{nova_json_schemas, post_request, 4}
]}.

{dialyzer, [
Expand Down
12 changes: 6 additions & 6 deletions rebar.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.0">>},2},
{<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},1},
{<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.1">>},0},
{<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.3.3">>},1},
{<<"nova">>,{pkg,<<"nova">>,<<"0.12.1">>},0},
{<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.4.0">>},1},
{<<"nova">>,{pkg,<<"nova">>,<<"0.13.7">>},0},
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},2},
{<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},1},
{<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.1">>},1}]}.
Expand All @@ -14,8 +14,8 @@
{<<"cowlib">>, <<"54592074EBBBB92EE4746C8A8846E5605052F29309D3A873468D76CDF932076F">>},
{<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>},
{<<"jesse">>, <<"C9E3670C7EE40F719734E3BC716578143AABA93FC7525A02A7D5CB300B3AD71E">>},
{<<"jhn_stdlib">>, <<"3E50C560334A85EE5B6C645D2E1BCD35E7BE667E5FCB62AA364F00737C2ADC61">>},
{<<"nova">>, <<"78B60221F632C5C7D1B0EEEABCB7370764304C32C757C0343513DA4108326BD3">>},
{<<"jhn_stdlib">>, <<"FAC6F19B35351278F1CB156E23A5B2A6047A9DD5AB1FD9E1189A7918006DF7ED">>},
{<<"nova">>, <<"C37A2161EA1EE643635739282D1DF9880D20D9521B6AD3ABE6A71C6A6867C842">>},
{<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>},
{<<"routing_tree">>, <<"72ACEF2095F0EC804F7AFD07EF781DDE5009425A1CA0A28F0706B1DB334A4812">>},
{<<"thoas">>, <<"19A25F31177A17E74004D4840F66D791D4298C5738790FA2CC73731EB911F195">>}]},
Expand All @@ -24,8 +24,8 @@
{<<"cowlib">>, <<"7F478D80D66B747344F0EA7708C187645CFCC08B11AA424632F78E25BF05DB51">>},
{<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>},
{<<"jesse">>, <<"0EDED3F18623FDA2F25989804A06CF518B4ACF2E9365B18C8E8C013D7E3C906F">>},
{<<"jhn_stdlib">>, <<"2CB184C505397B62A842AB3DE13F21B83ADF62364BD35A572191629E30E0258E">>},
{<<"nova">>, <<"C43DC05F0B0F31FD3CF3B0415F2772BFD9FFD75DFEBFFE0B806DE85B6581F4C7">>},
{<<"jhn_stdlib">>, <<"7EABD1B01D2DEFF495BF7C5CA1DBA4D3FA0B84DC3AF03CA85F31D52EBB03C6FC">>},
{<<"nova">>, <<"22F4C3FFDC08DB8568F3DA198647823D63123520F69C7D718A8DB468F4C7A1EA">>},
{<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>},
{<<"routing_tree">>, <<"85982C7AC502892C5179CD2A591331003BACD2D2A71723640BA7D23F45408E6E">>},
{<<"thoas">>, <<"E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A">>}]}
Expand Down
98 changes: 50 additions & 48 deletions src/nova_json_schemas.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
-behaviour(nova_plugin).

-export([
init/0,
load_local_schemas/0,
pre_request/2,
post_request/2,
pre_request/4,
post_request/4,
plugin_info/0
]).

-include_lib("kernel/include/logger.hrl").

init() ->
jesse_database:load_all(),
load_local_schemas(),
#{}.

%%--------------------------------------------------------------------
%% @doc
%% Load all local JSON schemas from the main application's
Expand All @@ -34,88 +40,85 @@ load_local_schemas() ->
%% Pre-request callback
%% @end
%%--------------------------------------------------------------------
-spec pre_request(Req :: cowboy_req:req(), Options :: map()) ->
{ok, Req0 :: cowboy_req:req()}
| {stop, Req0 :: cowboy_req:req()}
-spec pre_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) ->
{ok, Req0 :: cowboy_req:req(), NewState :: any()}
| {stop, Req0 :: cowboy_req:req(), NewState :: any()}
| {error, Reason :: term()}.
pre_request(
Req = #{extra_state := #{json_schema := SchemaLocation} = Extra, json := JSON}, Options
Req = #{extra_state := #{json_schema := SchemaLocation} = Extra, json := JSON},
_Env,
Options,
State
) ->
JesseOpts = maps:get(jesse_options, Extra, []),
%% JSON have already been parsed so we can just continue with the validation
case validate_json(SchemaLocation, JSON, JesseOpts) of
ok ->
?LOG_DEBUG("Schema validation on JSON body successful"),
{ok, Req};
{ok, Req, State};
{error, Errors} ->
?LOG_DEBUG("Got validation-errors on JSON body. Errors: ~p", [Errors]),
case maps:get(render_errors, Options, false) of
true ->
?LOG_DEBUG("Rendering validation-errors and send back to requester"),
JsonLib = nova:get_env(json_lib, thoas),
Req0 = cowboy_req:set_resp_headers(
#{<<"content-type">> => <<"application/json">>}, Req
),
ErrorStruct = render_error(Errors),
ErrorJson = erlang:apply(JsonLib, encode, [ErrorStruct]),
ErrorJson = json:encode(ErrorStruct),
Req1 = cowboy_req:set_resp_body(ErrorJson, Req0),
Req2 = cowboy_req:reply(400, Req1),
{stop, Req2};
{stop, Req2, State};
_ ->
?LOG_DEBUG(
"render_errors-option not set for plugin nova_json_schemas - returning plain 400-status to requester"
),
Req0 = cowboy_req:reply(400, Req),
{stop, Req0}
{stop, Req0, State}
end
end;
pre_request(#{extra_state := #{json_schema := _SchemaLocation}}, _Options) ->
%% The body have not been parsed. Log and error and stop
pre_request(#{extra_state := #{json_schema := _SchemaLocation}}, _Env, _Options, _State) ->
?LOG_ERROR(
"JSON Schema is set in 'extra_state' but body have not yet been parsed - rearrange your plugins so that JSON plugin is ran before this.."
),
{error, body_not_parsed};
pre_request(Req, _Options) ->
%% 'json_schema' is not set or 'extra_state' is completly missing. Just continue.
HasBody = cowboy_req:has_body(Req),
if
HasBody ->
?LOG_DEBUG("No schema is set for this route so will continue executing");
true ->
ok
end,
{ok, Req}.
pre_request(Req, _Env, _Options, State) ->
{ok, Req, State}.

%%--------------------------------------------------------------------
%% @doc
%% Post-request callback
%% @end
%%--------------------------------------------------------------------
-spec post_request(Req :: cowboy_req:req(), Options :: map()) ->
{ok, Req0 :: cowboy_req:req()}
| {stop, Req0 :: cowboy_req:req()}
| {error, Reason :: term()}.
post_request(Req, _Options) ->
{ok, Req}.
-spec post_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) ->
{ok, Req0 :: cowboy_req:req(), NewState :: any()}.
post_request(Req, _Env, _Options, State) ->
{ok, Req, State}.

%%--------------------------------------------------------------------
%% @doc
%% nova_plugin callback. Returns information about the plugin.
%% @end
%%--------------------------------------------------------------------
-spec plugin_info() ->
{Title :: binary(), Version :: binary(), Author :: binary(), Description :: binary(), [
{Key :: atom(), OptionDescription :: binary()}
]}.
#{
title := binary(),
version := binary(),
url := binary(),
authors := [binary()],
description := binary(),
options := [{Key :: atom(), OptionDescription :: binary()}]
}.
plugin_info() ->
{ok, Vsn} = application:get_key(nova_json_schemas, vsn),
{ok, Desc} = application:get_key(nova_json_schemas, description),

{<<"JSON schema plugin">>, list_to_binary(Vsn), <<"Niclas Axelsson <niclas@burbas.se">>,
list_to_binary(Desc), [
{render_errors, <<"If this is set, validation-errors is returned to the requester">>}
%% Options is specified as {Key, Description}
]}.
#{
title => <<"Nova JSON Schema plugin">>,
version => <<"0.2.0">>,
url => <<"https://github.com/novaframework/nova_json_schemas">>,
authors => [<<"Niclas Axelsson <niclas@burbas.se>">>],
description => <<"Validates JSON request bodies against JSON schemas using jesse">>,
options => [
{render_errors, <<"If true, validation errors are returned as JSON to the requester">>}
]
}.

validate_json(SchemaLocation, Json, JesseOpts) ->
case jesse:validate(SchemaLocation, Json, JesseOpts) of
Expand All @@ -125,8 +128,7 @@ validate_json(SchemaLocation, Json, JesseOpts) ->
PrivDir = code:priv_dir(MainApp),
SchemaLocation0 = filename:join([PrivDir, SchemaLocation]),
{ok, Filecontent} = file:read_file(SchemaLocation0),
JsonLib = nova:get_env(json_lib, thoas),
{ok, Schema} = erlang:apply(JsonLib, decode, [Filecontent]),
Schema = json:decode(Filecontent),
jesse:add_schema(SchemaLocation, Schema),
validate_json(SchemaLocation, Json, JesseOpts);
{error, ValidationError} ->
Expand Down Expand Up @@ -178,12 +180,12 @@ load_schemas_from_dir(Dir, RelativePrefix) ->
load_schema_file(FilePath, RelativePath) ->
case file:read_file(FilePath) of
{ok, FileContent} ->
JsonLib = nova:get_env(json_lib, thoas),
case erlang:apply(JsonLib, decode, [FileContent]) of
{ok, Schema} ->
try json:decode(FileContent) of
Schema ->
jesse:add_schema(RelativePath, Schema),
?LOG_DEBUG("Loaded JSON schema: ~s", [RelativePath]);
{error, Reason} ->
?LOG_DEBUG("Loaded JSON schema: ~s", [RelativePath])
catch
error:Reason ->
?LOG_ERROR("Failed to decode JSON schema ~s: ~p", [FilePath, Reason])
end;
{error, Reason} ->
Expand Down
Loading