Skip to content
Open
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
27 changes: 15 additions & 12 deletions .github/workflows/erlang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ on:
branches: [master]
pull_request:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-24.04
name: Erlang/OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}}
strategy:
fail-fast: false
matrix:
otp: ['26.1', '27.1', '28.0']
rebar3: ['3.25.0']
otp: ['27.3', '28.3']
rebar3: ['3.26.0']
steps:
- uses: actions/checkout@v6
- uses: erlef/setup-beam@v1
Expand All @@ -22,7 +25,7 @@ jobs:
rebar3-version: ${{matrix.rebar3}}
version-type: strict
- name: Cache rebar3 deps and build
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/rebar3
Expand All @@ -39,8 +42,8 @@ jobs:
strategy:
fail-fast: false
matrix:
otp: ['26.1', '27.1', '28.0']
rebar3: ['3.25.0']
otp: ['27.3', '28.3']
rebar3: ['3.26.0']
steps:
- uses: actions/checkout@v6
- uses: erlef/setup-beam@v1
Expand All @@ -49,7 +52,7 @@ jobs:
rebar3-version: ${{matrix.rebar3}}
version-type: strict
- name: Cache rebar3 deps and build
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/rebar3
Expand All @@ -65,8 +68,8 @@ jobs:
strategy:
fail-fast: false
matrix:
otp: ['26.1', '27.1', '28.0']
rebar3: ['3.25.0']
otp: ['27.3', '28.3']
rebar3: ['3.26.0']
steps:
- uses: actions/checkout@v6
- uses: erlef/setup-beam@v1
Expand All @@ -75,7 +78,7 @@ jobs:
rebar3-version: ${{matrix.rebar3}}
version-type: strict
- name: Cache rebar3 deps and build
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/rebar3
Expand All @@ -91,8 +94,8 @@ jobs:
strategy:
fail-fast: false
matrix:
otp: ['26.1', '27.1', '28.0']
rebar3: ['3.25.0']
otp: ['27.3', '28.3']
rebar3: ['3.26.0']
steps:
- uses: actions/checkout@v6
- uses: erlef/setup-beam@v1
Expand All @@ -101,7 +104,7 @@ jobs:
rebar3-version: ${{matrix.rebar3}}
version-type: strict
- name: Cache rebar3 deps and build
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/rebar3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0

Expand Down
11 changes: 1 addition & 10 deletions guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,9 @@ These parameters can be specified in your *main* application (Eg the one you've

| Key | Description | Value |
|-----|-------------|-------|
| `json_lib` | JSON lib to use. Read more in the subsection *Configure json lib* | `atom()` |
| `watchers` | Watchers are external programs that will run together with Nova. Watchers are defined as list of tuples where the tuples is in format `{Command, ArgumentList}` (Like `[{my_app, "npm", ["run", "watch"], #{workdir => "priv/assets/js/my-app"}}]`) | `[{string(), string()}] | [{atom(), string(), map()}] | [{atom(), string(), list(), map()}]` |



### Configure json_lib

One can configure which json library to use for encoding/decoding json structures. The module defined for this should expose two different functions:

`encode(Structure) -> binary() | iolist()`

`decode(JsonString) -> {ok, Structure}`
> **Note:** Nova uses the Erlang/OTP `json` module for JSON encoding and decoding. The `json_lib` configuration option has been removed.


## Handling errors in Nova
Expand Down
3 changes: 1 addition & 2 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
{cowboy, "2.13.0"},
{erlydtl, "0.14.0"},
{jhn_stdlib, "5.4.0"},
{routing_tree, "1.0.11"},
{thoas, "1.2.1"}
{routing_tree, "1.0.11"}
]}.

{profiles, [
Expand Down
21 changes: 9 additions & 12 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
{"1.2.0",
[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.13.0">>},0},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},1},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.0">>},1},
{<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},0},
{<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.4.0">>},0},
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},1},
{<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},0},
{<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.1">>},0}]}.
{<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},1},
{<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},0}]}.
[
{pkg_hash,[
{<<"cowboy">>, <<"09D770DD5F6A22CC60C071F432CD7CB87776164527F205C5A6B0F24FF6B38990">>},
{<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>},
{<<"cowlib">>, <<"54592074EBBBB92EE4746C8A8846E5605052F29309D3A873468D76CDF932076F">>},
{<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>},
{<<"jhn_stdlib">>, <<"FAC6F19B35351278F1CB156E23A5B2A6047A9DD5AB1FD9E1189A7918006DF7ED">>},
{<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>},
{<<"routing_tree">>, <<"72ACEF2095F0EC804F7AFD07EF781DDE5009425A1CA0A28F0706B1DB334A4812">>},
{<<"thoas">>, <<"19A25F31177A17E74004D4840F66D791D4298C5738790FA2CC73731EB911F195">>}]},
{<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>},
{<<"routing_tree">>, <<"72ACEF2095F0EC804F7AFD07EF781DDE5009425A1CA0A28F0706B1DB334A4812">>}]},
{pkg_hash_ext,[
{<<"cowboy">>, <<"E724D3A70995025D654C1992C7B11DBFEA95205C047D86FF9BF1CDA92DDC5614">>},
{<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>},
{<<"cowlib">>, <<"7F478D80D66B747344F0EA7708C187645CFCC08B11AA424632F78E25BF05DB51">>},
{<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>},
{<<"jhn_stdlib">>, <<"7EABD1B01D2DEFF495BF7C5CA1DBA4D3FA0B84DC3AF03CA85F31D52EBB03C6FC">>},
{<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>},
{<<"routing_tree">>, <<"85982C7AC502892C5179CD2A591331003BACD2D2A71723640BA7D23F45408E6E">>},
{<<"thoas">>, <<"E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A">>}]}
{<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>},
{<<"routing_tree">>, <<"85982C7AC502892C5179CD2A591331003BACD2D2A71723640BA7D23F45408E6E">>}]}
].
27 changes: 17 additions & 10 deletions src/controllers/nova_error_controller.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ not_found(Req) ->
lists:member(<<"text/html">>, AcceptList)} of
{true, _} ->
%% Render a json response
JsonLib = nova:get_env(json_lib, thoas),
Json = JsonLib:encode(#{message => "Resource not found"}),
Json = json:encode(#{message => <<"Resource not found">>}),
{status, 404, #{<<"content-type">> => <<"application/json">>}, Json};
{_, true} ->
%% Just assume HTML
Expand All @@ -47,8 +46,7 @@ server_error(#{crash_info := #{status_code := StatusCode} = CrashInfo} = Req) ->
true ->
case cowboy_req:header(<<"accept">>, Req) of
<<"application/json">> ->
JsonLib = nova:get_env(json_lib, thoas),
Json = JsonLib:encode(Variables),
Json = json:encode(ensure_json_safe(Variables)),
{status, StatusCode, #{<<"content-type">> => <<"application/json">>}, Json};
<<"text/html">> ->
{ok, Body} = nova_error_dtl:render(Variables),
Expand All @@ -61,19 +59,18 @@ server_error(#{crash_info := #{status_code := StatusCode} = CrashInfo} = Req) ->
end;
server_error(#{crash_info := #{class := Class, reason := Reason}} = Req) ->
Stacktrace = maps:get(stacktrace, Req, []),
Variables = #{status => "Internal Server Error",
title => "500 Internal Server Error",
message => "Something internal crashed. Please take a look!",
extra_msg => io_lib:format("Class: ~p<br /> Reason: ~p", [Class, Reason]),
Variables = #{status => <<"Internal Server Error">>,
title => <<"500 Internal Server Error">>,
message => <<"Something internal crashed. Please take a look!">>,
extra_msg => iolist_to_binary(io_lib:format("Class: ~p<br /> Reason: ~p", [Class, Reason])),
stacktrace => format_stacktrace(Stacktrace)},

case nova:get_environment() of
dev ->
%% We do show a proper error response
case cowboy_req:header(<<"accept">>, Req) of
<<"application/json">> ->
JsonLib = nova:get_env(json_lib, thoas),
Json = JsonLib:encode(Variables),
Json = json:encode(Variables),
{status, 500, #{<<"content-type">> => <<"application/json">>}, Json};
<<"text/html">> ->
{ok, Body} = nova_error_dtl:render(Variables),
Expand Down Expand Up @@ -123,6 +120,16 @@ format_arity(Arity, _) when is_function(Arity)->
format_arity(Arity, _) ->
Arity.

ensure_json_safe(Map) when is_map(Map) ->
maps:map(fun(_K, V) -> ensure_json_safe(V) end, Map);
ensure_json_safe(List) when is_list(List) ->
case io_lib:printable_unicode_list(List) of
true -> unicode:characters_to_binary(List);
false -> [ensure_json_safe(E) || E <- List]
end;
ensure_json_safe(Value) ->
Value.

-ifdef(TEST).
-compile(export_all).
-endif.
3 changes: 1 addition & 2 deletions src/nova.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
compiler,
erlydtl,
jhn_stdlib,
routing_tree,
thoas
routing_tree
]},
{env,[]},
{modules,[nova]},
Expand Down
6 changes: 2 additions & 4 deletions src/nova_basic_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@
handle_json({json, StatusCode, Headers, Req0, JSON}, Callback, _Req) ->
handle_json({json, StatusCode, Headers, JSON}, Callback, Req0);
handle_json({json, StatusCode, Headers, JSON}, _Callback, Req) ->
JsonLib = nova:get_env(json_lib, thoas),
EncodedJSON = JsonLib:encode(JSON),
EncodedJSON = json:encode(JSON),
Headers0 = maps:merge(#{<<"content-type">> => <<"application/json">>}, Headers),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = cowboy_req:set_resp_body(EncodedJSON, Req0),
Expand Down Expand Up @@ -154,11 +153,10 @@ handle_status({status, Status, ExtraHeaders, JSON, Req0}, Callback, _Req) ->
handle_status({status, Status, ExtraHeaders, JSON}, Callback, Req0);
handle_status({status, Status, ExtraHeaders, JSON}, _Callback, Req) when is_map(JSON) ->
%% We do not need to render a status page since we just return a JSON structure
JsonLib = nova:get_env(json_lib, thoas),
Headers0 = maps:merge(#{<<"content-type">> => <<"application/json">>}, ExtraHeaders),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = Req0#{resp_status_code => Status},
JSONStr = JsonLib:encode(JSON),
JSONStr = json:encode(JSON),
Req2 = cowboy_req:set_resp_body(JSONStr, Req1),
{ok, Req2};
handle_status({status, Status, ExtraHeaders, Body}, _Callback, Req) ->
Expand Down
51 changes: 24 additions & 27 deletions src/nova_jsonlogger.erl
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ merge_meta(Msg, Meta0, Config) ->
maps:merge(Msg, Meta2).

encode(Data, Config) ->
JsonLib = nova:get_env(json_lib, thoas),
Json = JsonLib:encode(Data),
Json = json:encode(Data),
case new_line(Config) of
true -> [Json, new_line_type(Config)];
false -> Json
Expand Down Expand Up @@ -162,7 +161,7 @@ meta_with(Meta, _ConfigNotPresent) ->
-include_lib("eunit/include/eunit.hrl").

-define(assertJSONEqual(Expected, Actual),
?assertEqual(thoas:decode(Expected), thoas:decode(Actual))
?assertEqual(json:decode(Expected), json:decode(iolist_to_binary(Actual)))
).

format_test() ->
Expand Down Expand Up @@ -260,20 +259,20 @@ meta_without_test() ->
meta => #{secret => xyz}
},
?assertEqual(
{ok, #{
#{
<<"answer">> => 42,
<<"level">> => <<"info">>,
<<"secret">> => <<"xyz">>
}},
thoas:decode(format(Error, #{}))
},
json:decode(iolist_to_binary(format(Error, #{})))
),
Config2 = #{meta_without => [secret]},
?assertEqual(
{ok, #{
#{
<<"answer">> => 42,
<<"level">> => <<"info">>
}},
thoas:decode(format(Error, Config2))
},
json:decode(iolist_to_binary(format(Error, Config2)))
),
ok.

Expand All @@ -284,36 +283,34 @@ meta_with_test() ->
meta => #{secret => xyz}
},
?assertEqual(
{ok, #{
#{
<<"answer">> => 42,
<<"level">> => <<"info">>,
<<"secret">> => <<"xyz">>
}},
thoas:decode(format(Error, #{}))
},
json:decode(iolist_to_binary(format(Error, #{})))
),
Config2 = #{meta_with => [level]},
?assertEqual(
{ok, #{
#{
<<"answer">> => 42,
<<"level">> => <<"info">>
}},
thoas:decode(format(Error, Config2))
},
json:decode(iolist_to_binary(format(Error, Config2)))
),
ok.

newline_test() ->
ConfigDefault = #{new_line => true},
?assertEqual(
[<<"{\"level\":\"alert\",\"text\":\"derp\"}">>, <<"\n">>],
format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigDefault)
),
ConfigCRLF = #{
new_line_type => crlf,
new_line => true
},
?assertEqual(
[<<"{\"level\":\"alert\",\"text\":\"derp\"}">>, <<"\r\n">>],
format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigCRLF)
).
[JsonDefault, NlDefault] = format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigDefault),
?assertEqual(#{<<"level">> => <<"alert">>, <<"text">> => <<"derp">>},
json:decode(iolist_to_binary(JsonDefault))),
?assertEqual(<<"\n">>, NlDefault),

ConfigCRLF = #{new_line_type => crlf, new_line => true},
[JsonCRLF, NlCRLF] = format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigCRLF),
?assertEqual(#{<<"level">> => <<"alert">>, <<"text">> => <<"derp">>},
json:decode(iolist_to_binary(JsonCRLF))),
?assertEqual(<<"\r\n">>, NlCRLF).

-endif.
12 changes: 6 additions & 6 deletions src/plugins/nova_request_plugin.erl
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json",
{stop, Req400, State};
modulate_state(Req = #{headers := #{<<"content-type">> := <<"application/json", _/binary>>}, body := Body}, [{decode_json_body, true}|Tl], State) ->
%% Decode the data
JsonLib = nova:get_env(json_lib, thoas),
case JsonLib:decode(Body) of
{ok, JSON} ->
modulate_state(Req#{json => JSON}, Tl, State);
Error ->
try json:decode(Body) of
JSON ->
modulate_state(Req#{json => JSON}, Tl, State)
catch
error:Reason ->
Req400 = cowboy_req:reply(400, Req),
logger:warning(#{status_code => 400,
msg => "Failed to decode json.",
error => Error}),
error => Reason}),
{stop, Req400, State}
end;
modulate_state(#{headers := #{<<"content-type">> := <<"application/x-www-form-urlencoded", _/binary>>}, body := Body} = Req,
Expand Down
2 changes: 1 addition & 1 deletion test/nova_error_controller_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ not_found_json_accept_test_() ->
Req1 = nova_test_helper:with_header(<<"accept">>, <<"application/json">>, Req),
{status, 404, Headers, Body} = nova_error_controller:not_found(Req1),
?assertEqual(<<"application/json">>, maps:get(<<"content-type">>, Headers)),
?assert(is_binary(Body))
?assert(is_list(Body) orelse is_binary(Body))
end}.

%% When no accept header, defaults to JSON
Expand Down
Loading
Loading