From 16a82eb1d81c139d4640484b06945685a1b26a85 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 11 Mar 2026 22:00:20 +0100 Subject: [PATCH 1/4] feat: replace thoas with Erlang/OTP json module Use json:encode/1 and json:decode/1 directly instead of the configurable json_lib approach via nova:get_env. The json_lib config option was removed from Nova. --- src/nova_json_schemas.erl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/nova_json_schemas.erl b/src/nova_json_schemas.erl index a6313ea..1cfccb4 100644 --- a/src/nova_json_schemas.erl +++ b/src/nova_json_schemas.erl @@ -52,12 +52,11 @@ pre_request( 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}; @@ -125,8 +124,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} -> @@ -178,12 +176,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} -> From edc2b3cb47370c039869e4ffec58a0fc82b537c5 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 11 Mar 2026 22:07:42 +0100 Subject: [PATCH 2/4] feat: update plugin callbacks to 4-arity Nova API Update pre_request/post_request to 4-arity callbacks matching the current nova_plugin behaviour. Update plugin_info to return a map instead of a tuple. --- src/nova_json_schemas.erl | 70 +++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/nova_json_schemas.erl b/src/nova_json_schemas.erl index 1cfccb4..98901d9 100644 --- a/src/nova_json_schemas.erl +++ b/src/nova_json_schemas.erl @@ -3,8 +3,8 @@ -export([ load_local_schemas/0, - pre_request/2, - post_request/2, + pre_request/4, + post_request/4, plugin_info/0 ]). @@ -34,19 +34,18 @@ 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 @@ -59,62 +58,53 @@ pre_request( 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()} - ]}. +-spec plugin_info() -> #{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 >, - 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 ">>], + 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 From b096ca5939dd6448c306895d57a7f351a78fb277 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 11 Mar 2026 22:31:13 +0100 Subject: [PATCH 3/4] feat: add init/0 callback to start jesse database and preload schemas --- src/nova_json_schemas.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/nova_json_schemas.erl b/src/nova_json_schemas.erl index 98901d9..d15dc9f 100644 --- a/src/nova_json_schemas.erl +++ b/src/nova_json_schemas.erl @@ -2,6 +2,7 @@ -behaviour(nova_plugin). -export([ + init/0, load_local_schemas/0, pre_request/4, post_request/4, @@ -10,6 +11,11 @@ -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 From 0e8ef0e1e59bfcb6c4bc5203234634e76210b1f8 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 18 Mar 2026 08:21:47 +0100 Subject: [PATCH 4/4] fix: update rebar.config for new plugin format - Bump nova dep to 0.13.7 (matches 4-arity plugin API) - Add xref_ignores for init/0, pre_request/4, post_request/4 - Apply erlfmt formatting --- rebar.config | 7 +++++-- rebar.lock | 12 ++++++------ src/nova_json_schemas.erl | 38 +++++++++++++++++++++++--------------- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/rebar.config b/rebar.config index 543589b..57d87d3 100644 --- a/rebar.config +++ b/rebar.config @@ -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, [ diff --git a/rebar.lock b/rebar.lock index 44b518e..569747f 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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}]}. @@ -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">>}]}, @@ -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">>}]} diff --git a/src/nova_json_schemas.erl b/src/nova_json_schemas.erl index d15dc9f..01c97d6 100644 --- a/src/nova_json_schemas.erl +++ b/src/nova_json_schemas.erl @@ -45,7 +45,10 @@ load_local_schemas() -> | {stop, Req0 :: cowboy_req:req(), NewState :: any()} | {error, Reason :: term()}. pre_request( - Req = #{extra_state := #{json_schema := SchemaLocation} = Extra, json := JSON}, _Env, Options, State + Req = #{extra_state := #{json_schema := SchemaLocation} = Extra, json := JSON}, + _Env, + Options, + State ) -> JesseOpts = maps:get(jesse_options, Extra, []), case validate_json(SchemaLocation, JSON, JesseOpts) of @@ -96,21 +99,26 @@ post_request(Req, _Env, _Options, State) -> %% nova_plugin callback. Returns information about the plugin. %% @end %%-------------------------------------------------------------------- --spec plugin_info() -> #{title := binary(), - version := binary(), - url := binary(), - authors := [binary()], - description := binary(), - options := [{Key :: atom(), OptionDescription :: binary()}]}. +-spec plugin_info() -> + #{ + title := binary(), + version := binary(), + url := binary(), + authors := [binary()], + description := binary(), + options := [{Key :: atom(), OptionDescription :: binary()}] + }. plugin_info() -> - #{title => <<"Nova JSON Schema plugin">>, - version => <<"0.2.0">>, - url => <<"https://github.com/novaframework/nova_json_schemas">>, - authors => [<<"Niclas Axelsson ">>], - description => <<"Validates JSON request bodies against JSON schemas using jesse">>, - options => [ - {render_errors, <<"If true, validation errors are returned as JSON to the requester">>} - ]}. + #{ + title => <<"Nova JSON Schema plugin">>, + version => <<"0.2.0">>, + url => <<"https://github.com/novaframework/nova_json_schemas">>, + authors => [<<"Niclas Axelsson ">>], + 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