├── run-tests.sh ├── .travis.yml ├── .gitignore ├── examples └── demo_pets │ ├── .gitignore │ ├── rebar.config │ ├── src │ ├── demo_pets.app.src │ ├── socket │ │ └── demo_pets_socket_pets_id.erl │ ├── demo_pets_sup.erl │ ├── rest │ │ └── demo_pets_rest_pets_id.erl │ └── demo_pets_app.erl │ ├── LICENSE │ ├── priv │ └── swagger.yaml │ └── README.md ├── src ├── swagger_routerl.app.src ├── rest │ ├── swagger_routerl_cowboy_v1_file_handler.erl │ └── swagger_routerl_cowboy_rest.erl ├── swagger_routerl.erl ├── websocket │ ├── swagger_routerl_cowboy_ws.erl │ └── swagger_routerl_cowboy_v1_ws_json_dispatcher.erl ├── tcp │ ├── swagger_routerl_tcp.erl │ └── swagger_routerl_tcp_ranch_json_handler.erl ├── swagger_routerl_utils.erl ├── swagger_routerl_router.erl └── emqtt │ └── swagger_routerl_emqtt.erl ├── README.md ├── rebar.config ├── test ├── swagger_routerl_tests.erl ├── swagger_routerl_cowboy_ws_dispatcher_tests.erl ├── swagger_routerl_router_tests.erl ├── swagger_routerl_utils_tests.erl ├── swagger_routerl_cowboy_ws_tests.erl ├── swagger_routerl_cowboy_rest_tests.erl └── swagger_routerl_emqtt_tests.erl └── LICENSE /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=`dirname $0` 4 | 5 | # start tests 6 | cd $DIR 7 | ./utils/rebar3 do compile, eunit, cover -v 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 18.0 4 | - 18.1 5 | - 18.2 6 | install: echo "fix travis for rebar3 compatibility" 7 | script: "./utils/rebar3 eunit" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | rebar.lock 17 | -------------------------------------------------------------------------------- /examples/demo_pets/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | rebar3.crashdump 18 | rebar.lock 19 | -------------------------------------------------------------------------------- /examples/demo_pets/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, [ 3 | {cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "1.1.2"}}}, 4 | {jsx, {git, "https://github.com/talentdeficit/jsx.git", {branch, master}}}, 5 | {swagger_routerl, 6 | {git, "https://github.com/hachreak/swagger_routerl.git", {branch, master}}} 7 | ]}. 8 | -------------------------------------------------------------------------------- /src/swagger_routerl.app.src: -------------------------------------------------------------------------------- 1 | {application, swagger_routerl, 2 | [{description, 3 | "Routing library that generate the routing table from swagger.yaml."}, 4 | {vsn, "1.0.0"}, 5 | {registered, []}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | yamerl 10 | ]}, 11 | {env,[]}, 12 | {modules, []}, 13 | 14 | {maintainers, []}, 15 | {licenses, []}, 16 | {links, []} 17 | ]}. 18 | -------------------------------------------------------------------------------- /examples/demo_pets/src/demo_pets.app.src: -------------------------------------------------------------------------------- 1 | {application, demo_pets, 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, { demo_pets_app, []}}, 6 | {applications, 7 | [ 8 | cowboy, 9 | jsx, 10 | kernel, 11 | stdlib, 12 | swagger_routerl 13 | ]}, 14 | {env,[]}, 15 | {modules, []}, 16 | 17 | {maintainers, []}, 18 | {licenses, []}, 19 | {links, []} 20 | ]}. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | swagger_routerl 2 | =============== 3 | 4 | Routing library that generate the routing table from `swagger.yaml`. 5 | 6 | Implemented plugins: 7 | 8 | - Cowboy REST routing table. 9 | - Cowboy Websocket routing table: dispatch events on top of a 10 | websocket endpoint. 11 | - Raw tcp: dispatch events on top of a socket. 12 | 13 | How configure plugins 14 | --------------------- 15 | 16 | See example `demo_pets`. 17 | 18 | Build 19 | ----- 20 | 21 | $ rebar3 compile 22 | 23 | Test 24 | ---- 25 | 26 | $ ./run-tests.sh 27 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {plugins, []}. 3 | {deps, [ 4 | {ranch, {git, "https://github.com/ninenines/ranch.git", {tag, "1.3.2"}}}, 5 | {jsx, 6 | {git, "https://github.com/talentdeficit/jsx.git", {branch, master}}}, 7 | {yamerl, {git, "https://github.com/yakaz/yamerl.git", {tag, "v0.7.0"}}} 8 | ]}. 9 | {profiles, [ 10 | {prod, [ 11 | {relx, [ 12 | {dev_mode, false}, 13 | {include_erts, true} 14 | ]} 15 | ]}, 16 | {test, [ 17 | {deps, [ 18 | {meck, 19 | {git, "https://github.com/eproxus/meck.git", {tag, "0.8.3"}}}, 20 | {jsx, 21 | {git, "https://github.com/talentdeficit/jsx.git", {branch, master}}}, 22 | {cowboy, 23 | {git, "https://github.com/ninenines/cowboy.git", 24 | {ref, "dbb636034f20736e16eb9d6c809217c9525b6cbd"}}}, 25 | {emqttd, {git, "https://github.com/emqtt/emqttd.git", {tag, "1.0.2"}}} 26 | ]} 27 | ]} 28 | ]}. 29 | {vim_erlang_compiler, [ 30 | {profile, "test"} 31 | ]}. 32 | {cover_enabled, true}. 33 | -------------------------------------------------------------------------------- /examples/demo_pets/src/socket/demo_pets_socket_pets_id.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc demo_pets public socket (tcp and websocket) API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(demo_pets_socket_pets_id). 7 | 8 | -author('Leonardo Rossi '). 9 | 10 | -export([ 11 | delete/4, 12 | get/4, 13 | put/4 14 | ]). 15 | 16 | %%%_ * API ------------------------------------------------------------- 17 | 18 | put(#{<<"pet">> := Pet}, Req, [Id], AppCtx) -> 19 | NewAppCtx = AppCtx#{Id => Pet}, 20 | {reply, #{}, Req, NewAppCtx}; 21 | put(_, Req, [_Id], AppCtx) -> 22 | {error, body_notfound, Req, AppCtx}. 23 | 24 | get(_Event, Req, [Id], AppCtx) -> 25 | try 26 | Value = maps:get(Id, AppCtx), 27 | {reply, Value, Req, AppCtx} 28 | catch error:{badkey, _} -> 29 | {error, pet_notfound, Req, AppCtx} 30 | end. 31 | 32 | delete(_Event, Req, [Id], AppCtx) -> 33 | NewAppCtx = maps:remove(Id, AppCtx), 34 | {reply, #{}, Req, NewAppCtx}. 35 | -------------------------------------------------------------------------------- /examples/demo_pets/src/demo_pets_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc demo_pets top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(demo_pets_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | %%==================================================================== 19 | %% API functions 20 | %%==================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 24 | 25 | %%==================================================================== 26 | %% Supervisor callbacks 27 | %%==================================================================== 28 | 29 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 30 | init([]) -> 31 | {ok, { {one_for_all, 0, 1}, []} }. 32 | 33 | %%==================================================================== 34 | %% Internal functions 35 | %%==================================================================== 36 | -------------------------------------------------------------------------------- /examples/demo_pets/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Leonardo Rossi . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/rest/swagger_routerl_cowboy_v1_file_handler.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Swagger file. 19 | %%% @end 20 | 21 | -module(swagger_routerl_cowboy_v1_file_handler). 22 | 23 | -author('Leonardo Rossi '). 24 | 25 | -export([to_text/2]). 26 | 27 | -export([ 28 | init/3, 29 | content_types_provided/2, 30 | rest_init/2 31 | ]). 32 | 33 | %%%_ * API ------------------------------------------------------------- 34 | 35 | init(_Transport, _Req, _AppCtx) -> 36 | {upgrade, protocol, cowboy_rest}. 37 | 38 | rest_init(Req, AppCtx) -> 39 | {ok, Req, AppCtx}. 40 | 41 | content_types_provided(Req, AppCtx) -> 42 | {[{<<"text/html">>, to_text}], Req, AppCtx}. 43 | 44 | to_text(ReqData, #{swagger_file := SwaggerFile}=Context) -> 45 | {SwaggerFile, ReqData, Context}. 46 | 47 | -------------------------------------------------------------------------------- /examples/demo_pets/priv/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: '1.0.0' 4 | title: Swagger Petstore 5 | host: petstore.swagger.io 6 | basePath: /api 7 | schemes: 8 | - http 9 | consumes: 10 | - application/json 11 | produces: 12 | - application/json 13 | paths: 14 | /pets/{id}: 15 | put: 16 | operationId: adPetById 17 | parameters: 18 | - name: id 19 | in: path 20 | description: ID of pet to put 21 | required: true 22 | type: string 23 | - name: pet 24 | in: body 25 | description: Pet to add to the store 26 | required: true 27 | schema: 28 | $ref: '#/definitions/pet' 29 | responses: 30 | '201': 31 | description: pet response 32 | get: 33 | operationId: findPetById 34 | parameters: 35 | - name: id 36 | in: path 37 | description: ID of pet to fetch 38 | required: true 39 | type: string 40 | responses: 41 | '200': 42 | description: pet response 43 | schema: 44 | $ref: '#/definitions/pet' 45 | delete: 46 | description: deletes a single pet based on the ID supplied 47 | operationId: deletePet 48 | parameters: 49 | - name: id 50 | in: path 51 | description: ID of pet to delete 52 | required: true 53 | type: string 54 | responses: 55 | '204': 56 | description: pet deleted 57 | definitions: 58 | pet: 59 | type: object 60 | required: 61 | - name 62 | properties: 63 | name: 64 | type: string 65 | description: 66 | type: string 67 | -------------------------------------------------------------------------------- /test/swagger_routerl_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test utils. 19 | %%% @end 20 | -module(swagger_routerl_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | all_test_() -> 27 | {foreach, 28 | fun start/0, 29 | fun stop/1, 30 | [ 31 | fun get_version/1 32 | ] 33 | }. 34 | 35 | start() -> ok. 36 | 37 | stop(_) -> ok. 38 | 39 | get_version(_) -> 40 | fun() -> 41 | File = [ 42 | {"info", [ 43 | {"version", "1.0.0"} 44 | ]} 45 | ], 46 | ?assertEqual( 47 | "1.0.0", 48 | swagger_routerl:get_version(File) 49 | ), 50 | File2 = [ 51 | {"info", [ 52 | ]} 53 | ], 54 | ?assertEqual( 55 | "0.1.0", 56 | swagger_routerl:get_version(File2) 57 | ) 58 | end. 59 | -------------------------------------------------------------------------------- /examples/demo_pets/src/rest/demo_pets_rest_pets_id.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc demo_pets REST handler 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(demo_pets_rest_pets_id). 7 | 8 | -author('Leonardo Rossi '). 9 | 10 | -export([ 11 | from_json/2, 12 | to_json/2 13 | ]). 14 | 15 | -export([ 16 | allowed_methods/2, 17 | content_types_accepted/2, 18 | content_types_provided/2, 19 | delete_resource/2, 20 | init/3, 21 | resource_exists/2, 22 | rest_init/2 23 | ]). 24 | 25 | %%%_ * API ------------------------------------------------------------- 26 | 27 | init(_Transport, _Req, _AppCtx) -> 28 | {upgrade, protocol, cowboy_rest}. 29 | 30 | allowed_methods(Req, AppCtx) -> 31 | {[<<"PUT">>, <<"GET">>, <<"DELETE">>], Req, AppCtx}. 32 | 33 | rest_init(Req, AppCtx) -> 34 | {Id, Req2} = cowboy_req:binding(id, Req), 35 | {ok, Req2, AppCtx#{id => Id}}. 36 | 37 | content_types_accepted(Req, AppCtx) -> 38 | {[{<<"application/json">>, from_json}], Req, AppCtx}. 39 | 40 | content_types_provided(Req, AppCtx) -> 41 | {[{<<"application/json">>, to_json}], Req, AppCtx}. 42 | 43 | resource_exists(Req, #{id := Id, db := Db}=AppCtx) -> 44 | case ets:lookup(Db, Id) of 45 | [] -> {false, Req, AppCtx}; 46 | _ -> {true, Req, AppCtx} 47 | end. 48 | 49 | delete_resource(Req, #{id := Id, db := Db}=AppCtx) -> 50 | ets:delete(Db, Id), 51 | {true, Req, AppCtx}. 52 | 53 | from_json(Req, #{id := Id, db := Db}=AppCtx) -> 54 | {ok, Body, Req2} = cowboy_req:body(Req), 55 | Input = jsx:decode(Body), 56 | ets:insert(Db, {Id, Input}), 57 | {true, Req2, AppCtx}. 58 | 59 | to_json(Req, #{id := Id, db := Db}=AppCtx) -> 60 | Json = jsx:encode(ets:lookup(Db, Id)), 61 | {Json, Req, AppCtx}. 62 | -------------------------------------------------------------------------------- /src/swagger_routerl.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Load the swagger file. 19 | %%% @end 20 | -module(swagger_routerl). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | %% API exports 25 | -export([load/1, get_version/1]). 26 | 27 | -export_type([appctx/0, filename/0, yaml/0]). 28 | 29 | -type appctx() :: ok. 30 | -type filename() :: binary() | string(). 31 | -type yaml() :: list({string(), term()}). 32 | 33 | %%==================================================================== 34 | %% API functions 35 | %%==================================================================== 36 | 37 | -spec load(filename()) -> yaml(). 38 | load(Filename) -> 39 | [File] = yamerl:decode_file(Filename), 40 | File. 41 | 42 | -spec get_version(yaml()) -> string(). 43 | get_version(Yaml) -> 44 | Info = proplists:get_value("info", Yaml, []), 45 | proplists:get_value("version", Info, "0.1.0"). 46 | 47 | %%==================================================================== 48 | %% Internal functions 49 | %%==================================================================== 50 | -------------------------------------------------------------------------------- /src/websocket/swagger_routerl_cowboy_ws.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016, 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a Cowboy WebSocket routing table. 19 | %%% @end 20 | -module(swagger_routerl_cowboy_ws). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | compile/4, 26 | dispatch/3 27 | ]). 28 | 29 | -type appctx() :: swagger_routerl_router:appctx(). 30 | -type req() :: cowboy_req:req(). 31 | -type event() :: swagger_routerl_router:event(). 32 | 33 | %%% API functions 34 | 35 | compile(Prefix, Yaml, RouteCtx, Ctx) -> 36 | Endpoint = maps:get(endpoint, Ctx, "/websocket"), 37 | Handler = maps:get( 38 | handler, Ctx, swagger_routerl_cowboy_v1_ws_json_dispatcher), 39 | AppCtx = swagger_routerl_router:compile(Prefix, Yaml, RouteCtx), 40 | [{Endpoint, Handler, AppCtx}]. 41 | 42 | -spec dispatch(event(), req(), appctx()) -> 43 | {reply, term(), req(), appctx()} 44 | | {ok, req(), appctx()} 45 | | {error, atom(), req(), appctx()}. 46 | dispatch(Event, Req, AppContext) -> 47 | swagger_routerl_router:dispatch(Event, Req, AppContext). 48 | -------------------------------------------------------------------------------- /src/tcp/swagger_routerl_tcp.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a Cowboy WebSocket routing table. 19 | %%% @end 20 | -module(swagger_routerl_tcp). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | compile/4, 26 | dispatch/3 27 | ]). 28 | 29 | -type appctx() :: swagger_routerl_router:appctx(). 30 | -type req() :: swagger_routerl_router:req(). 31 | -type event() :: swagger_routerl_router:event(). 32 | 33 | %%% API functions 34 | 35 | compile(Prefix, Yaml, RouteCtx, Ctx) -> 36 | Name = maps:get(name, Ctx, "tcp_driver"), 37 | % Protocol = maps:get(protocol, Ctx, "tcp"), 38 | Port = maps:get(port, Ctx, 54355), 39 | Handler = maps:get(handler, Ctx, swagger_routerl_tcp_ranch_json_handler), 40 | AppCtx = swagger_routerl_router:compile(Prefix, Yaml, RouteCtx), 41 | {Name, Port, Handler, AppCtx}. 42 | 43 | -spec dispatch(event(), req(), appctx()) -> 44 | {reply, term(), req(), appctx()} 45 | | {ok, req(), appctx()} 46 | | {error, atom(), req(), appctx()}. 47 | dispatch(Event, Req, AppContext) -> 48 | swagger_routerl_router:dispatch(Event, Req, AppContext). 49 | -------------------------------------------------------------------------------- /test/swagger_routerl_cowboy_ws_dispatcher_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test cowboy rest 19 | %%% @end 20 | -module(swagger_routerl_cowboy_ws_dispatcher_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | ping_test() -> 27 | AppCtx = swagger_routerl_router:build_context(qwerty, bar), 28 | ?assertEqual( 29 | {ok, fuu, AppCtx}, 30 | swagger_routerl_cowboy_ws_dispatcher:websocket_handle( 31 | {ping, hello}, fuu, AppCtx)). 32 | 33 | text_test() -> 34 | Event = #{<<"test">> => <<"tset">>}, 35 | Json = jsx:encode(Event), 36 | AppCtx = swagger_routerl_router:build_context(qwerty, bar), 37 | meck:new(swagger_routerl_cowboy_ws, 38 | [no_link, passthrough, no_history, non_strict]), 39 | meck:expect(swagger_routerl_cowboy_ws, execute, 3, 40 | fun(MyEvent, fuu, MyAppCtx) -> 41 | ?assertEqual(AppCtx, MyAppCtx), 42 | ?assertEqual(Event, MyEvent) 43 | end 44 | ), 45 | try 46 | swagger_routerl_cowboy_ws_dispatcher:websocket_handle( 47 | {text, Json}, fuu, AppCtx) 48 | after 49 | meck:validate(swagger_routerl_cowboy_ws), 50 | meck:unload(swagger_routerl_cowboy_ws) 51 | end. 52 | -------------------------------------------------------------------------------- /examples/demo_pets/README.md: -------------------------------------------------------------------------------- 1 | demo_pets 2 | ========= 3 | 4 | Example application that implement REST, Websocket and TCP API with the help of `swagger_routerl`. 5 | 6 | ### Swagger file endpoint 7 | 8 | At the address `http://127.0.0.1:8080/docs/swagger.yaml` we can find the file. 9 | 10 | ### Connect through TCP from python 11 | 12 | ```python 13 | import socket 14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | s.connect(('0.0.0.0', 12345)) 16 | 17 | s.send('{"path": "/pets/pippo", "method": "put", "pet": {"name": "pippo", "description": "My pet!"}}') 18 | print(s.recv(255)) 19 | # print: {"context":{},"result":"ok"} 20 | 21 | s.send('{"path": "/pets/pippo", "method": "get"}') 22 | print(s.recv(255)) 23 | # print: {"context":{"description":"My pet!","name":"pippo"},"result":"ok"} 24 | 25 | s.send('{"path": "/pets/pippo", "method": "delete"}') 26 | print(s.recv(255)) 27 | # print: {"context":{},"result":"ok"} 28 | ``` 29 | 30 | ### Connect through Websocket 31 | 32 | Open a connection to: `ws://0.0.0.0:8080/websocket` 33 | 34 | Send: `{"path": "/pets/pippo", "method": "put", "pet": {"name": "pippo", "description": "My pet!"}} 35 | Receive: `{"context":{},"result":"ok"}` 36 | 37 | Send: `{"path": "/pets/pippo", "method": "get"}` 38 | Receive: `{"context":{"description":"My pet!","name":"pippo"},"result":"ok"}` 39 | 40 | Send: `{"path": "/pets/pippo", "method": "delete"}` 41 | Receive: `{"context":{},"result":"ok"}` 42 | 43 | ### Connect through REST 44 | 45 | `http PUT http://0.0.0.0:8080/api/pets/pippo name=pippo description="My pet!"` 46 | 47 | ``` 48 | HTTP/1.1 204 No Content 49 | content-length: 0 50 | content-type: application/json 51 | date: Sun, 04 Jun 2017 14:24:00 GMT 52 | server: Cowboy 53 | ``` 54 | 55 | `http GET http://0.0.0.0:8080/api/pets/pippo` 56 | 57 | ``` 58 | HTTP/1.1 200 OK 59 | content-length: 50 60 | content-type: application/json 61 | date: Sun, 04 Jun 2017 14:24:03 GMT 62 | server: Cowboy 63 | 64 | { 65 | "pippo": { 66 | "description": "My pet!", 67 | "name": "pippo" 68 | } 69 | } 70 | ``` 71 | 72 | `http DELETE http://0.0.0.0:8080/api/pets/pippo` 73 | 74 | ``` 75 | HTTP/1.1 204 No Content 76 | content-length: 0 77 | content-type: application/json 78 | date: Sun, 04 Jun 2017 14:27:13 GMT 79 | server: Cowboy 80 | ``` 81 | 82 | Build 83 | ----- 84 | 85 | $ rebar3 compile 86 | 87 | Run 88 | --- 89 | 90 | $ rebar3 shell --apps demo_pets 91 | -------------------------------------------------------------------------------- /src/websocket/swagger_routerl_cowboy_v1_ws_json_dispatcher.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Websocket dispatcher 19 | %%% @end 20 | -module(swagger_routerl_cowboy_v1_ws_json_dispatcher). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | init/3, 26 | websocket_handle/3, 27 | websocket_info/3, 28 | websocket_init/3, 29 | websocket_terminate/3 30 | ]). 31 | 32 | 33 | %%% API functions 34 | 35 | init({tcp, http}, _Req, _AppCtx) -> 36 | {upgrade, protocol, cowboy_websocket}. 37 | 38 | websocket_init(_TransportName, Req, AppCtx) -> {ok, Req, AppCtx}. 39 | 40 | websocket_handle({ping, _Ping}, Req, AppCtx) -> 41 | {ok, Req, AppCtx}; 42 | websocket_handle({text, EventTxt}, Req, AppCtx) -> 43 | % decode event from a websocket client 44 | Event = jsx:decode(EventTxt, [return_maps]), 45 | % dispatch the request 46 | output(swagger_routerl_cowboy_ws:dispatch(Event, Req, AppCtx), AppCtx). 47 | 48 | websocket_info(_Info, Req, AppCtx) -> {ok, Req, AppCtx}. 49 | 50 | websocket_terminate(_Reason, _Req, _State) -> ok. 51 | 52 | %% Private functions 53 | 54 | output({reply, Msg, Req, RouteCtx}, AppCtx) -> 55 | MsgEncoded = jsx:encode(#{result => ok, context => Msg}), 56 | {reply, {text, MsgEncoded}, Req, AppCtx#{routectx => RouteCtx}}; 57 | output({error, Error, Req, RouteCtx}, AppCtx) -> 58 | MsgEncoded = jsx:encode(#{result => error, context => Error}), 59 | {reply, {text, MsgEncoded}, Req, AppCtx#{routectx => RouteCtx}}; 60 | output({ok, Req, RouteCtx}, AppCtx) -> 61 | {ok, Req, AppCtx#{routectx => RouteCtx}}. 62 | -------------------------------------------------------------------------------- /src/tcp/swagger_routerl_tcp_ranch_json_handler.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a Cowboy WebSocket routing table. 19 | %%% @end 20 | -module(swagger_routerl_tcp_ranch_json_handler). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -behaviour(ranch_protocol). 25 | 26 | -export([ 27 | init/4, 28 | start_link/4 29 | ]). 30 | 31 | 32 | %%% API functions 33 | 34 | start_link(Ref, Socket, Transport, AppCtx) -> 35 | Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, AppCtx]), 36 | {ok, Pid}. 37 | 38 | init(Ref, Socket, Transport, AppCtx) -> 39 | ok = ranch:accept_ack(Ref), 40 | loop(Socket, Transport, AppCtx). 41 | 42 | loop(Socket, Transport, AppCtx) -> 43 | case Transport:recv(Socket, 0, infinity) of 44 | {ok, EventTxt} -> 45 | Event = jsx:decode(EventTxt, [return_maps]), 46 | % dispatch the request 47 | AppCtxNew = case output( 48 | swagger_routerl_tcp:dispatch(Event, none, AppCtx), AppCtx) of 49 | {reply, Msg, AppCtx2} -> 50 | Transport:send(Socket, Msg), 51 | AppCtx2; 52 | _ -> AppCtx 53 | end, 54 | loop(Socket, Transport, AppCtxNew); 55 | _ -> ok = Transport:close(Socket) 56 | end. 57 | 58 | %% Private functions 59 | 60 | output({reply, Msg, _Req, RouteCtx}, AppCtx) -> 61 | MsgEncoded = jsx:encode(#{result => ok, context => Msg}), 62 | {reply, MsgEncoded, AppCtx#{routectx => RouteCtx}}; 63 | output({error, Error, _Req, RouteCtx}, AppCtx) -> 64 | MsgEncoded = jsx:encode(#{result => error, context => Error}), 65 | {reply, MsgEncoded, AppCtx#{routectx => RouteCtx}}; 66 | output({ok, _Req, RouteCtx}, AppCtx) -> 67 | {noreply, AppCtx#{routectx => RouteCtx}}. 68 | -------------------------------------------------------------------------------- /examples/demo_pets/src/demo_pets_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc demo_pets public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(demo_pets_app). 7 | 8 | -author('Leonardo Rossi '). 9 | 10 | -behaviour(application). 11 | 12 | %% Application callbacks 13 | -export([start/2, stop/1]). 14 | 15 | -type appctx() :: any(). 16 | 17 | %%==================================================================== 18 | %% API 19 | %%==================================================================== 20 | 21 | start(_StartType, _StartArgs) -> 22 | Filename = swagger_filename(), 23 | Yaml = swagger_routerl:load(Filename), 24 | {ok, SwaggerFileRaw} = file:read_file(Filename), 25 | 26 | http(Yaml, SwaggerFileRaw), 27 | tcp(Yaml), 28 | 29 | demo_pets_sup:start_link(). 30 | 31 | %%-------------------------------------------------------------------- 32 | stop(_State) -> 33 | ok. 34 | 35 | %%==================================================================== 36 | %% Internal functions 37 | %%==================================================================== 38 | 39 | tcp(Yaml) -> 40 | {Name, Port, Handler, AppCtx} = swagger_routerl_tcp:compile( 41 | "demo_pets_socket_", Yaml, #{}, #{ 42 | name => "demo_pets_service", port => 12345 43 | }), 44 | 45 | {ok, _} = ranch:start_listener( 46 | Name, 100, ranch_tcp, [{port, Port}], Handler, AppCtx). 47 | 48 | http(Yaml, SwaggerFileRaw) -> 49 | Dispatch = cowboy_router:compile([ 50 | {'_', routes(#{protocol => http}, Yaml, SwaggerFileRaw)} 51 | ]), 52 | {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [ 53 | {env, [{dispatch, Dispatch}]} 54 | ]). 55 | 56 | -spec routes(appctx(), list(), binary()) -> cowboy_router:routes(). 57 | routes(#{protocol := Protocol}, Yaml, SwaggerFileRaw) -> 58 | FileEndpoint = swagger_routerl_cowboy_rest:file_endpoint( 59 | SwaggerFileRaw, #{endpoint => endpoint(Yaml), 60 | protocol => swagger_routerl_utils:to_binary(Protocol)}), 61 | Db = ets:new(pets, [set, named_table, public]), 62 | RestEndpoints = swagger_routerl_cowboy_rest:compile( 63 | "demo_pets_rest_", Yaml, #{db => Db}), 64 | WSEndpoint = swagger_routerl_cowboy_ws:compile( 65 | "demo_pets_socket_", Yaml, #{}, #{ 66 | handler => swagger_routerl_cowboy_v1_ws_json_dispatcher 67 | }), 68 | 69 | FileEndpoint ++ RestEndpoints ++ WSEndpoint. 70 | 71 | swagger_filename() -> 72 | PrivDir = code:priv_dir(demo_pets), 73 | Filename = "swagger.yaml", 74 | filename:join([PrivDir, Filename]). 75 | 76 | endpoint(Yaml) -> 77 | Version = swagger_routerl:get_version(Yaml), 78 | "/" ++ Version ++ "/docs/swagger.yaml". 79 | -------------------------------------------------------------------------------- /src/rest/swagger_routerl_cowboy_rest.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a Cowboy REST routing table. 19 | %%% @end 20 | -module(swagger_routerl_cowboy_rest). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | compile/3, 26 | file_endpoint/2 27 | ]). 28 | 29 | -export_type([routectx/0]). 30 | 31 | -type routectx() :: term(). 32 | -type routes() :: cowboy_router:routes(). 33 | -type swagger_endpoint_ctx() :: #{protocol := _, endpoint := _, handler := _}. 34 | -type yaml() :: swagger_routerl:yaml(). 35 | 36 | -ifdef(TEST). 37 | -compile(export_all). 38 | -endif. 39 | 40 | %%% API functions 41 | 42 | -spec compile(string(), yaml(), routectx()) -> routes(). 43 | compile(Prefix, Yaml, RouteContext) -> 44 | BasePath = proplists:get_value("basePath", Yaml, ""), 45 | Paths = proplists:get_value("paths", Yaml), 46 | lists:map( 47 | fun({SwaggerPath, _Config}) -> 48 | {get_route(BasePath, SwaggerPath), 49 | get_filename(Prefix, SwaggerPath), 50 | RouteContext} 51 | end, Paths). 52 | 53 | -spec file_endpoint(binary(), swagger_endpoint_ctx()) -> routes(). 54 | file_endpoint(SwaggerFileRaw, Ctx) -> 55 | Protocol = maps:get(protocol, Ctx, <<"http">>), 56 | Endpoint = maps:get(endpoint, Ctx, "/docs/swagger.yaml"), 57 | Handler = maps:get(handler, Ctx, swagger_routerl_cowboy_v1_file_handler), 58 | SwaggerFile = set_scheme(SwaggerFileRaw, Protocol), 59 | [{Endpoint, Handler, #{swagger_file => SwaggerFile}}]. 60 | 61 | %%% Private functions 62 | 63 | -spec get_filename(string(), string()) -> atom(). 64 | get_filename(Prefix, PathConfig) -> 65 | swagger_routerl_utils:swaggerpath2module(Prefix, PathConfig). 66 | 67 | -spec get_route(string(), string()) -> string(). 68 | get_route(BasePath, SwaggerPath) -> 69 | Opts = [{return,list}, global], 70 | Path1 = re:replace(SwaggerPath, "{", "[:", Opts), 71 | CowboyPath = re:replace(Path1, "}", "]", Opts), 72 | BasePath ++ CowboyPath. 73 | 74 | -spec set_scheme(binary(), binary()) -> binary(). 75 | set_scheme(SwaggerFileRaw, Protocol) -> 76 | swagger_routerl_utils:render( 77 | SwaggerFileRaw, [{<<"scheme_protocol">>, Protocol}]). 78 | -------------------------------------------------------------------------------- /test/swagger_routerl_router_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test router 19 | %%% @end 20 | -module(swagger_routerl_router_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | 27 | context_test() -> 28 | AppCtx = swagger_routerl_router:build_context(qwerty, bar), 29 | ?assertEqual( 30 | bar, swagger_routerl_router:get_routectx(AppCtx)). 31 | 32 | build_regex_test() -> 33 | MP = swagger_routerl_router:build_regex( 34 | "/users/{userid}/email"), 35 | Handler = fuu, 36 | ?assertEqual({ok, {Handler, ["abc"]}}, swagger_routerl_router:match( 37 | <<"/users/abc/email">>, [{MP, Handler}])), 38 | ?assertEqual({ok, {Handler, ["abc-def"]}}, swagger_routerl_router:match( 39 | <<"/users/abc-def/email">>, [{MP, Handler}])), 40 | ?assertEqual( 41 | {ok, {Handler, ["abc-123-def"]}}, 42 | swagger_routerl_router:match( 43 | <<"/users/abc-123-def/email">>, [{MP, Handler}])), 44 | ?assertEqual({error, endpoint_undefined}, swagger_routerl_router:match( 45 | <<"/users">>, [{MP, Handler}])), 46 | ?assertEqual({error, endpoint_undefined}, swagger_routerl_router:match( 47 | <<"/users/">>, [{MP, Handler}])), 48 | ?assertEqual({error, endpoint_undefined}, swagger_routerl_router:match( 49 | <<"/users/abc/fuu">>, [{MP, Handler}])), 50 | ?assertEqual({error, endpoint_undefined}, swagger_routerl_router:match( 51 | <<"/users/abc/email/fuu">>, [{MP, Handler}])), 52 | 53 | % check "-" 54 | MP2 = swagger_routerl_router:build_regex( 55 | "/my-users/{userid}/email"), 56 | Handler2 = bar, 57 | ?assertEqual({ok, {Handler2, ["abc"]}}, swagger_routerl_router:match( 58 | <<"/my-users/abc/email">>, [{MP2, Handler2}])). 59 | 60 | get_filename_test() -> 61 | Path = swagger_routerl_router:get_filename( 62 | "ws_", "/users/{userid}/email"), 63 | ?assertEqual( 64 | ws_users_userid_email, Path 65 | ). 66 | 67 | match_test() -> 68 | Url = <<"/my-clients/pippo">>, 69 | {ok, MP} = re:compile("my-clients/([\\w|-]+)"), 70 | {ok, {myhandler, Params}} = swagger_routerl_router:match( 71 | Url, [{MP, myhandler}]), 72 | ?assertEqual(["pippo"], Params). 73 | -------------------------------------------------------------------------------- /test/swagger_routerl_utils_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test utils. 19 | %%% @end 20 | -module(swagger_routerl_utils_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | all_test_() -> 27 | {foreach, 28 | fun start/0, 29 | fun stop/1, 30 | [ 31 | fun swaggerpath2module/1, 32 | fun swaggerpath_build_regex/1 33 | ] 34 | }. 35 | 36 | start() -> ok. 37 | 38 | stop(_) -> ok. 39 | 40 | swaggerpath2module(_) -> 41 | fun() -> 42 | ?assertEqual( 43 | ws_users_userid, 44 | swagger_routerl_utils:swaggerpath2module("ws_", "/users/{userid}") 45 | ), 46 | ?assertEqual( 47 | ws_my_users_userid, 48 | swagger_routerl_utils:swaggerpath2module("ws_", "/my-users/{userid}") 49 | ) 50 | end. 51 | 52 | swaggerpath_build_regex(_) -> 53 | fun() -> 54 | % string 55 | MP = swagger_routerl_utils:swaggerpath_build_regex("/users/{userid}"), 56 | {match, _} = re:run("/users/hello", MP), 57 | MP2 = swagger_routerl_utils:swaggerpath_build_regex( 58 | "/users/{userid}", "/GET"), 59 | {match, _} = re:run("/GET/users/hello", MP2), 60 | MP3 = swagger_routerl_utils:swaggerpath_build_regex( 61 | "/users/{userid}", "GET"), 62 | {match, _} = re:run("/GET/users/hello", MP3), 63 | 64 | % binary string 65 | MP = swagger_routerl_utils:swaggerpath_build_regex("/users/{userid}"), 66 | {match, _} = re:run(<<"/users/hello">>, MP), 67 | MP2 = swagger_routerl_utils:swaggerpath_build_regex( 68 | "/users/{userid}", "/GET"), 69 | {match, _} = re:run(<<"/GET/users/hello">>, MP2), 70 | MP3 = swagger_routerl_utils:swaggerpath_build_regex( 71 | "/users/{userid}", "GET"), 72 | {match, _} = re:run(<<"/GET/users/hello">>, MP3) 73 | end. 74 | 75 | to_binary_test() -> 76 | ?assertEqual(<<"hello">>, swagger_routerl_utils:to_binary(<<"hello">>)), 77 | ?assertEqual(<<"hello">>, swagger_routerl_utils:to_binary("hello")), 78 | ?assertEqual(<<"hello">>, swagger_routerl_utils:to_binary(hello)). 79 | 80 | to_atom_test() -> 81 | ?assertEqual(hello, swagger_routerl_utils:to_atom(<<"hello">>)), 82 | ?assertEqual(hello, swagger_routerl_utils:to_atom("hello")), 83 | ?assertEqual(hello, swagger_routerl_utils:to_atom(hello)). 84 | 85 | render_test() -> 86 | ?assertEqual( 87 | <<"hello worlds! See you worlds..">>, 88 | swagger_routerl_utils:render( 89 | <<"hello {{subject}}! See you {{subject}}..">>, 90 | [{<<"subject">>, <<"worlds">>}])). 91 | -------------------------------------------------------------------------------- /test/swagger_routerl_cowboy_ws_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test cowboy rest 19 | %%% @end 20 | -module(swagger_routerl_cowboy_ws_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | compile_test() -> 27 | File = [ 28 | {"paths", [ 29 | {"/users/{userid}", ok}, 30 | {"/users/{userid}/email", ok}, 31 | {"/my-clients/{clientid}", ok}, 32 | {"/boxes", ok}, 33 | {"/boxes/{boxid}", ok}, 34 | {"/not-exists/pippo", ok} 35 | ]} 36 | ], 37 | [{"/websocket", swagger_routerl_cowboy_v1_ws_json_dispatcher, Appctx}] = 38 | swagger_routerl_cowboy_ws:compile("ws_", File, fuubar, #{}), 39 | Event1 = #{ 40 | <<"path">> => <<"/users/pippo/email">>, 41 | <<"method">> => <<"get">> 42 | }, 43 | Event2 = #{ 44 | <<"path">> => <<"/users/pippo/email2">>, 45 | <<"method">> => <<"get">> 46 | }, 47 | Event3 = #{ 48 | <<"path">> => <<"/not-exists/pippo">>, 49 | <<"method">> => <<"get">> 50 | }, 51 | 52 | meck:new(ws_users_userid_email, 53 | [no_link, passthrough, no_history, non_strict]), 54 | meck:expect(ws_users_userid_email, get, 4, 55 | fun(MyEvent, empty, ["pippo"], fuubar) -> 56 | ?assertEqual(#{ 57 | <<"path">> => <<"/users/pippo/email">>, 58 | <<"method">> => <<"get">> 59 | }, MyEvent), 60 | {ok, MyEvent} 61 | end 62 | ), 63 | try 64 | ?assertEqual({ok, Event1}, 65 | swagger_routerl_cowboy_ws:dispatch(Event1, empty, Appctx)), 66 | ?assertEqual({error, endpoint_undefined, empty, fuubar}, 67 | swagger_routerl_cowboy_ws:dispatch(Event2, empty, Appctx)), 68 | ?assertEqual({error, handler_undefined, empty, fuubar}, 69 | swagger_routerl_cowboy_ws:dispatch(Event3, empty, Appctx)) 70 | after 71 | meck:validate(ws_users_userid_email), 72 | meck:unload(ws_users_userid_email) 73 | end, 74 | 75 | Event4 = #{ 76 | <<"path">> => <<"/my-clients/pippo">>, 77 | <<"method">> => <<"post">> 78 | }, 79 | meck:new('ws_my_clients_clientid', 80 | [no_link, passthrough, no_history, non_strict]), 81 | meck:expect('ws_my_clients_clientid', post, 4, 82 | fun(MyEvent, empty, ["pippo"], fuubar) -> 83 | ?assertEqual(#{ 84 | <<"path">> => <<"/my-clients/pippo">>, 85 | <<"method">> => <<"post">> 86 | }, MyEvent), 87 | {ok, MyEvent} 88 | end 89 | ), 90 | try 91 | ?assertEqual({ok, Event4}, 92 | swagger_routerl_cowboy_ws:dispatch(Event4, empty, Appctx)) 93 | after 94 | meck:validate('ws_my_clients_clientid'), 95 | meck:unload('ws_my_clients_clientid') 96 | end. 97 | -------------------------------------------------------------------------------- /src/swagger_routerl_utils.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Utils. 19 | %%% @end 20 | -module(swagger_routerl_utils). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | extract_params/2, 26 | render/2, 27 | swaggerpath2module/2, 28 | swaggerpath_build_regex/1, 29 | swaggerpath_build_regex/2, 30 | to_atom/1, 31 | to_binary/1 32 | ]). 33 | 34 | -export_type([params/0]). 35 | 36 | -type key() :: binary(). 37 | -type matches() :: {integer(), integer()}. 38 | -type pairs() :: list({key(), value()}). 39 | -type params() :: list(list()). 40 | -type path() :: list(). 41 | -type value() :: binary(). 42 | 43 | % @doc Convert a swagger path into a concrete erlang module name. 44 | % 45 | % e.g. ws_users_userid = swaggerpath2module("ws_", "/users/{userid}"). 46 | % @end 47 | -spec swaggerpath2module(string(), string()) -> atom(). 48 | swaggerpath2module(BaseName, PathConfig) -> 49 | Tokens = string:tokens(PathConfig, "/{}-"), 50 | list_to_atom(BaseName ++ string:join(Tokens, "_")). 51 | 52 | % @doc Build the regex of a swagger path item. 53 | % 54 | % e.g. 55 | % MP = swaggerpath_build_regex("/users/{userid}"), 56 | % {match, _} = re:run("/users/hello", MP). 57 | % @end 58 | -spec swaggerpath_build_regex(string()) -> re:mp(). 59 | swaggerpath_build_regex(SwaggerPath) -> 60 | List = string:tokens(SwaggerPath, "/"), 61 | RegexList = lists:map( 62 | fun(El) -> 63 | case re:run(El, "^{.+}$") of 64 | {match, _} -> "([\\w|-]+)"; % extended version: ([^/]+) 65 | _Rest -> El 66 | end 67 | end, List), 68 | RegEx = "^/" ++ string:join(RegexList, "/") ++ "$", 69 | 70 | {ok, MP} = re:compile(RegEx), 71 | MP. 72 | 73 | % @doc Do the same job of swaggerpath_build_regex/1, adding a default head to 74 | % the compiled regex. 75 | % 76 | % e.g. 77 | % MP = swaggerpath_build_regex("/users/{userid}", "/GET"), 78 | % {match, _} = re:run("/GET/users/hello", MP). 79 | % @end 80 | -spec swaggerpath_build_regex(string(), string()) -> re:mp(). 81 | swaggerpath_build_regex(SwaggerPath, Head) -> 82 | swaggerpath_build_regex(Head ++ SwaggerPath). 83 | 84 | -spec extract_params(path(), matches()) -> params(). 85 | extract_params(Path, [_First | Matches]) -> 86 | [string:substr( 87 | binary_to_list(Path), Start + 1, Length) || {Start, Length} <- Matches]. 88 | 89 | -spec to_atom(binary() | list() | binary()) -> atom(). 90 | to_atom(Atom) when is_atom(Atom) -> Atom; 91 | to_atom(List) when is_list(List) -> list_to_atom(List); 92 | to_atom(Binary) when is_binary(Binary) -> to_atom(binary_to_list(Binary)). 93 | 94 | -spec to_binary(binary() | list() | atom()) -> binary(). 95 | to_binary(Binary) when is_binary(Binary) -> Binary; 96 | to_binary(List) when is_list(List) -> list_to_binary(List); 97 | to_binary(Atom) when is_atom(Atom) -> to_binary(atom_to_list(Atom)). 98 | 99 | -spec render(binary(), pairs()) -> binary(). 100 | render(Template, SubPair) -> 101 | lists:foldl( 102 | fun({K, V}, Acc) -> 103 | Pattern = << <<"{{">>/binary, K/binary, <<"}}">>/binary >>, 104 | re:replace(Acc, Pattern, V, [{return, binary}, global]) 105 | end, Template, SubPair). 106 | -------------------------------------------------------------------------------- /src/swagger_routerl_router.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2017 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a routing table. 19 | %%% @end 20 | -module(swagger_routerl_router). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -export([ 25 | compile/3, 26 | dispatch/3, 27 | get_routectx/1, 28 | routes/2 29 | ]). 30 | 31 | -export_type([routectx/0, appctx/0, routes/0, event/0]). 32 | 33 | -type yaml() :: swagger_routerl:yaml(). 34 | -type routes() :: list({re:mp(), handler()}). 35 | -type handler() :: atom(). 36 | -type routectx() :: term(). 37 | -type appctx() :: #{routectx => routectx(), routes => routes()}. 38 | -type req() :: cowboy_req:req(). 39 | -type event() :: map(). 40 | -type params() :: swagger_routerl_utils:params(). 41 | -type path() :: swagger_routerl_utils:path(). 42 | -type swagger_paths() :: list({list(), any()}). 43 | 44 | -ifdef(TEST). 45 | -compile(export_all). 46 | -endif. 47 | 48 | %%% API functions 49 | 50 | -spec compile(list(), yaml(), routectx()) -> appctx(). 51 | compile(Prefix, Yaml, RouteCtx) -> 52 | Paths = proplists:get_value("paths", Yaml, []), 53 | Routes = routes(Paths, Prefix), 54 | build_context(Routes, RouteCtx). 55 | 56 | -spec routes(swagger_paths(), list()) -> routes(). 57 | routes(Paths, Prefix) -> 58 | lists:map(fun({SwaggerPath, _Config}) -> 59 | {build_regex(SwaggerPath), get_filename(Prefix, SwaggerPath)} 60 | end, Paths). 61 | 62 | -spec dispatch(event(), any(), appctx()) -> 63 | {reply, term(), req(), routectx()} 64 | | {ok, req(), routectx()} 65 | | {error, atom(), req(), routectx()}. 66 | dispatch(Event, Req, AppContext) -> 67 | Routes = maps:get(routes, AppContext), 68 | RouteCtx = maps:get(routectx, AppContext), 69 | case match(maps:get(<<"path">>, Event), Routes) of 70 | {error, Error} -> {error, Error, Req, RouteCtx}; 71 | {ok, {Handler, Params}} -> 72 | Method = swagger_routerl_utils:to_atom(maps:get(<<"method">>, Event)), 73 | try 74 | Handler:Method(Event, Req, Params, RouteCtx) 75 | catch 76 | error:undef -> {error, handler_undefined, Req, RouteCtx} 77 | end 78 | end. 79 | 80 | -spec get_routectx(appctx()) -> routectx(). 81 | get_routectx(#{routectx := RouteCtx}) -> RouteCtx. 82 | 83 | %%% Private functions 84 | 85 | -spec build_context(routes(), routectx()) -> appctx(). 86 | build_context(Routes, RouteCtx) -> 87 | #{ 88 | % routing table compiles 89 | routes => Routes, 90 | % this context will be passed to `swagger_routerl_cowboy_ws` 91 | routectx => RouteCtx 92 | }. 93 | 94 | -spec match(path(), routes()) -> 95 | {ok, {handler(), params()}} | {error, notfound}. 96 | match(_Path, []) -> 97 | {error, endpoint_undefined}; 98 | match(Path, [{MP, Handler} | Rest]) -> 99 | case re:run(Path, MP) of 100 | {match, Matches} -> 101 | {ok, {Handler, swagger_routerl_utils:extract_params(Path, Matches)}}; 102 | _Rest -> match(Path, Rest) 103 | end. 104 | 105 | -spec build_regex(list()) -> re:mp(). 106 | build_regex(SwaggerPath) -> 107 | swagger_routerl_utils:swaggerpath_build_regex(SwaggerPath). 108 | 109 | -spec get_filename(list(), list()) -> atom(). 110 | get_filename(Prefix, PathConfig) -> 111 | swagger_routerl_utils:swaggerpath2module(Prefix, PathConfig). 112 | -------------------------------------------------------------------------------- /test/swagger_routerl_cowboy_rest_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test cowboy rest 19 | %%% @end 20 | -module(swagger_routerl_cowboy_rest_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | swagger_routerl_cowboy_rest_test_() -> 27 | {foreach, 28 | fun start/0, 29 | fun stop/1, 30 | [ 31 | fun get_route/1, 32 | fun get_filename/1, 33 | fun compile/1 34 | ] 35 | }. 36 | 37 | start() -> ok. 38 | 39 | stop(_) -> ok. 40 | 41 | get_route(_) -> 42 | fun() -> 43 | Path = swagger_routerl_cowboy_rest:get_route( 44 | "api/0.1.0", "/users/{userid}/email"), 45 | ?assertEqual( 46 | "api/0.1.0/users/[:userid]/email", Path 47 | ) 48 | end. 49 | 50 | get_filename(_) -> 51 | fun() -> 52 | Path = swagger_routerl_cowboy_rest:get_filename( 53 | "resource_", "/users/{userid}/email"), 54 | ?assertEqual( 55 | resource_users_userid_email, Path 56 | ) 57 | end. 58 | 59 | compile(_) -> 60 | fun() -> 61 | File = [ 62 | {"basePath", "/api/0.1.0"}, 63 | {"paths", [ 64 | {"/users/{userid}", ok}, 65 | {"/users/{userid}/email", ok}, 66 | {"/clients/{clientid}", ok}, 67 | {"/boxes", ok}, 68 | {"/boxes/{boxid}", ok} 69 | ]} 70 | ], 71 | Routes = swagger_routerl_cowboy_rest:compile("resource_", File, ctx), 72 | ?assertEqual([ 73 | {"/api/0.1.0/users/[:userid]", resource_users_userid, ctx}, 74 | {"/api/0.1.0/users/[:userid]/email", resource_users_userid_email, ctx}, 75 | {"/api/0.1.0/clients/[:clientid]", resource_clients_clientid, ctx}, 76 | {"/api/0.1.0/boxes", resource_boxes, ctx}, 77 | {"/api/0.1.0/boxes/[:boxid]", resource_boxes_boxid, ctx} 78 | ], Routes) 79 | end. 80 | 81 | file_endpoint_test() -> 82 | Swagger = <<" 83 | --- 84 | swagger: '2.0' 85 | info: 86 | version: 1.0.0 87 | title: Simple API 88 | schemes: 89 | - {{scheme_protocol}} 90 | paths: 91 | /: 92 | get: 93 | responses: 94 | 200: 95 | description: OK 96 | ">>, 97 | Expected_http = <<" 98 | --- 99 | swagger: '2.0' 100 | info: 101 | version: 1.0.0 102 | title: Simple API 103 | schemes: 104 | - http 105 | paths: 106 | /: 107 | get: 108 | responses: 109 | 200: 110 | description: OK 111 | ">>, 112 | Expected_https = <<" 113 | --- 114 | swagger: '2.0' 115 | info: 116 | version: 1.0.0 117 | title: Simple API 118 | schemes: 119 | - https 120 | paths: 121 | /: 122 | get: 123 | responses: 124 | 200: 125 | description: OK 126 | ">>, 127 | 128 | ?assertEqual([{ 129 | "/docs/swagger.yaml", 130 | swagger_routerl_cowboy_v1_file_handler, 131 | #{swagger_file => Expected_http} 132 | }], swagger_routerl_cowboy_rest:file_endpoint(Swagger, #{})), 133 | ?assertEqual([{ 134 | "/docs/swagger.yaml", 135 | swagger_routerl_cowboy_v1_file_handler, 136 | #{swagger_file => Expected_https} 137 | }], swagger_routerl_cowboy_rest:file_endpoint(Swagger, #{ 138 | protocol => <<"https">> 139 | })), 140 | ?assertEqual([{ 141 | "/docs/swagger.yaml", 142 | myhandler, 143 | #{swagger_file => Expected_https} 144 | }], swagger_routerl_cowboy_rest:file_endpoint(Swagger, #{ 145 | protocol => <<"https">>, handler => myhandler 146 | })), 147 | ?assertEqual([{ 148 | "/myendpoint/swagger.yaml", 149 | swagger_routerl_cowboy_v1_file_handler, 150 | #{swagger_file => Expected_http} 151 | }], swagger_routerl_cowboy_rest:file_endpoint(Swagger, #{ 152 | protocol => <<"http">>, endpoint => "/myendpoint/swagger.yaml" 153 | })). 154 | -------------------------------------------------------------------------------- /test/swagger_routerl_emqtt_tests.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Test mqtt plugin. 19 | %%% @end 20 | -module(swagger_routerl_emqtt_tests). 21 | 22 | -author('Leonardo Rossi '). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | all_test_() -> 27 | {foreach, 28 | fun start/0, 29 | fun stop/1, 30 | [ 31 | fun extract_method/1, 32 | fun get_filename/1, 33 | fun build_regex/1, 34 | fun compile/1 35 | ] 36 | }. 37 | 38 | start() -> ok. 39 | 40 | stop(_) -> ok. 41 | 42 | extract_method(_) -> 43 | fun() -> 44 | AppCtx = swagger_routerl_emqtt:build_context(routes, routectx), 45 | ?assertEqual( 46 | {ok, <<"get">>}, 47 | swagger_routerl_emqtt:extract_method( 48 | <<"/get/users/pippo/email">>, AppCtx)), 49 | ?assertEqual( 50 | {error, wrong_topic}, 51 | swagger_routerl_emqtt:extract_method( 52 | <<"get/users/pippo/email">>, AppCtx)) 53 | end. 54 | 55 | get_filename(_) -> 56 | fun() -> 57 | ?assertEqual( 58 | emqtt_users_userid, 59 | swagger_routerl_emqtt:get_filename("/users/{userid}") 60 | ), 61 | ?assertEqual( 62 | emqtt_my_users_userid, 63 | swagger_routerl_emqtt:get_filename("/my-users/{userid}") 64 | ) 65 | end. 66 | 67 | build_regex(_) -> 68 | fun() -> 69 | Config = [{"get", get_config}, {"post", post_config}, 70 | {"put", put_config}, {"delete", delete_config}], 71 | [MPDELETE, MPPUT, MPPOST, MPGET] = swagger_routerl_emqtt:build_regex( 72 | "/users/{userid}/email", Config), 73 | {match, _} = re:run("/get/users/userid/email", MPGET), 74 | {match, _} = re:run("/post/users/userid/email", MPPOST), 75 | {match, _} = re:run("/put/users/userid/email", MPPUT), 76 | {match, _} = re:run("/delete/users/userid/email", MPDELETE) 77 | end. 78 | 79 | compile(_) -> 80 | fun() -> 81 | File = [ 82 | {"paths", [ 83 | {"/users/{userid}", [{"get", ok}]}, 84 | {"/users/{userid}/email", [{"get", ok}]}, 85 | {"/my-clients/{clientid}", [{"post", ok}]}, 86 | {"/boxes", [{"get", ok}]}, 87 | {"/boxes/{boxid}", [{"get", ok}]}, 88 | {"/not-exists/pippo", [{"post", ok}]} 89 | ]} 90 | ], 91 | RouteCtx = routectx, 92 | AppCtx = swagger_routerl_emqtt:compile(File, RouteCtx), 93 | Event1 = <<"/get/users/pippo/email">>, 94 | Event2 = <<"/get/users/pippo/email2">>, 95 | Event3 = <<"/get/boxes/not-defined">>, 96 | 97 | meck:new(emqtt_users_userid_email, 98 | [no_link, passthrough, no_history, non_strict]), 99 | meck:expect(emqtt_users_userid_email, get, 4, 100 | fun(MyEvent, empty, ["pippo"], routectx) -> 101 | ?assertEqual(<<"/get/users/pippo/email">>, MyEvent), 102 | {reply, MyEvent} 103 | end 104 | ), 105 | try 106 | ?assertEqual({reply, Event1}, 107 | swagger_routerl_emqtt:execute(Event1, empty, AppCtx)), 108 | ?assertEqual({error, not_found}, 109 | swagger_routerl_emqtt:execute(Event2, empty, AppCtx)), 110 | ?assertEqual({error, not_defined}, 111 | swagger_routerl_emqtt:execute(Event3, empty, AppCtx)) 112 | after 113 | meck:validate(emqtt_users_userid_email), 114 | meck:unload(emqtt_users_userid_email) 115 | end, 116 | 117 | Event4 = <<"/post/my-clients/pippo">>, 118 | meck:new('emqtt_my_clients_clientid', 119 | [no_link, passthrough, no_history, non_strict]), 120 | meck:expect('emqtt_my_clients_clientid', post, 4, 121 | fun(MyEvent, empty, ["pippo"], routectx) -> 122 | ?assertEqual(<<"/post/my-clients/pippo">>, MyEvent), 123 | {reply, MyEvent} 124 | end 125 | ), 126 | try 127 | ?assertEqual({reply, Event4}, 128 | swagger_routerl_emqtt:execute(Event4, empty, AppCtx)) 129 | after 130 | meck:validate('emqtt_my_clients_clientid'), 131 | meck:unload('emqtt_my_clients_clientid') 132 | end 133 | end. 134 | 135 | -------------------------------------------------------------------------------- /src/emqtt/swagger_routerl_emqtt.erl: -------------------------------------------------------------------------------- 1 | %%% @author Leonardo Rossi 2 | %%% @copyright (C) 2016 Leonardo Rossi 3 | %%% 4 | %%% This software is free software; you can redistribute it and/or 5 | %%% modify it under the terms of the GNU General Public License as 6 | %%% published by the Free Software Foundation; either version 2 of the 7 | %%% License, or (at your option) any later version. 8 | %%% 9 | %%% This software is distributed in the hope that it will be useful, but 10 | %%% WITHOUT ANY WARRANTY; without even the implied warranty of 11 | %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | %%% General Public License for more details. 13 | %%% 14 | %%% You should have received a copy of the GNU General Public License 15 | %%% along with this software; if not, write to the Free Software Foundation, 16 | %%% Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. 17 | %%% 18 | %%% @doc Transform the swagger file into a MQTT Application Layer. 19 | %%% 20 | %%% Through Swagger you can define endpoints connected to the topics. 21 | %%% 22 | %%% e.g. the topic /get/users/fuu will be equivalent to the REST GET /users/fuu 23 | %%% @end 24 | -module(swagger_routerl_emqtt). 25 | 26 | -author('Leonardo Rossi '). 27 | 28 | -export([compile/2, get_routectx/1, execute/3]). 29 | 30 | -export_type([routectx/0]). 31 | 32 | -type yaml() :: swagger_routerl:yaml(). 33 | -type routes() :: list({re:mp(), atom()}). 34 | -type routectx() :: term(). 35 | -type appctx() :: #{routectx => routectx(), routes => routes()}. 36 | -type payload() :: binary(). 37 | -type params() :: swagger_routerl_utils:params(). 38 | -type topic() :: swagger_routerl_utils:url(). 39 | -type handler() :: atom(). 40 | -type method() :: binary(). 41 | 42 | -ifdef(TEST). 43 | -compile(export_all). 44 | -endif. 45 | 46 | %%% API functions 47 | 48 | -spec compile(yaml(), routectx()) -> appctx(). 49 | compile(Yaml, RouteCtx) -> 50 | Paths = proplists:get_value("paths", Yaml), 51 | % merge the list of list in a single list 52 | Routes = lists:merge( 53 | % Return [ [{MP, module}, ..], ..] 54 | lists:map( 55 | fun({SwaggerPath, Config}) -> 56 | Module = get_filename(SwaggerPath), 57 | % return [{MP, module}, ..] 58 | lists:map(fun(MP) -> 59 | {MP, Module} 60 | end, build_regex(SwaggerPath, Config)) 61 | end, Paths)), 62 | build_context(Routes, RouteCtx). 63 | 64 | % TODO extend to bi-directionality stream. 65 | -spec execute(topic(), payload(), appctx()) -> 66 | {reply, payload()} | noreply | 67 | {error, not_found | not_defined | wrong_topic}. 68 | execute(Topic, Payload, #{routes := Routes, routectx := RouteCtx}=AppCtx) -> 69 | % match topic 70 | case match(Topic, Routes) of 71 | {error, _}=Error -> Error; 72 | {ok, {Handler, Params}} -> 73 | % extract method 74 | case extract_method(Topic, AppCtx) of 75 | {error, _}=Error -> Error; 76 | {ok, StringMethod} -> 77 | Method = swagger_routerl_utils:to_atom(StringMethod), 78 | try 79 | % execute endpoint 80 | Handler:Method(Topic, Payload, Params, RouteCtx) 81 | catch 82 | error:undef -> {error, not_defined} 83 | end 84 | end 85 | end; 86 | execute(_Topic, _Payload, _AppCtx) -> {error, wrong_topic}. 87 | 88 | %% Private functions 89 | 90 | -spec extract_method(topic(), appctx()) -> 91 | {ok, method()} | {error, wrong_topic}. 92 | extract_method(Topic, #{extract_method := MP}) -> 93 | case re:run(Topic, MP) of 94 | nomatch -> {error, wrong_topic}; 95 | {match, [_, {Init, End}, _Rest]} -> {ok, binary:part(Topic, Init, End)} 96 | end. 97 | 98 | 99 | -spec match(topic(), routes()) -> 100 | {ok, {handler(), params()}} | {error, not_found}. 101 | match(_Topic, []) -> 102 | {error, not_found}; 103 | match(Topic, [{MP, Handler} | Rest]) -> 104 | case re:run(Topic, MP) of 105 | {match, Matches} -> 106 | {ok, {Handler, swagger_routerl_utils:extract_params(Topic, Matches)}}; 107 | _Rest -> match(Topic, Rest) 108 | end. 109 | 110 | % @doc Build a list of regex 111 | -spec build_regex(string(), list({string(), list()})) -> list(re:mp()). 112 | build_regex(SwaggerPath, Config) -> 113 | lists:map( 114 | fun(Prefix) -> 115 | swagger_routerl_utils:swaggerpath_build_regex(SwaggerPath, Prefix) 116 | end, proplists:get_keys(Config)). 117 | 118 | -spec get_filename(list()) -> atom(). 119 | get_filename(PathConfig) -> 120 | swagger_routerl_utils:swaggerpath2module("emqtt_", PathConfig). 121 | 122 | -spec build_context(routes(), routectx()) -> appctx(). 123 | build_context(Routes, RouteCtx) -> 124 | {ok, MP} = re:compile("^/([\\w|-]+)/(.*)$"), 125 | #{ 126 | % routing table compiles 127 | routes => Routes, 128 | % this context will be passed to the route 129 | routectx => RouteCtx, 130 | % compiled regex to extract the method from the topic 131 | extract_method => MP 132 | }. 133 | 134 | -spec get_routectx(appctx()) -> routectx(). 135 | get_routectx(#{routectx := RouteCtx}) -> RouteCtx. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | You may use, distribute and copy swagger_routerl under the terms of GNU General 3 | Public License version 2, which is displayed below. 4 | 5 | ------------------------------------------------------------------------- 6 | 7 | GNU GENERAL PUBLIC LICENSE 8 | Version 2, June 1991 9 | 10 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 11 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 12 | Everyone is permitted to copy and distribute verbatim copies 13 | of this license document, but changing it is not allowed. 14 | 15 | Preamble 16 | 17 | The licenses for most software are designed to take away your 18 | freedom to share and change it. By contrast, the GNU General Public 19 | License is intended to guarantee your freedom to share and change free 20 | software--to make sure the software is free for all its users. This 21 | General Public License applies to most of the Free Software 22 | Foundation's software and to any other program whose authors commit to 23 | using it. (Some other Free Software Foundation software is covered by 24 | the GNU Library General Public License instead.) You can apply it to 25 | your programs, too. 26 | 27 | When we speak of free software, we are referring to freedom, not 28 | price. Our General Public Licenses are designed to make sure that you 29 | have the freedom to distribute copies of free software (and charge for 30 | this service if you wish), that you receive source code or can get it 31 | if you want it, that you can change the software or use pieces of it 32 | in new free programs; and that you know you can do these things. 33 | 34 | To protect your rights, we need to make restrictions that forbid 35 | anyone to deny you these rights or to ask you to surrender the rights. 36 | These restrictions translate to certain responsibilities for you if you 37 | distribute copies of the software, or if you modify it. 38 | 39 | For example, if you distribute copies of such a program, whether 40 | gratis or for a fee, you must give the recipients all the rights that 41 | you have. You must make sure that they, too, receive or can get the 42 | source code. And you must show them these terms so they know their 43 | rights. 44 | 45 | We protect your rights with two steps: (1) copyright the software, and 46 | (2) offer you this license which gives you legal permission to copy, 47 | distribute and/or modify the software. 48 | 49 | Also, for each author's protection and ours, we want to make certain 50 | that everyone understands that there is no warranty for this free 51 | software. If the software is modified by someone else and passed on, we 52 | want its recipients to know that what they have is not the original, so 53 | that any problems introduced by others will not reflect on the original 54 | authors' reputations. 55 | 56 | Finally, any free program is threatened constantly by software 57 | patents. We wish to avoid the danger that redistributors of a free 58 | program will individually obtain patent licenses, in effect making the 59 | program proprietary. To prevent this, we have made it clear that any 60 | patent must be licensed for everyone's free use or not licensed at all. 61 | 62 | The precise terms and conditions for copying, distribution and 63 | modification follow. 64 | 65 | GNU GENERAL PUBLIC LICENSE 66 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 67 | 68 | 0. This License applies to any program or other work which contains 69 | a notice placed by the copyright holder saying it may be distributed 70 | under the terms of this General Public License. The "Program", below, 71 | refers to any such program or work, and a "work based on the Program" 72 | means either the Program or any derivative work under copyright law: 73 | that is to say, a work containing the Program or a portion of it, 74 | either verbatim or with modifications and/or translated into another 75 | language. (Hereinafter, translation is included without limitation in 76 | the term "modification".) Each licensee is addressed as "you". 77 | 78 | Activities other than copying, distribution and modification are not 79 | covered by this License; they are outside its scope. The act of 80 | running the Program is not restricted, and the output from the Program 81 | is covered only if its contents constitute a work based on the 82 | Program (independent of having been made by running the Program). 83 | Whether that is true depends on what the Program does. 84 | 85 | 1. You may copy and distribute verbatim copies of the Program's 86 | source code as you receive it, in any medium, provided that you 87 | conspicuously and appropriately publish on each copy an appropriate 88 | copyright notice and disclaimer of warranty; keep intact all the 89 | notices that refer to this License and to the absence of any warranty; 90 | and give any other recipients of the Program a copy of this License 91 | along with the Program. 92 | 93 | You may charge a fee for the physical act of transferring a copy, and 94 | you may at your option offer warranty protection in exchange for a fee. 95 | 96 | 2. You may modify your copy or copies of the Program or any portion 97 | of it, thus forming a work based on the Program, and copy and 98 | distribute such modifications or work under the terms of Section 1 99 | above, provided that you also meet all of these conditions: 100 | 101 | a) You must cause the modified files to carry prominent notices 102 | stating that you changed the files and the date of any change. 103 | 104 | b) You must cause any work that you distribute or publish, that in 105 | whole or in part contains or is derived from the Program or any 106 | part thereof, to be licensed as a whole at no charge to all third 107 | parties under the terms of this License. 108 | 109 | c) If the modified program normally reads commands interactively 110 | when run, you must cause it, when started running for such 111 | interactive use in the most ordinary way, to print or display an 112 | announcement including an appropriate copyright notice and a 113 | notice that there is no warranty (or else, saying that you provide 114 | a warranty) and that users may redistribute the program under 115 | these conditions, and telling the user how to view a copy of this 116 | License. (Exception: if the Program itself is interactive but 117 | does not normally print such an announcement, your work based on 118 | the Program is not required to print an announcement.) 119 | 120 | These requirements apply to the modified work as a whole. If 121 | identifiable sections of that work are not derived from the Program, 122 | and can be reasonably considered independent and separate works in 123 | themselves, then this License, and its terms, do not apply to those 124 | sections when you distribute them as separate works. But when you 125 | distribute the same sections as part of a whole which is a work based 126 | on the Program, the distribution of the whole must be on the terms of 127 | this License, whose permissions for other licensees extend to the 128 | entire whole, and thus to each and every part regardless of who wrote it. 129 | 130 | Thus, it is not the intent of this section to claim rights or contest 131 | your rights to work written entirely by you; rather, the intent is to 132 | exercise the right to control the distribution of derivative or 133 | collective works based on the Program. 134 | 135 | In addition, mere aggregation of another work not based on the Program 136 | with the Program (or with a work based on the Program) on a volume of 137 | a storage or distribution medium does not bring the other work under 138 | the scope of this License. 139 | 140 | 3. You may copy and distribute the Program (or a work based on it, 141 | under Section 2) in object code or executable form under the terms of 142 | Sections 1 and 2 above provided that you also do one of the following: 143 | 144 | a) Accompany it with the complete corresponding machine-readable 145 | source code, which must be distributed under the terms of Sections 146 | 1 and 2 above on a medium customarily used for software interchange; or, 147 | 148 | b) Accompany it with a written offer, valid for at least three 149 | years, to give any third party, for a charge no more than your 150 | cost of physically performing source distribution, a complete 151 | machine-readable copy of the corresponding source code, to be 152 | distributed under the terms of Sections 1 and 2 above on a medium 153 | customarily used for software interchange; or, 154 | 155 | c) Accompany it with the information you received as to the offer 156 | to distribute corresponding source code. (This alternative is 157 | allowed only for noncommercial distribution and only if you 158 | received the program in object code or executable form with such 159 | an offer, in accord with Subsection b above.) 160 | 161 | The source code for a work means the preferred form of the work for 162 | making modifications to it. For an executable work, complete source 163 | code means all the source code for all modules it contains, plus any 164 | associated interface definition files, plus the scripts used to 165 | control compilation and installation of the executable. However, as a 166 | special exception, the source code distributed need not include 167 | anything that is normally distributed (in either source or binary 168 | form) with the major components (compiler, kernel, and so on) of the 169 | operating system on which the executable runs, unless that component 170 | itself accompanies the executable. 171 | 172 | If distribution of executable or object code is made by offering 173 | access to copy from a designated place, then offering equivalent 174 | access to copy the source code from the same place counts as 175 | distribution of the source code, even though third parties are not 176 | compelled to copy the source along with the object code. 177 | 178 | 4. You may not copy, modify, sublicense, or distribute the Program 179 | except as expressly provided under this License. Any attempt 180 | otherwise to copy, modify, sublicense or distribute the Program is 181 | void, and will automatically terminate your rights under this License. 182 | However, parties who have received copies, or rights, from you under 183 | this License will not have their licenses terminated so long as such 184 | parties remain in full compliance. 185 | 186 | 5. You are not required to accept this License, since you have not 187 | signed it. However, nothing else grants you permission to modify or 188 | distribute the Program or its derivative works. These actions are 189 | prohibited by law if you do not accept this License. Therefore, by 190 | modifying or distributing the Program (or any work based on the 191 | Program), you indicate your acceptance of this License to do so, and 192 | all its terms and conditions for copying, distributing or modifying 193 | the Program or works based on it. 194 | 195 | 6. Each time you redistribute the Program (or any work based on the 196 | Program), the recipient automatically receives a license from the 197 | original licensor to copy, distribute or modify the Program subject to 198 | these terms and conditions. You may not impose any further 199 | restrictions on the recipients' exercise of the rights granted herein. 200 | You are not responsible for enforcing compliance by third parties to 201 | this License. 202 | 203 | 7. If, as a consequence of a court judgment or allegation of patent 204 | infringement or for any other reason (not limited to patent issues), 205 | conditions are imposed on you (whether by court order, agreement or 206 | otherwise) that contradict the conditions of this License, they do not 207 | excuse you from the conditions of this License. If you cannot 208 | distribute so as to satisfy simultaneously your obligations under this 209 | License and any other pertinent obligations, then as a consequence you 210 | may not distribute the Program at all. For example, if a patent 211 | license would not permit royalty-free redistribution of the Program by 212 | all those who receive copies directly or indirectly through you, then 213 | the only way you could satisfy both it and this License would be to 214 | refrain entirely from distribution of the Program. 215 | 216 | If any portion of this section is held invalid or unenforceable under 217 | any particular circumstance, the balance of the section is intended to 218 | apply and the section as a whole is intended to apply in other 219 | circumstances. 220 | 221 | It is not the purpose of this section to induce you to infringe any 222 | patents or other property right claims or to contest validity of any 223 | such claims; this section has the sole purpose of protecting the 224 | integrity of the free software distribution system, which is 225 | implemented by public license practices. Many people have made 226 | generous contributions to the wide range of software distributed 227 | through that system in reliance on consistent application of that 228 | system; it is up to the author/donor to decide if he or she is willing 229 | to distribute software through any other system and a licensee cannot 230 | impose that choice. 231 | 232 | This section is intended to make thoroughly clear what is believed to 233 | be a consequence of the rest of this License. 234 | 235 | 8. If the distribution and/or use of the Program is restricted in 236 | certain countries either by patents or by copyrighted interfaces, the 237 | original copyright holder who places the Program under this License 238 | may add an explicit geographical distribution limitation excluding 239 | those countries, so that distribution is permitted only in or among 240 | countries not thus excluded. In such case, this License incorporates 241 | the limitation as if written in the body of this License. 242 | 243 | 9. The Free Software Foundation may publish revised and/or new versions 244 | of the General Public License from time to time. Such new versions will 245 | be similar in spirit to the present version, but may differ in detail to 246 | address new problems or concerns. 247 | 248 | Each version is given a distinguishing version number. If the Program 249 | specifies a version number of this License which applies to it and "any 250 | later version", you have the option of following the terms and conditions 251 | either of that version or of any later version published by the Free 252 | Software Foundation. If the Program does not specify a version number of 253 | this License, you may choose any version ever published by the Free Software 254 | Foundation. 255 | 256 | 10. If you wish to incorporate parts of the Program into other free 257 | programs whose distribution conditions are different, write to the author 258 | to ask for permission. For software which is copyrighted by the Free 259 | Software Foundation, write to the Free Software Foundation; we sometimes 260 | make exceptions for this. Our decision will be guided by the two goals 261 | of preserving the free status of all derivatives of our free software and 262 | of promoting the sharing and reuse of software generally. 263 | 264 | NO WARRANTY 265 | 266 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 267 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 268 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 269 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 270 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 271 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 272 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 273 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 274 | REPAIR OR CORRECTION. 275 | 276 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 277 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 278 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 279 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 280 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 281 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 282 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 283 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 284 | POSSIBILITY OF SUCH DAMAGES. 285 | 286 | END OF TERMS AND CONDITIONS 287 | ------------------------------------------------------------------------- 288 | --------------------------------------------------------------------------------