├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── cover.spec ├── include └── giallo.hrl ├── rebar ├── rebar.config ├── rebar.test.config ├── src ├── giallo.app.src ├── giallo.erl ├── giallo_app.erl ├── giallo_middleware.erl ├── giallo_multipart.erl ├── giallo_response.erl ├── giallo_sup.erl └── giallo_util.erl └── test ├── default_handler.erl ├── default_handler_before_template.html ├── default_handler_hello_world_template.html ├── default_handler_hello_world_template_var.html ├── giallo_SUITE.erl └── minimal_handler.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | ebin 4 | logs 5 | test/*.beam 6 | *.plt 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - R16B 4 | - R15B02 5 | - R15B01 6 | - R15B 7 | script: make ct 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = giallo 2 | DIALYZER = dialyzer 3 | REBAR = ./rebar 4 | 5 | .PHONY: all deps compile clean test ct build-plt dialyze 6 | 7 | all: deps compile 8 | 9 | deps: 10 | $(REBAR) get-deps 11 | 12 | doc: deps 13 | $(REBAR) doc skip_deps=true 14 | 15 | compile: 16 | $(REBAR) compile 17 | 18 | clean: 19 | $(REBAR) clean 20 | rm -f test/*.beam 21 | rm -f erl_crash.dump 22 | 23 | test: ct dialyze doc 24 | 25 | test-build: 26 | $(REBAR) -C rebar.test.config compile 27 | 28 | ct: clean deps test-build 29 | $(REBAR) -C rebar.test.config ct skip_deps=true 30 | 31 | build-plt: 32 | $(DIALYZER) --build_plt --output_plt .$(PROJECT).plt \ 33 | --apps erts kernel stdlib sasl inets crypto public_key ssl \ 34 | ./deps/cowboy/ebin ./deps/erlydtl/ebin ./deps/jsx/ebin \ 35 | ./deps/mimetypes/ebin ./deps/ranch/ebin 36 | 37 | dialyze: clean deps test-build 38 | $(DIALYZER) --plt .$(PROJECT).plt ebin \ 39 | -Werror_handling -Wrace_conditions -Wunmatched_returns 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Giallo (v0.2.0) [![Build Status](https://travis-ci.org/kivra/giallo.png?branch=master)](https://travis-ci.org/kivra/giallo) 2 | ====== 3 | 4 | Giallo (Italian pronunciation: [ˈdʒallo], plural gialli) is an Italian 5 | 20th-century genre of literature and film, which in Italian indicates crime 6 | fiction and mystery. 7 | 8 | Giallo is released under the terms of the [MIT](http://en.wikipedia.org/wiki/MIT_License) license 9 | 10 | Current stable version: [0.2.0](https://github.com/kivra/giallo/tree/0.2.0) 11 | 12 | Current α alpha version: [0.3.0](https://github.com/kivra/giallo) 13 | 14 | copyright 2012-2013 Kivra 15 | 16 | Goals 17 | ----- 18 | 19 | Giallo aims to provide a small and flexible web framework. Giallo 20 | builds on [Cowboy](https://github.com/extend/cowboy) and provides a bare 21 | minimum to implement a basic View-Controller pattern. Yes we've removed 22 | the Model from MVC, this is not a "everything but the kitchen sink" type of 23 | framework. 24 | 25 | Giallo can easily be embedded and provides some conveinient methods for 26 | working with Controller and Views while still giving you possibility to use 27 | standard Cowboy features when necessary. 28 | 29 | Examples 30 | -------- 31 | 32 | Here's some [examples](https://github.com/kivra/giallo_examples) that will 33 | show Giallo in action. 34 | 35 | Getting Started 36 | ---- 37 | 38 | A minimal working app could look like this: 39 | 40 | ```erlang 41 | -module(minimal_handler). 42 | 43 | -export([start/0]). 44 | -export([hi/4]). 45 | 46 | start() -> 47 | giallo:start([{'_', [{"/[...]", minimal_handler, []}]}]). 48 | 49 | hi(<<"GET">>, _PathInfo, _Extra, _Req) -> 50 | {output, <<"Ohai!">>}. 51 | 52 | ``` 53 | 54 | This would map the `my_handler` using standard Cowboy 55 | [Routing](http://ninenines.eu/docs/en/cowboy/HEAD/guide/routing) to the 56 | URI `http://yourserver/`. Implementing the action `hi` would make 57 | Giallo output "Ohai!" when performing a GET on `/hi` or anything 58 | below `/hi/extra...`. Any Giallo action that is implemented, such as `hi/4` 59 | gets precedence over the standard Cowboy Handler. 60 | 61 | The first argument is the HTTP method being used. 62 | 63 | The `_PathInfo` argument contains any extra url fragments as binaries in a 64 | list such as `/hi/extra/path/information` would give a list like 65 | `[<<"extra">>, <<"path">>, <<"information">>]`. The exact match, i.e. 66 | `/hi` would result in an empty list, `[]`. 67 | 68 | The third argument contains any extra variables that either get passed 69 | from the cowboy dispatcher or via an other action redispatching using 70 | `action_other` or from the before_/4 function. 71 | 72 | The `Req` argument is the standard Cowboy [Request] 73 | (https://github.com/extend/cowboy/blob/master/src/cowboy_req.erl#L128) Object. 74 | It's an opaque record so take a look at the functions in 75 | [cowboy_req.erl] 76 | (https://github.com/extend/cowboy/blob/master/src/cowboy_req.erl) on how 77 | to use it. 78 | 79 | It's also possible to use standard Cowboy handlers and also to mix the 80 | two behaviors, i.e. 81 | 82 | ```erlang 83 | -module(default_handler). 84 | 85 | -export([start/0]). 86 | -export([hi/4]). 87 | 88 | %% Standard Cowboy callback handlers 89 | -export([init/3]). 90 | -export([handle/2]). 91 | -export([terminate/3]). 92 | 93 | 94 | start() -> 95 | giallo:start([{'_', [{"/[...]", default_handler, []}]}]). 96 | 97 | %% Standard Cowboy callback handlers 98 | init(_Transport, Req, []) -> 99 | {ok, Req, undefined}. 100 | 101 | handle(Req, State) -> 102 | {ok, Req2} = cowboy_req:reply(200, [], <<"Hello World!">>, Req), 103 | {ok, Req2, State}. 104 | 105 | terminate(_Reason, _Req, _State) -> 106 | ok. 107 | 108 | %% Giallo handler 109 | hi(<<"GET">>, _PathInfo, _Extra, _Req) -> 110 | {output, <<"Ohai!">>}. 111 | 112 | ``` 113 | 114 | This would output "Ohai!" for the same URI's as the previous example but 115 | for anything else it would use the standard Cowboy `handle/2` function. 116 | So any Giallo function takes precedence over Cowboy handlers. 117 | 118 | Index action 119 | ------------ 120 | 121 | `index_/4` is a special action that can be implemented and will handle 122 | resource listings, i.e. `http://my.server/path/`. Any handler mapped to 123 | `/path/` will have it's `index_` function executed. `index_` functions 124 | behave the same as any other function and can thus use templating, etc. 125 | 126 | ```erlang 127 | index_(<<"GET">>, [], _Extra, _Req) -> 128 | {output, <<"Index listing">>}. 129 | 130 | ``` 131 | 132 | Templating 133 | ---------- 134 | 135 | Giallo uses [ErlyDTL](https://github.com/evanmiller/erlydtl) for 136 | standard templating. To dispatch a request from a Giallo controller you 137 | return `ok`, `{ok, Variables}` or `{ok, Variables, Headers}`. Giallo 138 | will then render the template associated with that controller and 139 | action. Giallo compounds the template name as `_`. 140 | 141 | You control how you compile your ErlyDtl templates through rebar. Using 142 | the `erlydtl_opts` directive you can specify where to find your 143 | templates: 144 | 145 | ```erlang 146 | {erlydtl_opts, [ 147 | {doc_root, "templates"}, % Where to find your templates 148 | {source_ext, ".html"} % Extension on your uncomplied templates 149 | ]}. 150 | 151 | ``` 152 | 153 | With these ErlyDTL options you would create your templates such as 154 | `controller_action.html` 155 | 156 | Session Management 157 | ------------------ 158 | 159 | If you need Session Management you'll have to include the [Giallo 160 | Session Backend](https://github.com/kivra/giallo_session). 161 | 162 | before_ function 163 | ---------------- 164 | 165 | There's a special function which can be implemented called before_/2 166 | which can be used to preprocess any request before dispatching to any 167 | action. This is especially useful when implementing an authentication 168 | scheme. 169 | 170 | The function header looks like: 171 | 172 | ```erlang 173 | before_(_Action, _Req) -> 174 | {ok, []}. 175 | 176 | ``` 177 | 178 | The first parameter is the Action that is to be performed after 179 | `before_` has been complete, it's supplied as an atom, i.e. `action_name`. 180 | The second parameter is the standard Req object. 181 | 182 | Possible return values are: 183 | 184 | #### `{ok, Extra}` #### 185 | Extra will get passed as the third argument to the action and as a 186 | variable called `_before` to any template 187 | 188 | #### `{redirect, Location}` #### 189 | Redirect to the `Location` effectively bypassing the Action 190 | 191 | Return values 192 | ------------- 193 | 194 | Here's a list of possible return values from a Giallo controller. 195 | 196 | All return values can take an optional `Req :: cowboy_req:req()` as the last 197 | parameter. This is especially useful if you need to set anything special 198 | to the response, like cookies, sessions, etc. 199 | 200 | #### `ok` #### 201 | Continue and render the template for the corresponding action 202 | 203 | #### `{ok, Variables::proplist()}` #### 204 | Same as above but pass `Variables` to the template. Variables can then be outputted in 205 | the template as `{{ variable_name }}` 206 | 207 | #### `{ok, Variables::proplist(), Headers::proplist()}` ### 208 | Same as above but also set additional HTTP Headers 209 | 210 | #### `{redirect, Location::binary()` ### 211 | Send a 302 redirect to the `Location` 212 | 213 | #### `{redirect, Location::binary(), Headers::proplist()}` ### 214 | Same as above but also set additional HTTP Headers 215 | 216 | #### `{moved, Location::binary()}` ### 217 | Send a 301 redirect to the `Location` 218 | 219 | #### `{moved, Location::binary(), Headers::proplist()}` ### 220 | Same as above but also set additional HTTP Headers 221 | 222 | #### `{action_other, Location::proplist()}` ### 223 | Possible values for `Location` are `[{action, your_action}, {controller, your_handler}]`. 224 | If `controller` is ommitted it will assume the current handler. 225 | 226 | #### `{action_other, Location::proplist(), Variables::proplist()}` ### 227 | Same as above but pass `Variables` that can be retrieved from the 228 | controller. 229 | 230 | #### `{render_other, Location::proplist()}` ### 231 | Render the view associated with the Action at `Location`. Possible values 232 | for `Location` are `[{action, your_action}, {controller, your_handler}]`. 233 | If `controller` is ommitted it will assume the current handler. 234 | 235 | #### `{render_other, Location::proplist(), Variables::proplist()}` ### 236 | Same as above but pass `Variables` that can be retrieved in the 237 | template. Possible values for `Location` are 238 | `[{action, your_action}, {controller, your_handler}]`. 239 | If `controller` is ommitted it will assume the current handler. 240 | 241 | #### `{output, Output::binary()}` ### 242 | print out the `Output` 243 | 244 | #### `{output, Output::binary(), Headers::proplist()}` ### 245 | Same as above but also set additional HTTP Headers 246 | 247 | #### `{stream, Generator::function(), Acc0::any()}` ### 248 | Stream a response to the client using HTTP chunked encoding. For each chunk, 249 | the Generator function is passed an accumulator (initally Acc0) and should 250 | return either {output, Data, Acc1} or done. I.e: 251 | 252 | ```erlang 253 | 254 | stream(<<"GET">>, _Pathinfo, _Extra, _Req) -> 255 | F = fun(Acc) -> 256 | case Acc =:= 3 of 257 | true -> done; 258 | false -> {output, <<"Hello\n">>, Acc+1} 259 | end 260 | end, 261 | {stream, F, 0}. 262 | 263 | ``` 264 | 265 | #### `{stream, Generator::function(), Acc::any(), Headers::proplist()}` ### 266 | Same as above but also set additional HTTP Headers 267 | 268 | #### `{json, Data::proplist()}` ### 269 | Encode the `Data` as json and output 270 | 271 | #### `{json, Data::proplist(), Headers::proplist()}` ### 272 | Same as above but also set additional HTTP Headers 273 | 274 | #### `{jsonp, Callback::string(), Data::proplist()}` ### 275 | Encode the `Data` as valid jsonp using the `Callback` 276 | 277 | #### `{jsonp, Callback::string(), Data::proplist(), Headers::proplist()}` ### 278 | Same as above but also set additional HTTP Headers 279 | 280 | #### `not_found` ### 281 | Respond with a 404 282 | 283 | #### `{error, Status::integer()}` ### 284 | Respond with the given error Status Code 285 | 286 | Request processing 287 | ------------- 288 | 289 | There is some conventience-functions for working with headers, 290 | querystrings, multipart-data, etc. Please generate and look at the docs: 291 | ``` 292 | 293 | make doc 294 | 295 | ``` 296 | 297 | ## License 298 | It's the [MIT license](http://en.wikipedia.org/wiki/MIT_License). So go ahead 299 | and do what you want! 300 | -------------------------------------------------------------------------------- /cover.spec: -------------------------------------------------------------------------------- 1 | {level,details}. 2 | {incl_mods, [ giallo_middleware 3 | , giallo_response 4 | , giallo_util 5 | , giallo 6 | ]}. 7 | -------------------------------------------------------------------------------- /include/giallo.hrl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | %% A Giallo record carrying all the information needed to handle a request 28 | -record(g, 29 | { req :: cowboy_req:req() %cowboy Request Obj 30 | , env :: cowboy_middleware:env() %cowboy Env Obj 31 | , handler=undefined :: atom() %current handler 32 | , action=undefined :: atom() | binary() %action to be executed 33 | , extra=[] :: list() %extra URL fragments 34 | , args=[] :: list() %extra arguments 35 | , before_args=[] :: list() %Arguments from before_ 36 | , resp=undefined :: undefined | any() %payload 37 | , method=undefined :: undefined | binary() %method of invocation 38 | }). 39 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivra/giallo/bec3539fb3e0d01177c87c302188fe231f647125/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {lib_dirs, ["deps"]}. 2 | 3 | {deps, [ 4 | {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", {tag, "0.8.5"}}}, 5 | {jsx, ".*", {git, "git://github.com/talentdeficit/jsx.git", {tag, "v1.4.1"}}}, 6 | {erlydtl, ".*", {git, "git://github.com/evanmiller/erlydtl.git", "876fd2b47e"}}, 7 | {mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git", {tag, "1.0"}}} 8 | ]}. 9 | 10 | {erlydtl_opts, [ 11 | {source_ext, ".html"} 12 | ]}. 13 | -------------------------------------------------------------------------------- /rebar.test.config: -------------------------------------------------------------------------------- 1 | {lib_dirs, ["deps"]}. 2 | {erl_opts, [ debug_info 3 | , nowarn_shadow_vars 4 | , warnings_as_errors 5 | ]}. 6 | {xref_checks, [ exports_not_used 7 | , undefined_function_calls 8 | ]}. 9 | {cover_enabled, true}. 10 | {cover_print_enabled, true}. 11 | {eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}. 12 | 13 | {deps, [ 14 | {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", {tag, "0.8.5"}}}, 15 | {jsx, ".*", {git, "git://github.com/talentdeficit/jsx.git", {tag, "v1.4.1"}}}, 16 | {erlydtl, ".*", {git, "git://github.com/evanmiller/erlydtl.git", "876fd2b47e"}} 17 | ]}. 18 | 19 | {erlydtl_opts, [ 20 | {compiler_options, [debug_info]}, 21 | {source_ext, ".html"}, 22 | {doc_root, "test"} 23 | ]}. 24 | -------------------------------------------------------------------------------- /src/giallo.app.src: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | {application, giallo, 28 | [ 29 | {description, "a small flexible web framework"}, 30 | {vsn, "0.3.0"}, 31 | {registered, []}, 32 | {applications, [ 33 | kernel, 34 | stdlib, 35 | cowboy 36 | ]}, 37 | {mod, {giallo_app, []}}, 38 | {env, [ 39 | {acceptors, 100}, 40 | {port, 8080} 41 | ]} 42 | ]}. 43 | -------------------------------------------------------------------------------- /src/giallo.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | %% @doc API for stoping and starting Giallo and convenience function. 28 | %% 29 | %% Giallo uses standard Cowboy features and makes it easy to mix and match 30 | %% conventient Giallo modules with the full power of Cowboy, REST-handlers, 31 | %% etc. 32 | %% 33 | %% This module provides functions for starting and stopping Giallo as well as 34 | %% some convenience functions for working with headers, parameters and 35 | %% multipart data. 36 | %% @end 37 | 38 | -module(giallo). 39 | 40 | -include("giallo.hrl"). 41 | 42 | -export([start/1]). 43 | -export([start/2]). 44 | -export([stop/0]). 45 | 46 | -export([header/2]). 47 | -export([header/3]). 48 | -export([post_param/2]). 49 | -export([post_param/3]). 50 | -export([post_param/4]). 51 | -export([query_param/2]). 52 | -export([query_param/3]). 53 | -export([multipart_file/2]). 54 | -export([multipart_param/2]). 55 | -export([multipart_param/3]). 56 | -export([multipart_stream/4]). 57 | 58 | -opaque giallo_req() :: #g{}. 59 | -export_type([giallo_req/0]). 60 | 61 | %% API ------------------------------------------------------------------------ 62 | 63 | %% @equiv start(Dispatch, []) 64 | -spec start(Dispatch) -> {ok, pid()} | {error, Reason} when 65 | Dispatch :: cowboy_router:routes(), 66 | Reason :: term(). 67 | start(Dispatch) -> 68 | start(Dispatch, []). 69 | 70 | %% @doc 71 | %% Start Giallo with the given routes and an options proplist with 72 | %% arguments for Giallo. Optional arguments would be one of: 73 | %%
74 | %%
acceptors:
75 | %%
Number of acceptors that Cowboy should start, 76 | %% Default:[{acceptors, 100}]
77 | %%
port:
78 | %%
The port on which Giallo should listen to, 79 | %% Default: [{port, 8080}]
80 | %%
81 | -spec start(Dispatch, Env) -> {ok, pid()} | {error, Reason} when 82 | Dispatch :: cowboy_router:routes(), 83 | Env :: proplists:proplist(), 84 | Reason :: term(). 85 | start(Dispatch, Env) -> 86 | CompiledDispatch = cowboy_router:compile(Dispatch), 87 | {ok, Acceptors} = get_env(acceptors, Env), 88 | {ok, Port} = get_env(port, Env), 89 | cowboy:start_http(giallo_http_listener, Acceptors, [{port, Port}], [ 90 | {env, [{dispatch, CompiledDispatch}]}, 91 | {middlewares, [cowboy_router, giallo_middleware, 92 | cowboy_handler]} 93 | ]). 94 | 95 | %% @doc Stop Giallo 96 | -spec stop() -> ok | {error, Reason} when 97 | Reason :: term(). 98 | stop() -> 99 | application:stop(giallo). 100 | 101 | %% Req Convenience functions -------------------------------------------------- 102 | 103 | %% @equiv post_param(Key, Req0, undefined) 104 | -spec post_param(Key, Req0) -> Result when 105 | Key :: binary(), 106 | Req0 :: cowboy_req:req(), 107 | Result :: {binary() | undefined, cowboy_req:req()} | {error, atom()}. 108 | post_param(Key, Req0) -> 109 | post_param(Key, Req0, undefined). 110 | 111 | %% @equiv post_param(Key, Req0, Default, 16000) 112 | -spec post_param(Key, Req0, Default) -> Result when 113 | Key :: binary(), 114 | Req0 :: cowboy_req:req(), 115 | Default :: any(), 116 | Result :: {binary() | undefined, cowboy_req:req()} | {error, atom()}. 117 | post_param(Key, Req0, Default) -> 118 | post_param(Key, Req0, Default, 16000). 119 | 120 | %% @doc 121 | %% Return a named parameter from a HTTP POST or Default if not found, 122 | %% see query_param/2 for query parameter retrieving. 123 | %% 124 | %% There's a default limit on body post size on 16kb, if that limit is 125 | %% exceeded a {error, badlength} will get returned. You can 126 | %% optionally pass in an other value for MaxBodyLength or the atom 127 | %% infinity to bypass size constraints 128 | -spec post_param(Key, Req0, Default, MaxBodyLength) -> Result when 129 | Key :: binary(), 130 | Req0 :: cowboy_req:req(), 131 | Default :: any(), 132 | MaxBodyLength :: non_neg_integer() | infinity, 133 | Result :: {binary() | Default, cowboy_req:req()} | {error, atom()}. 134 | post_param(Key, Req0, Default, MaxBodyLength) -> 135 | case cowboy_req:body(MaxBodyLength, Req0) of 136 | {error, _} = E -> E; 137 | {ok, Buffer, Req1} -> 138 | BodyQs = cowboy_http:x_www_form_urlencoded(Buffer), 139 | Req2 = cowboy_req:set([{buffer, Buffer}], Req1), 140 | Req3 = cowboy_req:set([{body_state, waiting}], Req2), 141 | case lists:keyfind(Key, 1, BodyQs) of 142 | {Key, Value} -> {Value, Req3}; 143 | false -> {Default, Req3} 144 | end 145 | end. 146 | 147 | %% @equiv query_param(Key, Req0, undefined) 148 | -spec query_param(Key, Req0) -> Result when 149 | Key :: binary(), 150 | Req0 :: cowboy_req:req(), 151 | Result :: {binary() | undefined, cowboy_req:req()}. 152 | query_param(Key, Req0) -> 153 | query_param(Key, Req0, undefined). 154 | 155 | %% @doc 156 | %% Return a named parameter from the querystring or Default 157 | %% if not found, see post_param/2 for HTTP POST parameter retrieving. 158 | -spec query_param(Key, Req0, Default) -> Result when 159 | Key :: binary(), 160 | Req0 :: cowboy_req:req(), 161 | Default :: any(), 162 | Result :: {binary() | Default, cowboy_req:req()}. 163 | query_param(Key, Req0, Default) -> 164 | cowboy_req:qs_val(Key, Req0, Default). 165 | 166 | %% @equiv header(Key, Req0, undefined) 167 | -spec header(Key, Req0) -> {binary() | undefined, cowboy_req:req()} when 168 | Key :: binary(), 169 | Req0 :: cowboy_req:req(). 170 | header(Key, Req0) -> 171 | header(Key, Req0, undefined). 172 | 173 | %% @doc 174 | %% Return a named HTTP Header from the Request or Default 175 | %% if not found. 176 | -spec header(Key, Req0, Default) -> Result when 177 | Key :: binary(), 178 | Req0 :: cowboy_req:req(), 179 | Default :: any(), 180 | Result :: {binary() | Default, cowboy_req:req()}. 181 | header(Key, Req0, Default) -> 182 | cowboy_req:header(Key, Req0, Default). 183 | 184 | %% @equiv multipart_param(Key, Req0, undefined) 185 | -spec multipart_param(Key, Req0) -> binary() | undefined when 186 | Key :: binary(), 187 | Req0 :: cowboy_req:req(). 188 | multipart_param(Key, Req0) -> 189 | multipart_param(Key, Req0, undefined). 190 | 191 | %% @doc 192 | %% Returns the value of a multipart request, or Default if not found. 193 | -spec multipart_param(Key, Req0, Default) -> binary() | Default when 194 | Key :: binary(), 195 | Req0 :: cowboy_req:req(), 196 | Default :: any(). 197 | multipart_param(Key, Req0, Default) -> 198 | case giallo_multipart:param(Key, Req0) of 199 | undefined -> Default; 200 | Value -> Value 201 | end. 202 | 203 | %% @doc 204 | %% Locates a multipart field named Param, assumed to contain a file. 205 | %% Returns {Filename, Body}, where Filename is the result of decoding 206 | %% the "filename" part of the Content-Disposition header. 207 | -spec multipart_file(Key, Req0) -> {binary(), binary()} | undefined when 208 | Key :: binary(), 209 | Req0 :: cowboy_req:req(). 210 | multipart_file(Key, Req0) -> 211 | giallo_multipart:file(Key, Req0). 212 | 213 | %% @doc 214 | %% Streams fragments of a multipart part by repeatedly calling 215 | %% Fun(Fragment, Meta, State) where Fragment is a binary containing 216 | %% a part of the body, Meta contains the header fields of the part, 217 | %% and State is a user-specified updated on each call to Fun. 218 | %% When the end of the part is reached, Fun is called with Fragment 219 | %% set to the atom "eof". 220 | -spec multipart_stream(Key, Fun, State, Req0) -> 221 | {binary(), binary()} | undefined when 222 | Key :: binary(), 223 | Fun :: fun(), 224 | State :: any(), 225 | Req0 :: cowboy_req:req(). 226 | multipart_stream(Key, Fun, State, Req0) -> 227 | giallo_multipart:stream_param(Key, Fun, State, Req0). 228 | 229 | %% Private -------------------------------------------------------------------- 230 | 231 | get_env(Key, Env) -> 232 | case lists:keyfind(Key, 1, Env) of 233 | {Key, Val} -> {ok, Val}; 234 | false -> application:get_env(giallo, Key) 235 | end. 236 | -------------------------------------------------------------------------------- /src/giallo_app.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(giallo_app). 28 | -behaviour(application). 29 | 30 | -export([start/2]). 31 | -export([stop/1]). 32 | 33 | %% API ------------------------------------------------------------------------ 34 | 35 | start(_StartType, _StartArgs) -> 36 | giallo_sup:start_link(). 37 | 38 | stop(_State) -> 39 | ok. 40 | -------------------------------------------------------------------------------- /src/giallo_middleware.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | %% @doc Giallo middleware. 28 | %% 29 | %% This module is implemented as a Cowboy middleware and checks if a given 30 | %% request should be handled by a Giallo handler. If so it executes that 31 | %% handler and evaluates the response. If Giallo determines that it shouldn't 32 | %% execute the handler it will hand it off to Cowboy or return 404 depending 33 | %% on if the handler implements the correct Cowboy behavior or not. 34 | %% @end 35 | 36 | -module(giallo_middleware). 37 | -behaviour(cowboy_middleware). 38 | 39 | -include("giallo.hrl"). 40 | 41 | -export([execute/2]). 42 | -export([execute_handler/1]). 43 | 44 | %% API ------------------------------------------------------------------------ 45 | 46 | %% @doc This function is called from Cowboy for every request. For every call 47 | %% it will check to see if it should execute a Giallo handler, or 48 | %% transparently pass on to Cowboy. The flow of control is: 49 | %% 50 | %%
 51 | %%      Cowboy
 52 | %%        |
 53 | %%      Giallo ({@link giallo_middleware:execute/2})
 54 | %%        |
 55 | %%      Is handler valid?
 56 | %%        |  \ no
 57 | %%        |   - Hand over the control to Cowboy to 404 it
 58 | %%        |
 59 | %%        | yes
 60 | %%      Is action valid?
 61 | %%        |  \ no
 62 | %%        |   \
 63 | %%        |    - Is the index_/4 action valid?
 64 | %%        |    | \ no
 65 | %%        |    |  \
 66 | %%        |    |   - Is the handler a valid Cowboy handler?
 67 | %%        |    |   |  \ no
 68 | %%        |    |   |   \
 69 | %%        |    |   |    - Return a 404
 70 | %%        |    |   |
 71 | %%        |    | Hand over control to Cowboy to continue processing
 72 | %%        |    |
 73 | %%        |  Execute(*) the index_/4 action and render the result
 74 | %%        |
 75 | %%      Execute(*) the action and render the result
 76 | %%
 77 | %%      (*) Look to see if before_/4 should be executed
 78 | %%      
79 | -spec execute(Req0, Env) -> 80 | {ok, Req, Env} | {error, 500, Req} | {halt, Req} when 81 | Req0 :: cowboy_req:req() 82 | ,Req :: cowboy_req:req() 83 | ,Env :: cowboy_middleware:env(). 84 | execute(Req0, Env) -> 85 | {handler, Handler} = lists:keyfind(handler, 1, Env), 86 | {handler_opts, Arguments} = lists:keyfind(handler_opts, 1, Env), 87 | {Extra, Req1} = giallo_util:get_extra(Req0), 88 | {Action, Req2} = giallo_util:get_action(Req1), 89 | {Method, Req3} = cowboy_req:method(Req2), 90 | GialloReq = #g{ req=Req3, env=Env, handler=Handler 91 | , action=Action, extra=Extra 92 | , args=Arguments, method=Method }, 93 | 94 | giallo_response:eval( 95 | case {valid_handler(GialloReq) 96 | , valid_action(GialloReq) 97 | , valid_action(GialloReq#g{action=index_})} of 98 | {true, true, _} -> run(GialloReq); 99 | {true, false, true} -> run(GialloReq#g{action=index_}); 100 | {true, false, false} -> maybe_continue(GialloReq); 101 | {false, _, _} -> continue(GialloReq) 102 | end). 103 | 104 | -spec execute_handler(GialloReq) -> 105 | {ok, Req, Env} | {error, 500, Req} | {halt, Req} when 106 | GialloReq :: giallo:giallo_req() 107 | ,Req :: cowboy_req:req() 108 | ,Env :: cowboy_middleware:env(). 109 | execute_handler(#g{ req=Req0 } = GialloReq0) -> 110 | {Extra, Req1} = giallo_util:get_extra(Req0), 111 | GialloReq1 = GialloReq0#g{ extra=Extra, req=Req1 }, 112 | 113 | giallo_response:eval( 114 | case {valid_handler(GialloReq1), valid_action(GialloReq1)} of 115 | {true, true} -> run(GialloReq1); 116 | {true, false} -> notfound(GialloReq1); 117 | {false, _} -> continue(GialloReq1) 118 | end). 119 | 120 | %% Private -------------------------------------------------------------------- 121 | 122 | -spec run(giallo:giallo_req()) -> giallo:giallo_req(). 123 | run(#g{ args=Args, action=Action, method=Method 124 | , req=Req, extra=Extra }=GialloReq0 ) -> 125 | GialloReq1 = GialloReq0#g{ action=giallo_util:any2ea(Action) }, 126 | case maybe_do_before(GialloReq1) of 127 | {ok, BeforeArgs} -> 128 | GialloReq1#g{ before_args=BeforeArgs 129 | , resp={before, handler_handle(GialloReq1#g{ 130 | args=[ Method 131 | , Extra 132 | , Args 133 | ++ BeforeArgs 134 | , Req] })} }; 135 | BeforeResponse -> GialloReq1#g{ resp=BeforeResponse } 136 | end. 137 | 138 | -spec valid_handler(giallo:giallo_req()) -> boolean(). 139 | valid_handler(#g{ handler = Handler }) -> 140 | case code:ensure_loaded(Handler) of 141 | {module, Handler} -> true; 142 | {error, _Reason} -> false 143 | end. 144 | 145 | -spec valid_action(giallo:giallo_req()) -> boolean(). 146 | valid_action(#g{ action=non_existent_action }) -> 147 | false; 148 | valid_action(#g{ handler=Handler, action=Action }) -> 149 | try 150 | %% if there exist an exported function in the running VM with the name 151 | %% Action that would get converted to an atom. If not an 152 | %% erlang bad arg would get thrown and we need to catch that not to 153 | %% crash the current Cowboy process. 154 | erlang:function_exported(Handler, giallo_util:any2ea(Action), 4) 155 | catch _:_ -> 156 | false 157 | end. 158 | 159 | %% @doc Validate if the Handler is indeed a Cowboy handler, if so pass over 160 | %% control to Cowboy, else return 404. 161 | -spec maybe_continue(giallo:giallo_req()) -> giallo:giallo_req(). 162 | maybe_continue(#g{ handler=Handler }=GialloReq) -> 163 | case erlang:function_exported(Handler, init, 3) of 164 | true -> continue(GialloReq); 165 | false -> notfound(GialloReq) 166 | end. 167 | 168 | -spec notfound(giallo:giallo_req()) -> giallo:giallo_req(). 169 | notfound(GialloReq) -> 170 | GialloReq#g{ resp={error, 404} }. 171 | 172 | -spec continue(giallo:giallo_req()) -> giallo:giallo_req(). 173 | continue(GialloReq) -> 174 | GialloReq#g{ action=non_existent_action, resp=continue }. 175 | 176 | handler_handle(#g{ handler=Handler, action=Action, args=Args }=GialloReq) -> 177 | try apply(Handler, Action, Args) 178 | catch Class:Reason -> 179 | giallo_util:error(GialloReq, erlang:length(Args), Class 180 | , Reason, erlang:get_stacktrace()) 181 | end. 182 | 183 | maybe_do_before(#g{ handler=Handler }=GialloReq) -> 184 | case erlang:function_exported(Handler, before_, 2) of 185 | true -> 186 | handler_handle(GialloReq#g{ action=before_ 187 | , args=[ GialloReq#g.action 188 | , GialloReq#g.req ] }); 189 | false -> {ok, []} 190 | end. 191 | -------------------------------------------------------------------------------- /src/giallo_multipart.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(giallo_multipart). 28 | 29 | -include("giallo.hrl"). 30 | 31 | -export([param/2]). 32 | -export([file/2]). 33 | -export([stream_param/4]). 34 | 35 | %% API ------------------------------------------------------------------------ 36 | 37 | %% @doc Returns the value of a multipart request, or undefined if not found. 38 | -spec param(Param, Req) -> undefined | binary() when 39 | Param :: binary(), 40 | Req :: cowboy_req:req(). 41 | param(Param, Req) -> 42 | stream_param(Param, 43 | fun(eof, _Meta, Acc) -> 44 | iolist_to_binary(lists:reverse(Acc)); 45 | (Frag, _Meta, Acc) -> 46 | [Frag|Acc] 47 | end, 48 | [], 49 | Req). 50 | 51 | %% @doc 52 | %% Streams fragments of a multipart part by repeatedly calling 53 | %% Fun(Fragment, Meta, State) where Fragment is a binary containing 54 | %% a part of the body, Meta contains the header fields of the part, 55 | %% and State is a user-specified updated on each call to Fun. 56 | %% When the end of the part is reached, Fun is called with Fragment 57 | %% set to the atom "eof". 58 | %% @end 59 | -spec stream_param(Name, Fun, StateTy, Req) -> undefined | StateTy when 60 | Name :: binary(), 61 | Fun :: fun(), 62 | State0 :: any(), 63 | Req :: cowboy_req:req(), 64 | State0 :: any(). 65 | stream_param(Name, Fun, State0, Req) -> 66 | case find_multipart(Name, cowboy_req:multipart_data(Req)) of 67 | {ok, {Meta, Req2}} -> 68 | read_multipart(Fun, 69 | State0, 70 | Meta, 71 | cowboy_req:multipart_data(Req2)); 72 | undefined -> 73 | undefined 74 | end. 75 | 76 | %% @doc 77 | %% Locates a multipart field named Param, assumed to contain a file. 78 | %% Returns {Filename, Body}, where Filename is the result of decoding 79 | %% the "filename" part of the Content-Disposition header. 80 | %% @end 81 | -spec file(Param, Req) -> {Filename, Body} when 82 | Param :: binary(), 83 | Req :: cowboy_req:req(), 84 | Filename :: binary(), 85 | Body :: binary(). 86 | file(Param, Req) -> 87 | stream_param(Param, 88 | fun(eof, Meta, Acc) -> 89 | {<<"filename">>, Filename} = 90 | lists:keyfind(<<"filename">>, 1, Meta), 91 | {Filename, iolist_to_binary(lists:reverse(Acc))}; 92 | (Frag, _Meta, Acc) -> 93 | [Frag|Acc] 94 | end, 95 | [], 96 | Req). 97 | 98 | %% Private -------------------------------------------------------------------- 99 | 100 | %% @doc 101 | %% Tries to locate a named parameter in a multipart-encoded request. 102 | %% If found, a tuple of the part header and a request object for which 103 | %% calling cowboy_req:multipart_data will start generating 104 | %% body data is returned. Otherwise, undefined is returned. 105 | %% @end 106 | %% @private 107 | -spec find_multipart(Target, Part) -> Result when 108 | Target :: binary(), 109 | Part :: {{headers, [tuple()]}, cowboy_req:req()} 110 | | {eof, cowboy_req:req()}, 111 | Result :: undefined | {[tuple()], cowboy_req:req()}. 112 | find_multipart(Target, {headers, Headers, Req}) -> 113 | {<<"content-disposition">>, ContentDisposition} = 114 | lists:keyfind(<<"content-disposition">>, 1, Headers), 115 | case ContentDisposition of 116 | <<"form-data", Rest/binary>> -> 117 | Meta = cowboy_http:params(Rest, fun(_, Params) -> Params end), 118 | case lists:keyfind(<<"name">>, 1, Meta) of 119 | {<<"name">>, Target} -> 120 | {ok, {Meta, Req}}; 121 | _ -> 122 | {ok, Req2} = cowboy_req:multipart_skip(Req), 123 | find_multipart(Target, cowboy_req:multipart_data(Req2)) 124 | end; 125 | _ -> 126 | {ok, Req2} = cowboy_req:multipart_skip(Req), 127 | find_multipart(Target, cowboy_req:multipart_data(Req2)) 128 | end; 129 | find_multipart(_Target, {eof, _Req}) -> 130 | undefined. 131 | 132 | %% @doc 133 | %% Reads the body of a multipart request part, calling a function for every 134 | %% fragment received, returning the result of the last function invocation. 135 | %% @end 136 | %% @private 137 | -spec read_multipart(Fun, State, Meta, Part) -> any() when 138 | Fun :: fun(), 139 | State :: any(), 140 | Meta :: [tuple()], 141 | Part :: {headers, cowboy_http:headers(), cowboy_req:req()} 142 | | {body, binary(), cowboy_req:req()} 143 | | {end_of_part | eof, cowboy_req:req()}. 144 | read_multipart(Fun, State, Meta, {body, Body, Req}) -> 145 | State2 = Fun(Body, Meta, State), 146 | read_multipart( 147 | Fun, 148 | State2, 149 | Meta, 150 | cowboy_req:multipart_data(Req)); 151 | read_multipart(Fun, State, Meta, {end_of_part, _Req}) -> 152 | Fun(eof, Meta, State). 153 | -------------------------------------------------------------------------------- /src/giallo_response.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | %% @doc Handle Giallo's returntypes. 28 | %% 29 | %% This module provides one function eval/5 which evaluates any 30 | %% return value from a Giallo handler. 31 | %% @end 32 | 33 | -module(giallo_response). 34 | 35 | -export([eval/1]). 36 | 37 | -include("giallo.hrl"). 38 | 39 | -define(DEFAULT_CT, [{<<"content-type">>, <<"text/html">>}]). 40 | -define(JSON_CT, [{<<"content-type">>, <<"application/json">>}]). 41 | -define(JS_CT, [{<<"content-type">>, <<"application/javascript">>}]). 42 | 43 | %% API ------------------------------------------------------------------------ 44 | 45 | -spec eval(GialloReq) -> 46 | {halt, Req} | {error, Status, Req} | {ok, Req, Env} when 47 | GialloReq :: giallo:giallo_req(), 48 | Req :: cowboy_req:req(), 49 | Env :: cowboy_middleware:env(), 50 | Status :: non_neg_integer(). 51 | eval(GialloReq) -> 52 | do_eval(unmarshal_req(unmarshal_before(GialloReq))). 53 | 54 | %% Private -------------------------------------------------------------------- 55 | 56 | unmarshal_before(#g{ resp={before, ok}, before_args=Args }=GialloReq) -> 57 | GialloReq#g{ resp={ok, [{<<"_before">>, Args}]} }; 58 | unmarshal_before(#g{ resp={before, {ok, Var}}, before_args=Args }=GialloReq) -> 59 | GialloReq#g{ resp={ok, [{<<"_before">>, Args} | Var]} }; 60 | unmarshal_before(#g{ resp={before, {ok, Var, Headers}} 61 | , before_args=Args }=GialloReq) -> 62 | GialloReq#g{ resp={ok, [{<<"_before">>, Args} | Var], Headers} }; 63 | unmarshal_before(#g{ resp={before, {render_other, Props, Var}} 64 | , before_args=Args }=GialloReq) -> 65 | GialloReq#g{ resp={render_other, Props, [{<<"_before">>, Args} | Var]} }; 66 | unmarshal_before(#g{ resp={before, Eval} }=GialloReq) -> 67 | GialloReq#g{ resp=Eval }; 68 | unmarshal_before(GialloReq) -> 69 | GialloReq. 70 | 71 | unmarshal_req(#g{ resp={Eval, X, Req} }=GialloReq) when is_tuple(Req) -> 72 | GialloReq#g{ resp={Eval, X}, req=Req }; 73 | unmarshal_req(#g{ resp={Eval, X, Y, Req} }=GialloReq) when is_tuple(Req) -> 74 | GialloReq#g{ resp={Eval, X, Y}, req=Req }; 75 | unmarshal_req(#g{ resp={Eval, X, Y, Z, Req} }=GialloReq) when is_tuple(Req) -> 76 | GialloReq#g{ resp={Eval, X, Y, Z}, req=Req }; 77 | unmarshal_req(GialloReq) -> 78 | GialloReq. 79 | 80 | do_eval(#g{ resp=ok }=GialloReq) -> 81 | do_eval(GialloReq#g{ resp={ok, []} }); 82 | do_eval(#g{ resp={ok, Var} }=GialloReq) -> 83 | do_eval(GialloReq#g{ resp={ok, Var, []} }); 84 | do_eval(#g{ resp={ok, Var, Headers} }=GialloReq) -> 85 | do_eval(GialloReq#g{ resp=render_template(GialloReq, Var, Headers) }); 86 | do_eval(#g{ resp={redirect, Location} }=GialloReq) when is_binary(Location) -> 87 | do_eval(GialloReq#g{ resp={redirect, Location, []} }); 88 | do_eval(#g{ resp={redirect, Location, Headers}, req=Req }) 89 | when is_binary(Location) -> 90 | redirect_or_move(302, Location, Headers, Req); 91 | do_eval(#g{ resp={moved, Location} }=GialloReq) when is_binary(Location) -> 92 | do_eval(GialloReq#g{ resp={moved, Location, []} }); 93 | do_eval(#g{ resp={moved, Location, Headers}, req=Req }) 94 | when is_binary(Location) -> 95 | redirect_or_move(301, Location, Headers, Req); 96 | do_eval(#g{ resp={action_other, Props} }=GialloReq) -> 97 | do_eval((GialloReq#g{ resp=action_other(GialloReq, Props) })); 98 | do_eval(#g{ resp={action_other, Props, Args} }=GialloReq) -> 99 | do_eval((GialloReq#g{ resp=action_other(GialloReq, Props), args=Args })); 100 | do_eval(#g{ resp={render_other, Props} }=GialloReq) -> 101 | do_eval(GialloReq#g{ resp={render_other, Props, []} }); 102 | do_eval(#g{ resp={render_other, Props, Args} }=GialloReq) -> 103 | do_eval(GialloReq#g{ resp=render_other(GialloReq, Props, Args) }); 104 | do_eval(#g{ resp={stream, Fun, Acc} }=GialloReq) -> 105 | do_eval(GialloReq#g{ resp={stream, Fun, Acc, []} }); 106 | do_eval(#g{ resp={stream, Fun, Acc, Headers}, req=Req0 }=GialloReq) -> 107 | {ok, Req1} = cowboy_req:chunked_reply(200, Req0), 108 | ok = stream(GialloReq#g{ req=Req1 }, Fun, Acc), 109 | {halt, cowboy_req:set([ {resp_headers, Headers} ], Req1)}; 110 | do_eval(#g{ resp={json, Data} }=GialloReq) -> 111 | do_eval(GialloReq#g{ resp={json, Data, []} }); 112 | do_eval(#g{ resp={json, Data, []} }=GialloReq) -> 113 | do_eval(GialloReq#g{ resp={json, Data, ?JSON_CT} }); 114 | do_eval(#g{ resp={json, Data, Headers} }=GialloReq) -> 115 | do_eval(GialloReq#g{ resp=encode_json(GialloReq, Data, Headers) }); 116 | do_eval(#g{ resp={jsonp, Callback, Data} }=GialloReq) -> 117 | do_eval(GialloReq#g{ resp={jsonp, Callback, Data, []} }); 118 | do_eval(#g{ resp={jsonp, Callback, Data, []} }=GialloReq) -> 119 | do_eval(GialloReq#g{ resp={jsonp, Callback, Data, ?JS_CT} }); 120 | do_eval(#g{ resp={jsonp, Callback, Data, Headers} }=GialloReq) -> 121 | case encode_json(GialloReq, Data, Headers) of 122 | {output, Json, Headers} -> 123 | do_eval(GialloReq#g{ resp={output 124 | , <> 125 | , Headers} }); 126 | Error -> do_eval(GialloReq#g{ resp=Error }) 127 | end; 128 | do_eval(#g{ resp={output, Output} }=GialloReq) -> 129 | do_eval(GialloReq#g{ resp={output, Output, []} }); 130 | do_eval(#g{ resp={output, Output, []} }=GialloReq) -> 131 | do_eval(GialloReq#g{ resp={output, Output, ?DEFAULT_CT} }); 132 | do_eval(#g{ resp={output, Output, Headers}, req=Req0 }) -> 133 | {ok, Req1} = cowboy_req:reply(200, Headers, Output, Req0), 134 | {halt, Req1}; 135 | do_eval(#g{ resp=not_found, req=Req0 }) -> 136 | {ok, Req1} = cowboy_req:reply(404, Req0), 137 | {halt, Req1}; 138 | do_eval(#g{ resp={error, Status}, req=Req }) -> 139 | {error, Status, Req}; 140 | do_eval(#g{ resp=continue, req=Req, env=Env }) -> 141 | {ok, Req, Env}. 142 | 143 | stream(#g{ req=Req }=GialloReq, Fun, Acc0) -> 144 | case Fun(Acc0) of 145 | {output, Data, Acc1} -> 146 | ok = cowboy_req:chunk(Data, Req), 147 | stream(GialloReq, Fun, Acc1); 148 | done -> 149 | ok 150 | end. 151 | 152 | encode_json(GialloReq, Data, Headers) -> 153 | try {output, jsx:encode(Data), Headers} 154 | catch C:R -> 155 | giallo_util:error(GialloReq#g{ action=jsx, handler=encode } 156 | , 1, C, R, erlang:get_stacktrace()) 157 | end. 158 | 159 | action_other(#g{ handler=Handler }=GialloReq, Props) -> 160 | giallo_middleware:execute_handler( 161 | GialloReq#g{ 162 | handler=giallo_util:any2ea(get_value(controller, Props, Handler)) 163 | , action=get_value(action, Props) }). 164 | 165 | render_other(#g{ handler=Handler }=GialloReq, Props, Args) -> 166 | render_template(GialloReq#g{ handler=get_value(controller, Props, Handler) 167 | , action=get_value(action, Props) }, Args, []). 168 | 169 | redirect_or_move(Status, Location, Headers, Req0) -> 170 | {halt, cowboy_req:set([{connection, close}, {resp_state, done}] 171 | , unwrap(cowboy_req:reply(Status, [{<<"location">>, Location}] 172 | , Headers, Req0)))}. 173 | 174 | render_template(#g{ handler=Handler, action=Action }=GReq, Args, Headers) -> 175 | try 176 | case code:ensure_loaded(giallo_util:any2ea(atom_to_list(Handler) 177 | ++"_"++atom_to_list(Action)++"_dtl")) 178 | of 179 | {module, Template} -> 180 | {output, 181 | unwrap(apply(Template, render, [Args])), Headers}; 182 | {error, _Reason} -> continue 183 | end 184 | catch C:R -> 185 | giallo_util:error(GReq, erlang:length(Args), C, R 186 | , erlang:get_stacktrace()) 187 | end. 188 | 189 | get_value(Key, List) -> get_value(Key, List, undefined). 190 | 191 | get_value(Key, List, Default) -> 192 | case lists:keyfind(Key, 1, List) of 193 | false -> Default; 194 | {Key, Val} -> Val 195 | end. 196 | 197 | unwrap({ok, Val}) -> Val; 198 | unwrap(Val) -> Val. 199 | -------------------------------------------------------------------------------- /src/giallo_sup.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(giallo_sup). 28 | -behaviour(supervisor). 29 | 30 | -export([start_link/0]). 31 | -export([init/1]). 32 | 33 | -define(SUPERVISOR, ?MODULE). 34 | 35 | %% API ------------------------------------------------------------------------ 36 | 37 | start_link() -> 38 | supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). 39 | 40 | init([]) -> 41 | Procs = [], 42 | {ok, {{one_for_one, 10, 10}, Procs}}. 43 | -------------------------------------------------------------------------------- /src/giallo_util.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | 28 | %% @doc Giallo utility function. 29 | %% 30 | %% This module is contains various utility functions internal to Giallo 31 | %% @end 32 | 33 | -module(giallo_util). 34 | 35 | -include("giallo.hrl"). 36 | 37 | -export([error/5]). 38 | -export([get_extra/1]). 39 | -export([get_action/1]). 40 | -export([any2ea/1]). 41 | 42 | %% API ------------------------------------------------------------------------ 43 | 44 | %% @doc Print a Giallo error using error_logger 45 | -spec error(GialloReq, Arity, Class, Reason, Stack) -> Error when 46 | GialloReq :: giallo:giallo_req() 47 | ,Arity :: non_neg_integer() 48 | ,Class :: term() 49 | ,Reason :: term() 50 | ,Stack :: term() 51 | ,Error :: {error, 500}. 52 | error(#g{handler=H, action=A, req=R, env=Env}, Arity, Class, Reason, Stack) -> 53 | error_logger:error_msg( 54 | "** Giallo handler ~p terminating in ~p/~p~n" 55 | " for the reason ~p:~p~n" 56 | "** Handler state was ~p~n" 57 | "** Request was ~p~n" 58 | "** Stacktrace: ~p~n~n", 59 | [H, A, Arity, Class, Reason, Env, 60 | cowboy_req:to_list(R), Stack]), 61 | {error, 500}. 62 | 63 | %% @doc Tries to extract the current action from a Cowboy Req-object. 64 | %% Action is the first part of the URI. 65 | %% i.e. URI /action/extra/extra/extra would for a Giallo controller 66 | %% mapped to / return {@type @{cowboy_req:req(), Action::binary()@}}. 67 | -spec get_action(Req0) -> Action when 68 | Req0 :: cowboy_req:req() 69 | ,Action :: {binary(), cowboy_req:req()} | 70 | {non_existent_action, cowboy_req:req()}. 71 | get_action(Req0) -> 72 | case cowboy_req:path_info(Req0) of 73 | {[Action | _], Req1} -> {Action, Req1}; 74 | {_, Req1} -> {non_existent_action, Req1} 75 | end. 76 | 77 | %% @doc Tries to extract the current extra from a Cowboy Req-object. 78 | %% Extra is the remainding parts of the URI when action is extracted. 79 | %% i.e. URI /action/extra/extra/extra would for a Giallo controller 80 | %% return a list of path segments (as binaries) {@type list(binary())}. 81 | -spec get_extra(Req0) -> Extra when 82 | Req0 :: cowboy_req:req() 83 | ,Extra :: {list(binary()), cowboy_req:req()} | {[], cowboy_req:req()}. 84 | get_extra(Req0) -> 85 | {PathInfo, Req1} = cowboy_req:path_info(Req0), 86 | {do_get_extra(PathInfo), Req1}. 87 | 88 | -spec any2ea(Any) -> AnyAtom when 89 | Any :: list() | binary() | atom() 90 | ,AnyAtom :: atom(). 91 | any2ea(A) when is_atom(A) -> A; 92 | any2ea(L) when is_list(L) -> any2ea(list_to_existing_atom(L)); 93 | any2ea(B) when is_binary(B) -> any2ea(binary_to_list(B)). 94 | 95 | %% Private -------------------------------------------------------------------- 96 | 97 | do_get_extra([]) -> 98 | []; 99 | do_get_extra(undefined) -> 100 | []; 101 | do_get_extra([_ | PathInfo]) -> 102 | PathInfo. 103 | -------------------------------------------------------------------------------- /test/default_handler.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(default_handler). 28 | 29 | %-export([hi/3]). 30 | 31 | %% Standard Cowboy callback handlers 32 | -export([init/3]). 33 | -export([handle/2]). 34 | -export([terminate/3]). 35 | 36 | %% Giallo callback handlers 37 | -export([before_/2]). 38 | -export([hi/4]). 39 | -export([moved/4]). 40 | -export([stream/4]). 41 | -export([redirect/4]). 42 | -export([before_template/4]). 43 | -export([query_param/4]). 44 | -export([post_param/4]). 45 | -export([action_other/4]). 46 | -export([render_other/4]). 47 | -export([render_other_landing/4]). 48 | -export([extra_req_return/4]). 49 | -export([not_found/4]). 50 | -export([error_500/4]). 51 | -export([hello_world_template/4]). 52 | -export([hello_world_template_var/4]). 53 | 54 | %% Standard Cowboy callback handlers 55 | init(_Transport, Req, []) -> 56 | {ok, Req, undefined}. 57 | 58 | handle(Req, State) -> 59 | {ok, Req2} = cowboy_req:reply(200, [], <<"Hello World!">>, Req), 60 | {ok, Req2, State}. 61 | 62 | terminate(_Reason, _Req, _State) -> 63 | ok. 64 | 65 | %% Giallo callback handlers 66 | before_(before_template, _Req) -> 67 | {ok, [{before_var, <<"Before!">>}]}; 68 | before_(_, _Req) -> 69 | {ok, []}. 70 | 71 | hi(<<"GET">>, [<<"you">>], _Extra, _Req) -> 72 | {output, <<"Ohai!">>}; 73 | hi(<<"GET">>, [<<"json">>], _Extra, _Req) -> 74 | {json, [{<<"jason">>, <<"Ohai!">>}]}; 75 | hi(<<"GET">>, [<<"jsonp">>], _Extra, _Req) -> 76 | {jsonp, <<"callback">>, [{<<"jason">>, <<"Ohai!">>}]}. 77 | 78 | action_other(<<"GET">>, [], _Extra, _Req) -> 79 | {action_other, [{action, render_other_landing}, 80 | {controller, <<"default_handler">>}]}. 81 | 82 | before_template(<<"GET">>, [], Extra, _Req) -> 83 | <<"Before!">> = proplists:get_value(before_var, Extra), 84 | ok. 85 | 86 | moved(<<"GET">>, _Pathinfo, _Extra, _Req) -> 87 | {moved, <<"http://127.0.0.1:8080/hi/you">>}. 88 | 89 | stream(<<"GET">>, _Pathinfo, _Extra, _Req) -> 90 | F = fun(Acc) -> 91 | case Acc =:= 3 of 92 | true -> done; 93 | false -> {output, <<"Hello">>, Acc+1} 94 | end 95 | end, 96 | {stream, F, 0}. 97 | 98 | redirect(<<"GET">>, _Pathinfo, _Extra, _Req) -> 99 | {redirect, <<"http://127.0.0.1:8080/hi/you">>}. 100 | 101 | render_other(<<"GET">>, _Pathinfo, _Extra, _Req) -> 102 | {render_other, [{action, hello_world_template}, 103 | {controller, default_handler}]}. 104 | 105 | render_other_landing(<<"GET">>, [], _Extra, _Req) -> 106 | {output, <<"You got rendered!">>}. 107 | 108 | query_param(<<"GET">>, [], _Extra, Req0) -> 109 | {<<"b">>, Req1} = giallo:query_param(<<"a">>, Req0), 110 | {<<"d">>, _Req2} = giallo:query_param(<<"c">>, Req1), 111 | {output, <<"Ok!">>}. 112 | 113 | post_param(<<"POST">>, [], _Extra, Req0) -> 114 | {<<"b">>, Req1} = giallo:post_param(<<"a">>, Req0), 115 | {<<"d">>, _Req2} = giallo:post_param(<<"c">>, Req1), 116 | {output, <<"Ok!">>}. 117 | 118 | not_found(<<"GET">>, _Pathinfo, _Extra, _Req) -> 119 | not_found. 120 | 121 | error_500(<<"GET">>, _Pathinfo, _Extra, _Req) -> 122 | root_cause:analysis(). 123 | 124 | hello_world_template(<<"GET">>, _Pathinfo, _Extra, _Req) -> 125 | ok. 126 | 127 | hello_world_template_var(<<"GET">>, _Pathinfo, _Extra, _Req) -> 128 | {ok, [{payload, <<"Hello World!">>}]}. 129 | 130 | extra_req_return(<<"GET">>, [], _Extra, Req0) -> 131 | Req1 = cowboy_req:set_resp_header(<<"extra-pextra">>, <<"C.R.E.A.M">>, Req0), 132 | {output, <<"Hello World!">>, [], Req1}. 133 | -------------------------------------------------------------------------------- /test/default_handler_before_template.html: -------------------------------------------------------------------------------- 1 | {{ _before.before_var }} 2 | -------------------------------------------------------------------------------- /test/default_handler_hello_world_template.html: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/default_handler_hello_world_template_var.html: -------------------------------------------------------------------------------- 1 | {{ payload }} 2 | -------------------------------------------------------------------------------- /test/giallo_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(giallo_SUITE). 28 | 29 | -include_lib("common_test/include/ct.hrl"). 30 | 31 | %% CT 32 | -export([all/0]). 33 | -export([groups/0]). 34 | -export([init_per_suite/1]). 35 | -export([end_per_suite/1]). 36 | -export([init_per_group/2]). 37 | -export([end_per_group/2]). 38 | 39 | %% Tests 40 | -export([minimal/1]). 41 | -export([moved/1]). 42 | -export([redirect/1]). 43 | -export([stream/1]). 44 | -export([query_param/1]). 45 | -export([post_param/1]). 46 | -export([hi_world/1]). 47 | -export([hi_json/1]). 48 | -export([hi_jsonp/1]). 49 | -export([before_template/1]). 50 | -export([subpath_hi_world/1]). 51 | -export([hello_world/1]). 52 | -export([hello_world_template/1]). 53 | -export([hello_world_template_var/1]). 54 | -export([action_other/1]). 55 | -export([extra_req_return/1]). 56 | -export([not_found/1]). 57 | -export([error_500/1]). 58 | -export([render_other/1]). 59 | 60 | %% CT Setup ------------------------------------------------------------------- 61 | 62 | all() -> 63 | [ 64 | {group, minimal}, 65 | {group, subpath}, 66 | {group, default} 67 | ]. 68 | 69 | groups() -> 70 | Tests = [ 71 | hi_json, 72 | hi_jsonp, 73 | hi_world, 74 | stream, 75 | moved, 76 | redirect, 77 | hello_world, 78 | hello_world_template, 79 | hello_world_template_var, 80 | extra_req_return, 81 | before_template, 82 | not_found, 83 | render_other, 84 | post_param, 85 | query_param, 86 | action_other, 87 | error_500 88 | ], 89 | [ 90 | {minimal, [], [minimal]}, 91 | {subpath, [], [subpath_hi_world]}, 92 | {default, [], Tests} 93 | ]. 94 | 95 | init_per_suite(Config) -> 96 | application:start(crypto), 97 | application:start(inets), 98 | Config. 99 | 100 | end_per_suite(_Config) -> 101 | inets:stop(), 102 | ok. 103 | 104 | giallo_init() -> 105 | application:start(ranch), 106 | application:start(cowboy), 107 | application:start(giallo). 108 | 109 | init_per_group(minimal, Config) -> 110 | giallo_init(), 111 | Dispatch = [ 112 | {'_', [ 113 | {"/", minimal_handler, []} 114 | ] 115 | } 116 | ], 117 | giallo:start(Dispatch), 118 | Config; 119 | init_per_group(default, Config) -> 120 | giallo_init(), 121 | Dispatch = [ 122 | {'_', [ 123 | {"/[...]", default_handler, []} 124 | ] 125 | } 126 | ], 127 | giallo:start(Dispatch), 128 | Config; 129 | init_per_group(subpath, Config) -> 130 | giallo_init(), 131 | Dispatch = [ 132 | {'_', [ 133 | {"/subpath/[...]", default_handler, []} 134 | ] 135 | } 136 | ], 137 | giallo:start(Dispatch), 138 | Config. 139 | 140 | end_per_group(_, _Config) -> 141 | application:stop(giallo), 142 | application:stop(cowboy), 143 | application:stop(ranch). 144 | 145 | %% Tests ---------------------------------------------------------------------- 146 | 147 | subpath_hi_world(Config) -> 148 | Url = base_url(Config), 149 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "subpath/hi/you"), 150 | {"HTTP/1.1", 200, "OK"} = Status, 151 | "Ohai!" = Body, 152 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 153 | 154 | hi_world(Config) -> 155 | Url = base_url(Config), 156 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "hi/you"), 157 | {"HTTP/1.1", 200, "OK"} = Status, 158 | "Ohai!" = Body, 159 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 160 | 161 | query_param(Config) -> 162 | Url = base_url(Config), 163 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "query_param?a=b&c=d"), 164 | {"HTTP/1.1", 200, "OK"} = Status, 165 | "Ok!" = Body, 166 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 167 | 168 | post_param(Config) -> 169 | Url = base_url(Config), 170 | {ok, {Status, Headers, Body}} = httpc:request(post, 171 | {Url ++ "post_param", [], 172 | "application/x-www-form-urlencoded", 173 | "a=b&c=d"}, [], []), 174 | {"HTTP/1.1", 200, "OK"} = Status, 175 | "Ok!" = Body, 176 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 177 | 178 | extra_req_return(Config) -> 179 | Url = base_url(Config), 180 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "extra_req_return"), 181 | {"HTTP/1.1", 200, "OK"} = Status, 182 | "Hello World!" = Body, 183 | {"extra-pextra", "C.R.E.A.M"} = 184 | lists:keyfind("extra-pextra", 1, Headers). 185 | 186 | hi_json(Config) -> 187 | Url = base_url(Config), 188 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "hi/json"), 189 | {"HTTP/1.1", 200, "OK"} = Status, 190 | "{\"jason\":\"Ohai!\"}" = Body, 191 | {"content-type", "application/json"} = 192 | lists:keyfind("content-type", 1, Headers). 193 | 194 | hi_jsonp(Config) -> 195 | Url = base_url(Config), 196 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "hi/jsonp"), 197 | {"HTTP/1.1", 200, "OK"} = Status, 198 | "callback({\"jason\":\"Ohai!\"});" = Body, 199 | {"content-type", "application/javascript"} = 200 | lists:keyfind("content-type", 1, Headers). 201 | 202 | stream(Config) -> 203 | Url = base_url(Config), 204 | {ok, {Status, _Headers, Body}} = httpc:request(Url ++ "stream"), 205 | {"HTTP/1.1", 200, "OK"} = Status, 206 | "HelloHelloHello" = Body. 207 | 208 | moved(Config) -> 209 | Url = base_url(Config), 210 | {ok, {Status, _Headers, _Body}} = httpc:request(get, {Url ++ "moved", []}, 211 | [{autoredirect, false}], 212 | []), 213 | {"HTTP/1.1",301,"Moved Permanently"} = Status. 214 | 215 | action_other(Config) -> 216 | Url = base_url(Config), 217 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "action_other"), 218 | {"HTTP/1.1", 200, "OK"} = Status, 219 | "You got rendered!" = Body, 220 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 221 | 222 | render_other(Config) -> 223 | Url = base_url(Config), 224 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ "render_other"), 225 | {"HTTP/1.1", 200, "OK"} = Status, 226 | "Hello World!\n" = Body, 227 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 228 | 229 | redirect(Config) -> 230 | Url = base_url(Config), 231 | {ok, {Status, _Headers, _Body}} = httpc:request(get, {Url ++ "redirect", 232 | []}, 233 | [{autoredirect, false}], 234 | []), 235 | {"HTTP/1.1",302,"Found"} = Status. 236 | 237 | hello_world(Config) -> 238 | Url = base_url(Config), 239 | {ok, {Status, _Headers, Body}} = httpc:request(Url), 240 | {"HTTP/1.1", 200, "OK"} = Status, 241 | "Hello World!" = Body. 242 | 243 | hello_world_template(Config) -> 244 | Url = base_url(Config), 245 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ 246 | "hello_world_template"), 247 | {"HTTP/1.1", 200, "OK"} = Status, 248 | "Hello World!\n" = Body, 249 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 250 | 251 | before_template(Config) -> 252 | Url = base_url(Config), 253 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ 254 | "before_template"), 255 | {"HTTP/1.1", 200, "OK"} = Status, 256 | "Before!\n" = Body, 257 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 258 | 259 | hello_world_template_var(Config) -> 260 | Url = base_url(Config), 261 | {ok, {Status, Headers, Body}} = httpc:request(Url ++ 262 | "hello_world_template_var"), 263 | {"HTTP/1.1", 200, "OK"} = Status, 264 | "Hello World!\n" = Body, 265 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 266 | 267 | not_found(Config) -> 268 | Url = base_url(Config), 269 | {ok, {Status, _Headers, _Body}} = httpc:request(Url ++ "not_found"), 270 | {"HTTP/1.1", 404, "Not Found"} = Status. 271 | 272 | error_500(Config) -> 273 | Url = base_url(Config), 274 | {ok, {Status, _Headers, _Body}} = httpc:request(Url ++ "error_500"), 275 | {"HTTP/1.1", 500, "Internal Server Error"} = Status. 276 | 277 | minimal(Config) -> 278 | Url = base_url(Config), 279 | {ok, {Status, Headers, Body}} = httpc:request(Url), 280 | {"HTTP/1.1", 200, "OK"} = Status, 281 | "Ohai!" = Body, 282 | {"content-type", "text/html"} = lists:keyfind("content-type", 1, Headers). 283 | 284 | %% Private -------------------------------------------------------------------- 285 | 286 | base_url(Config) -> 287 | Port = get_config(port, Config, 8080), 288 | "http://localhost:" ++ integer_to_list(Port) ++ "/". 289 | 290 | get_config(Key, Config, Default) -> 291 | case lists:keyfind(Key, 1, Config) of 292 | {Key, Val} -> Val; 293 | false -> Default 294 | end. 295 | -------------------------------------------------------------------------------- /test/minimal_handler.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% giallo: A small and flexible web framework 4 | %% 5 | %% Copyright (c) 2013 KIVRA 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(minimal_handler). 28 | 29 | -export([index_/4]). 30 | 31 | %% API ------------------------------------------------------------------------ 32 | 33 | index_(<<"GET">>, [], _Extra, _Req) -> 34 | {output, <<"Ohai!">>}. 35 | --------------------------------------------------------------------------------