diff --git a/Makefile b/Makefile index 3c88ea5..c0833aa 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,21 @@ PROJECT = cowboy_swagger +CONFIG ?= test/test.config + DEPS = jiffy trails dep_jiffy = git https://github.com/davisp/jiffy.git 0.14.2 -dep_trails = git https://github.com/inaka/cowboy-trails.git 0.0.1 +dep_trails = git https://github.com/inaka/cowboy-trails.git 0.0.2 SHELL_DEPS = sync dep_sync = git https://github.com/inaka/sync.git 0.1.3 -TEST_DEPS = xref_runner +TEST_DEPS = xref_runner mixer shotgun dep_xref_runner = git https://github.com/inaka/xref_runner.git 0.2.2 +dep_mixer = git https://github.com/inaka/mixer.git 0.1.3 +dep_shotgun = git https://github.com/inaka/shotgun.git 0.1.12 PLT_APPS := trails cowboy DIALYZER_DIRS := ebin/ @@ -20,9 +24,10 @@ DIALYZER_OPTS := --verbose --statistics -Werror_handling \ include erlang.mk -SHELL_OPTS = -s sync - # Commont Test Config CT_DEPS = xref_runner +TEST_ERLC_OPTS += +debug_info +CT_SUITES = cowboy_swagger cowboy_swagger_handler +CT_OPTS = -cover test/cowboy_swagger.coverspec -erl_args -config ${CONFIG} -CT_OPTS = -cover test/cowboy_swagger.coverspec -erl_args +SHELL_OPTS = -s sync diff --git a/src/cowboy_swagger.app.src b/src/cowboy_swagger.app.src index 95183ac..8f1b2f7 100644 --- a/src/cowboy_swagger.app.src +++ b/src/cowboy_swagger.app.src @@ -9,7 +9,7 @@ trails ]}, {modules, []}, - {mod, {cowboy_swagger, []}}, + {mod, {cowboy_swagger_app, []}}, {registered, []} ] }. diff --git a/src/cowboy_swagger.erl b/src/cowboy_swagger.erl index 46c6b86..af5489c 100644 --- a/src/cowboy_swagger.erl +++ b/src/cowboy_swagger.erl @@ -4,6 +4,10 @@ %% API -export([to_json/1]). +%% Utilities +-export([enc_json/1, dec_json/1]). +-export([swagger_paths/1]). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Types. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -41,19 +45,35 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Returns the swagger json specification from given `trails'. --spec to_json(Trails :: [trails:trail()]) -> iolist(). +-spec to_json([trails:trail()]) -> iolist(). to_json(Trails) -> SwaggerSpec = #{paths => swagger_paths(Trails)}, - jiffy:encode(SwaggerSpec, [uescape]). + enc_json(SwaggerSpec). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Private API. +%% Utilities. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% @private +-spec enc_json(jiffy:json_value()) -> iolist(). +enc_json(Json) -> + jiffy:encode(Json, [uescape]). + +-spec dec_json(iodata()) -> jiffy:json_value(). +dec_json(Data) -> + try jiffy:decode(Data, [return_maps]) + catch + _:{error, _} -> + throw(bad_json) + end. + +-spec swagger_paths([trails:trail()]) -> map(). swagger_paths(Trails) -> swagger_paths(Trails, #{}). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Private API. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %% @private swagger_paths([], Acc) -> Acc; diff --git a/src/cowboy_swagger_app.erl b/src/cowboy_swagger_app.erl new file mode 100644 index 0000000..a520003 --- /dev/null +++ b/src/cowboy_swagger_app.erl @@ -0,0 +1,15 @@ +%%% @hidden +-module(cowboy_swagger_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +-spec start(term(), term()) -> {error, term()} | {ok, pid()}. +start(_Type, _Args) -> + cowboy_swagger_sup:start_link(). + +-spec stop(term()) -> ok. +stop(_State) -> + ok. diff --git a/src/cowboy_swagger_handler.erl b/src/cowboy_swagger_handler.erl new file mode 100644 index 0000000..d330b82 --- /dev/null +++ b/src/cowboy_swagger_handler.erl @@ -0,0 +1,58 @@ +-module(cowboy_swagger_handler). + +%% Cowboy callbacks +-export([ init/3 + , rest_init/2 + , content_types_provided/2 + ]). + +%% Handlers +-export([handle_get/2]). + +%% Trails +-behaviour(trails_handler). +-export([trails/0]). + +-type state() :: #{}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Cowboy Callbacks +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec init({atom(), atom()}, cowboy_req:req(), state()) -> + {upgrade, protocol, cowboy_rest}. +init(_Transport, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +-spec rest_init(cowboy_req:req(), state()) -> + {ok, cowboy_req:req(), term()}. +rest_init(Req, _Opts) -> + {ok, Req, #{}}. + +-spec content_types_provided(cowboy_req:req(), state()) -> + {[term()], cowboy_req:req(), state()}. +content_types_provided(Req, State) -> + {[{<<"application/json">>, handle_get}], Req, State}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Handlers +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% @hidden +handle_get(Req, State) -> + Trails = trails:all(), + {cowboy_swagger:to_json(Trails), Req, State}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Trails +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% @hidden +trails() -> + MD = + #{get => + #{description => "Retrives swagger's specification.", + produces => ["application/json"] + } + }, + [trails:trail("/api-docs/swagger.json", cowboy_swagger_handler, [], MD)]. diff --git a/src/cowboy_swagger_sup.erl b/src/cowboy_swagger_sup.erl new file mode 100644 index 0000000..77e8979 --- /dev/null +++ b/src/cowboy_swagger_sup.erl @@ -0,0 +1,14 @@ +%%% @hidden +-module(cowboy_swagger_sup). + +-behaviour(supervisor). + +-export([init/1]). +-export([start_link/0]). + +-spec start_link() -> {ok, pid()} | {error, term()}. +start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec init([]) -> {ok, {{one_for_one, 10, 60}, []}}. +init([]) -> + {ok, {{one_for_one, 10, 60}, []}}. diff --git a/test/cowboy_swagger.coverspec b/test/cowboy_swagger.coverspec index b31f171..dfc0372 100644 --- a/test/cowboy_swagger.coverspec +++ b/test/cowboy_swagger.coverspec @@ -1,5 +1,8 @@ %% Specific modules to include in cover. { incl_mods, - [cowboy_swagger] + [ + cowboy_swagger, + cowboy_swagger_handler + ] }. diff --git a/test/cowboy_swagger_SUITE.erl b/test/cowboy_swagger_SUITE.erl index 6fe111c..a862aa6 100644 --- a/test/cowboy_swagger_SUITE.erl +++ b/test/cowboy_swagger_SUITE.erl @@ -1,35 +1,29 @@ -module(cowboy_swagger_SUITE). -%% CT --export([all/0, init_per_suite/1, end_per_suite/1]). +-include_lib("mixer/include/mixer.hrl"). +-mixin([ + {cowboy_swagger_test_utils, + [ init_per_suite/1 + , end_per_suite/1 + ]} + ]). -%% Test cases +-export([all/0]). -export([to_json_test/1]). --type config() :: [{atom(), term()}]. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Common test %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec all() -> [atom()]. all() -> - Exports = ?MODULE:module_info(exports), - [F || {F, 1} <- Exports, F /= module_info]. - --spec init_per_suite(config()) -> config(). -init_per_suite(Config) -> - Config. - --spec end_per_suite(config()) -> config(). -end_per_suite(Config) -> - Config. + cowboy_swagger_test_utils:all(?MODULE). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Test Cases %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec to_json_test(config()) -> {atom(), string()}. +-spec to_json_test(cowboy_swagger_test_utils:config()) -> {atom(), string()}. to_json_test(_Config) -> Trails = test_trails(), SwaggerJson = cowboy_swagger:to_json(Trails), diff --git a/test/cowboy_swagger_handler_SUITE.erl b/test/cowboy_swagger_handler_SUITE.erl new file mode 100644 index 0000000..614e069 --- /dev/null +++ b/test/cowboy_swagger_handler_SUITE.erl @@ -0,0 +1,55 @@ +-module(cowboy_swagger_handler_SUITE). + +%% CT +-export([ all/0 + , init_per_suite/1 + , end_per_suite/1 + ]). + +%% Test cases +-export([handler_test/1]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Common test +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec all() -> [atom()]. +all() -> + cowboy_swagger_test_utils:all(?MODULE). + +-spec init_per_suite( + cowboy_swagger_test_utils:config() +) -> cowboy_swagger_test_utils:config(). +init_per_suite(Config) -> + shotgun:start(), + example:start(), + Config. + +-spec end_per_suite( + cowboy_swagger_test_utils:config() +) -> cowboy_swagger_test_utils:config(). +end_per_suite(Config) -> + shotgun:stop(), + example:stop(), + Config. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Test Cases +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec handler_test(cowboy_swagger_test_utils:config()) -> {atom(), string()}. +handler_test(_Config) -> + %% Expected result + Trails = trails:trails([example_echo_handler, + example_description_handler, + cowboy_swagger_handler]), + ExpectedPaths = cowboy_swagger:dec_json( + cowboy_swagger:enc_json(cowboy_swagger:swagger_paths(Trails))), + + %% GET swagger.json spec + ct:comment("GET /api-docs/swagger.json should return 200 OK"), + #{status_code := 200, body := Body0} = + cowboy_swagger_test_utils:api_call(get, "/api-docs/swagger.json"), + #{<<"paths">> := ExpectedPaths} = cowboy_swagger:dec_json(Body0), + + {comment, ""}. diff --git a/test/cowboy_swagger_test_utils.erl b/test/cowboy_swagger_test_utils.erl new file mode 100644 index 0000000..46749f1 --- /dev/null +++ b/test/cowboy_swagger_test_utils.erl @@ -0,0 +1,46 @@ +-module(cowboy_swagger_test_utils). + +-export([ all/1 + , init_per_suite/1 + , end_per_suite/1 + ]). +-export([ api_call/2 + , api_call/3 + , api_call/4 + ]). + +-type config() :: proplists:proplist(). +-export_type([config/0]). + +-spec all(atom()) -> [atom()]. +all(Module) -> + ExcludedFuns = [module_info, init_per_suite, end_per_suite, group, all], + Exports = apply(Module, module_info, [exports]), + [F || {F, 1} <- Exports, not lists:member(F, ExcludedFuns)]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + Config. + +-spec end_per_suite(config()) -> config(). +end_per_suite(Config) -> + Config. + +-spec api_call(atom(), string()) -> #{}. +api_call(Method, Uri) -> + api_call(Method, Uri, #{}). + +-spec api_call(atom(), string(), #{}) -> #{}. +api_call(Method, Uri, Headers) -> + api_call(Method, Uri, Headers, []). + +-spec api_call(atom(), string(), #{}, iodata()) -> #{}. +api_call(Method, Uri, Headers, Body) -> + Port = application:get_env(example, http_port, 8080), + {ok, Pid} = shotgun:open("localhost", Port), + try + {ok, Response} = shotgun:request(Pid, Method, Uri, Headers, Body, #{}), + Response + after + shotgun:close(Pid) + end. diff --git a/test/example.app b/test/example.app new file mode 100644 index 0000000..b6ed58f --- /dev/null +++ b/test/example.app @@ -0,0 +1,16 @@ +{application, example, + [ + {description, "Cowboy Trails Basic Example."}, + {vsn, "0.1"}, + {applications, + [kernel, + stdlib, + cowboy, + trails, + cowboy_swagger + ]}, + {modules, []}, + {mod, {example, []}}, + {start_phases, [{start_trails_http, []}]} + ] +}. diff --git a/test/example.erl b/test/example.erl new file mode 100644 index 0000000..1f87721 --- /dev/null +++ b/test/example.erl @@ -0,0 +1,49 @@ +-module(example). + +-export([start/0]). +-export([start/2]). +-export([stop/0]). +-export([stop/1]). +-export([start_phase/3]). + +%% application +%% @doc Starts the application +start() -> + application:ensure_all_started(example). + +%% @doc Stops the application +stop() -> + application:stop(example). + +%% behaviour +%% @private +start(_StartType, _StartArgs) -> + example_sup:start_link(). + +%% @private +stop(_State) -> + ok = cowboy:stop_listener(example_http). + +% start_listeners() -> +-spec start_phase(atom(), application:start_type(), []) -> ok | {error, term()}. +start_phase(start_trails_http, _StartType, []) -> + {ok, Port} = application:get_env(example, http_port), + {ok, ListenerCount} = application:get_env(example, http_listener_count), + Trails = trails:trails([example_echo_handler, + example_description_handler, + cowboy_swagger_handler]), + trails:store(Trails), + Dispatch = trails:single_host_compile(Trails), + RanchOptions = [{port, Port}], + CowboyOptions = + [ + {env, + [ + {dispatch, Dispatch} + ]}, + {compress, true}, + {timeout, 12000} + ], + {ok, _} = + cowboy:start_http(example_http, ListenerCount, RanchOptions, CowboyOptions), + ok. diff --git a/test/example_default.erl b/test/example_default.erl new file mode 100644 index 0000000..d176c53 --- /dev/null +++ b/test/example_default.erl @@ -0,0 +1,28 @@ +-module(example_default). + +-export([ init/3 + , rest_init/2 + , content_types_accepted/2 + , content_types_provided/2 + , forbidden/2 + , resource_exists/2 + ]). + +%% cowboy +init(_Transport, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +rest_init(Req, _Opts) -> + {ok, Req, #{}}. + +content_types_accepted(Req, State) -> + {[{<<"text/plain">>, handle_put}], Req, State}. + +content_types_provided(Req, State) -> + {[{<<"text/plain">>, handle_get}], Req, State}. + +forbidden(Req, State) -> + {false, Req, State}. + +resource_exists(Req, State) -> + {true, Req, State}. diff --git a/test/example_description_handler.erl b/test/example_description_handler.erl new file mode 100644 index 0000000..45a6a7b --- /dev/null +++ b/test/example_description_handler.erl @@ -0,0 +1,39 @@ +-module(example_description_handler). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([ + {example_default, + [ + init/3, + rest_init/2, + content_types_accepted/2, + content_types_provided/2, + resource_exists/2 + ]} + ]). + +-export([ allowed_methods/2 + , handle_get/2 + ]). + +%trails +-behaviour(trails_handler). +-export([trails/0]). + +trails() -> + Metadata = + #{get => + #{description => "Retrives trails's server description", + produces => ["text/plain"] + } + }, + [trails:trail("/description", example_description_handler, [], Metadata)]. + +%% cowboy +allowed_methods(Req, State) -> + {[<<"GET">>], Req, State}. + +%% internal +handle_get(Req, State) -> + Body = trails:all(), + {io_lib:format("~p~n", [Body]), Req, State}. diff --git a/test/example_echo_handler.erl b/test/example_echo_handler.erl new file mode 100644 index 0000000..1babf81 --- /dev/null +++ b/test/example_echo_handler.erl @@ -0,0 +1,59 @@ +-module(example_echo_handler). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([ + {example_default, + [ + init/3, + rest_init/2, + content_types_accepted/2, + content_types_provided/2, + resource_exists/2 + ]} + ]). + +-export([ allowed_methods/2 + , handle_put/2 + , handle_get/2 + ]). + +%trails +-behaviour(trails_handler). +-export([trails/0]). + +trails() -> + Metadata = + #{get => + #{description => "Gets echo var from the server", + produces => ["text/plain"] + }, + put => + #{description => "Sets echo var in the server", + produces => ["text/plain"], + parameters => [ + #{name => <<"echo">>, + description => <<"Echo message">>, + in => <<"path">>, + required => false, + type => <<"string">>} + ] + } + }, + [trails:trail("/message/[:echo]", example_echo_handler, [], Metadata)]. + +%% cowboy +allowed_methods(Req, State) -> + {[<<"GET">>, <<"PUT">>, <<"HEAD">>], Req, State}. + +%% internal +handle_get(Req, State) -> + Echo = application:get_env(example, echo, ""), + Body = [<<"You Get an echo! ">> , Echo], + {Body, Req, State}. + +handle_put(Req, State) -> + {Echo, Req1} = cowboy_req:binding(echo, Req, ""), + application:set_env(example, echo, Echo), + Body = [<<"You put an echo! ">> , Echo], + Req2 = cowboy_req:set_resp_body(Body, Req1), + {true, Req2, State}. diff --git a/test/example_sup.erl b/test/example_sup.erl new file mode 100644 index 0000000..a910efe --- /dev/null +++ b/test/example_sup.erl @@ -0,0 +1,14 @@ +-module(example_sup). + +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +%% admin api +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, {}). + +%% behaviour callbacks +init({}) -> + {ok, {{one_for_one, 5, 10}, []} }. diff --git a/test/test.config b/test/test.config new file mode 100644 index 0000000..4ab06f7 --- /dev/null +++ b/test/test.config @@ -0,0 +1,8 @@ +[ + {example, + [ + {http_port, 8080}, + {http_listener_count, 10} + ] + } +].