forked from ChicagoBoss/ChicagoBoss
/
api-controller.html
484 lines (433 loc) · 23.9 KB
/
api-controller.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
{% extends "api.html" %}
{% block api_content %}
<p style="font-size: 14px;"><em>Jump to:</em>
<a href="#routes">Routes</a>
<a href="#auth">Authorization</a>
<a href="#return_values">Return values</a>
<a href="#cache">Caching</a>
<a href="#lang">Content-Language</a>
<a href="#filter">Post-processing</a>
<a href="#simplebridge">SimpleBridge request object</a></p>
<p>Chicago Boss associates each URL with a function of a controller.
The URL <nobr>/foo/bar</nobr> will call the function <code>foo_controller:bar</code>.
Each controller module should go into your project's src/controller/ directory and the file name should start with the application name and end with "_controller.erl", e.g. "appname_my_controller.erl".
Helper functions should go into your project's src/lib/ directory.
Controllers can take one parameter or two parameters: the <a href="#simplebridge">SimpleBridge request object</a>, and an optional session ID (if <a href="api-session.html">sessions</a> are enabled). Declare it like:</p>
<div class="code">
<span class="attr">-module</span>(appname_my_controller, [Req]).
</div>
<p>Or:</p>
<div class="code">
<span class="attr">-module</span>(appname_my_controller, [Req, SessionID]).
</div>
<p>Each exported controller function takes two or three arguments:</p>
<ul>
<li>First argument: the HTTP request method as an atom, e.g. <code>'GET'</code> or <code>'POST'</code></li>
<li>Second argument: the list of slash-separated tokens after the action name in the URL.</li>
<li>Third argument (optional): the result of a function named <code>before_</code> in the controller</li>
</ul>
<p>Example function clauses:</p>
<div class="code">
<span class="comment">% GET /blog/view</span><br />
view(<span class="atom">'GET'</span>, []) -><br />
...<br />
<span class="comment">% GET /blog/view/1234</span><br />
view(<span class="atom">'GET'</span>, [Id]) -><br />
...<br />
<span class="comment">% GET /blog/view/tag/funny</span><br />
view(<span class="atom">'GET'</span>, [<span class="string">"tag"</span>, Tag]) -><br />
...<br />
<span class="comment">% GET /blog/view/tag/funny/author/saint-paul</span><br />
view(<span class="atom">'GET'</span>, [<span class="string">"tag"</span>, Tag, <span class="string">"author"</span>, AuthorName]) -><br />
...<br />
<span class="comment">% GET /blog/view/2009/08</span><br />
view(<span class="atom">'GET'</span>, [Year, Month]) -><br />
...<br />
</div>
<p>These function clauses act as templates for constructing URLs in the view; for each CamelCase variable, simply use the lower-cased underscored equivalent as the parameter name. To continue the example above, you can construct URLs to match the above controllers with the following view tags:</p>
<div class="code">
{{ '{% url action="view" %}' }}<br />
{{ '{% url action="view" id="1234" %}' }}<br />
{{ '{% url action="view" tag="funny" %}' }}<br />
{{ '{% url action="view" tag="funny" author_name="saint-paul" %}' }}<br />
{{ '{% url action="view" year="2009" month="08" %}' }}<br />
</div>
<p>Template variables can of course be used in place of string literals.</p>
<a name="routes"></a>
<h3>Routes</h3>
<p>Most routing takes place in the controller pattern-matching code. You can define additional routes in <code>priv/my_application.routes</code>. The file contains a list of erlang terms, one per line finished with a dot. Each term is a tuple with a URL or an HTTP status code as the first term, and a <code>Location::proplist()</code> as the second term.</p>
<p>The <code>Location</code> proplist must contain keys for <code>controller</code> and <code>action</code>. An optional <code>application</code> key will route requests across applications.</p>
<p>A few examples of routes:</p>
<div class="code">
{"/", [{controller, "main"}, {action, "welcome"}]}.<br />
{"/signup", [{application, login_app}, {controller, "account"}, {action, "create"}]}.<br />
{404, [{controller, "main"}, {action, "not_found"}]}.
</div>
<p>Most routes directly render the specified action; however, routing across applications (as in the second example) results in a 302 redirect. Note that the <code>{{ '{% url %}' }}</code> template tag will use the routes file to generate "pretty" URLs where appropriate.</p>
<p>Additional <code>Location</code> parameters will be matched against the variable names of the controller's token list. For example, if <code>user_controller.erl</code> contains:</p>
<div class="code">
profile('GET', [UserId]) ->
...
</div>
<p>Then the location <code>[{controller, "user"}, {action, "profile"}, {user_id, "123"}]</code> will invoke the "profile" action of <code>user_controller</code> and pass in "123" as the only token (that is, <code>UserId</code>). If a location parameter does not match any variables in the token list, it will be passed in as a query parameter (e.g. <code>?user_id=123</code>).</p>
<p>Routing URLs may contain regular expresssions. For example, the following route will match all URLs that start with a digit:</p>
<div class="code">
{"/[0-9].*", [...]}.
</div>
<p>Sub-expressions can be captured using parentheses and substituted with the atoms <code>'$1'</code>, <code>'$2'</code>, etc. For example, the following route will capture a string of digits and pass them in as the <code>coupon_id</code> parameter:</p>
<div class="code">
{"/([0-9]+)", [{controller, "coupon"}, {action, "redeem"}, {coupon_id, '$1'}]}.
</div>
<p>Alternatively, named groups may be used to identify captured sub-expressions. For example, the following route is equivalent to the one above:</p>
<div class="code">
{"/(?<id>[0-9]+)", [{controller, "coupon"}, {action, "redeem"}, {coupon_id, '$id'}]}.
</div>
<p>The route expressions must match the entire URL. That is, the expressions are implicitly bookended with ^ and $.</p>
<p>To define a default action for a controller, simply add a <code>default_action</code> attribute to the controller like so:</p>
<div class="code">
<span class="attr">-default_action</span>(index).
</div>
<a name="auth"></a>
<h3>Authorization</h3>
<p>If an action takes three arguments, then the function <code>before_/3</code> in your controller will be passed:</p>
<ol>
<li>the action name as a string</li>
<li>the request method as an atom</li>
<li>the list of URL tokens</li>
</ol>
<p><code>before_/3</code> should return one of:</P>
<div class="example">
<div class="code spec">
{ok, ExtraInfo}
</div>
<p><code>ExtraInfo</code> will be passed as the third argument to the action, and as a variable called "_before" to the templates.</p>
</div>
<div class="example row2">
<div class="code spec">
{redirect, Location}
</div>
<p><code>Location = string() | [{Key<span class="typevar">::atom()</span>, Value<span class="typevar">::atom()</span>}]</code></p>
<p>Do not execute the action. Instead, perform a 302 redirect to <code>Location</code>, which can be a string or a proplist that will be converted to a URL using the routes system.</p>
</div>
<p>Probably most common <code>before_/3</code> looks like:</p>
<pre class="code spec">
before_(_, _, _) ->
<span class="function">my_user_lib</span>:<span class="function">require_login</span>(Req).
</pre>
<p>Which might return a tuple of user credential or else redirect to a login page. This way, if you want to require a login to a set of actions, just give those actions a <code>User</code> argument, and the actions will be login protected and have access to the <code>User</code> variable.</p>
<br />
<a name="return_values"></a>
<h3>Return values</h3>
<p>Whether or not it takes a third argument, a controller action should return with one of the following:</p>
<div class="example row1">
<div class="code spec">
ok
</div>
<p>The template will be rendered without any variables.</p>
</div>
<div class="example row2">
<div class="code spec">
{ok, Variables<span class="typevar">::proplist()</span>}
</div>
<p><code>Variables</code> will be passed into the associated <a href="api-view.html#nav">Django template</a>.</p>
</div>
<div class="example row1">
<div class="code spec">
{ok, Variables<span class="typevar">::proplist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p><code>Variables</code> will be passed into the associated Django template, and <code>Headers</code> are HTTP headers you want to set (e.g., <code>Content-Type</code>).</p>
</div>
<div class="example row1">
<div class="code spec">
js
</div>
<p>The template will be rendered without any variables and served as <code>Content-Type: application/javascript</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{js, Variables<span class="typevar">::proplist()</span>}
</div>
<p><code>Variables</code> will be passed into the associated <a href="api-view.html#nav">Django template</a> and the result will be served as <code>Content-Type: application/javascript</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{js, Variables<span class="typevar">::proplist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p><code>Variables</code> will be passed into the associated Django template, the result served as <code>Content-Type: application/javascript</code>, and <code>Headers</code> are HTTP headers you want to set.</p>
</div>
<div class="example row2">
<div class="code spec">
{redirect, Location}
</div>
<p><code>Location = string() | [{action, Value<span class="typevar">::string()</span>}, ...]</code></p>
<p>Perform a 302 HTTP redirect to <code>Location</code>, which may be a URL string or a proplist of parameters that will be converted to a URL using the routes system.</p>
</div>
<div class="example row1">
<div class="code spec">
{redirect, Location, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Perform a 302 HTTP redirect to <code>Location</code> and set additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{moved, Location}
</div>
<p><code>Location = string() | [{action, Value<span class="typevar">::string()</span>}, ...]</code></p>
<p>Perform a 301 HTTP redirect to <code>Location</code>, which may be a URL string or a proplist of parameters that will be converted to a URL using the routes system.</p>
</div>
<div class="example row1">
<div class="code spec">
{moved, Location, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Perform a 301 HTTP redirect to <code>Location</code> and set additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{action_other, OtherLocation}
</div>
<p><code>OtherLocation = [{action, Value<span class="typevar">::string()</span>}, ...]</code></p>
<p>Execute the controller action specified by <code>OtherLocation</code>, but without performing an HTTP redirect.</p>
</div>
<div class="example row1">
<div class="code spec">
{render_other, OtherLocation}
</div>
<p><code>OtherLocation = [{action, Value<span class="typevar">::string()</span>}, ...]</code></p>
<p>Render the view from <code>OtherLocation</code>, but don't actually execute the associated controller action.</p>
</div>
<div class="example row2">
<div class="code spec">
{render_other, OtherLocation, Variables}
</div>
<p>Render the view from <code>OtherLocation</code> using <code>Variables</code>, but don't actually execute the associated controller action.</p>
</div>
<div class="example row1">
<div class="code spec">
{output, Output<span class="typevar">::iolist()</span>}
</div>
<p>Skip views altogether and return <code>Output</code> to the client.</p>
</div>
<div class="example row2">
<div class="code spec">
{output, Output<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Skip views altogether and return <code>Output</code> to the client while setting additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{stream, Generator<span class="typevar">::function()</span>, Acc0}
</div>
<p>Stream a response to the client using HTTP chunked encoding. For each chunk, the <code>Generator</code> function is passed an accumulator (initally <code>Acc0</code>) and should return either <code>{output, Data, Acc1}</code> or <code>done</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{stream, Generator<span class="typevar">::function()</span>, Acc0, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Same as above, but set additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{json, Data<span class="typevar">::proplist()</span>}
</div>
<p>Return <code>Data</code> as a JSON object to the client. Performs appropriate serialization if the values in Data contain a BossRecord or a list of BossRecords.</p>
</div>
<div class="example row2">
<div class="code spec">
{json, Data<span class="typevar">::proplist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Return <code>Data</code> to the client as a JSON object while setting additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{jsonp, Callback<span class="typevar">::string()</span>, Data<span class="typevar">::proplist()</span>}
</div>
<p>Returns <code>Data</code> as a JSONP method call to the client. Performs appropriate serialization if the values in Data contain a BossRecord or a list of BossRecords.</p>
</div>
<div class="example row2">
<div class="code spec">
{jsonp, Callback<span class="typevar">::string()</span>, Data<span class="typevar">::proplist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Return <code>Data</code> to the client as a JSONP method call (as above) while setting additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
not_found
</div>
<p>Invoke the 404 File Not Found handler.</p>
</div>
<div class="example row2">
<div class="code spec">
{StatusCode<span class="typevar">::integer()</span>, Body<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Return an arbitary HTTP integer <code>StatusCode</code> along with a <code>Body</code> and additional HTTP <code>Headers</code>.</p>
</div>
<br />
<a name="cache"></a>
<h3>Caching</h3>
<p>Caching should be a part of any scalability strategy. In addition to caching the results of database queries (see <a href="api-config.html">config</a>), Chicago Boss can cache the list of variables returned from controller actions. CB can also cache entire rendered web pages, but in doing so you will lose the benefit of customizing the page contents with the <code>_before</code> variable, which is not cached.</p>
<p>To enable caching, first make sure the <code>cache_enable</code> is set to <code>true</code> in your configuration and that you've configured cache servers. At present only Memcached cache servers are supported, but additional adapters will be added in the future.</p>
<p>Next, define a <code>cache_</code> function with the following arguments:</p>
<ol>
<li><code>Action</code> - the action name (a string)</li>
<li><code>Tokens</code> - a list of tokens</li>
<li><code>AuthInfo</code> (optional) - authorization information returned from the <code>before_</code> filter (see <a href="#auth">Authorization</a>)</li>
</ol>
<p>The <code>cache_</code> function should return one of:</p>
<ul>
<li><code>{vars, CacheOptions}</code> - cache the variable list returned by the controller action</li>
<li><code>{page, CacheOptions}</code> - cache the rendered page contents</li>
<li><code>none</code> - don't cache</li>
</ul>
<p>Finally, <code>CacheOptions</code> is a proplist possibly containing:</p>
<ul>
<li><code>seconds</code> - length of time to cache the result, in seconds</li>
<li><code>watch</code> - a <a href="api-news.html">topic string</a> defining a collection of records to watch for changes. When a change is observed, the cached data will be invalidated</li>
</ul>
<p>Note that separate cache entries are created for each language as returned by <code>lang_</code> (see <a href="#lang">Content-Language</a>).</p>
<p>Example <code>cache_</code> function:</p>
<div class="code">
cache_(<span class="string">"index"</span>, []) -><br />
{page, [{seconds, 30}]};<br />
cache_(<span class="string">"profile"</span>, [ProfileId]) -><br />
{vars, [{seconds, 300}, {watch, ProfileId ++ <span class="string">".*"</span>}]};<br />
cache_(_, _) -><br />
none.
</div>
<br />
<a name="lang"></a>
<h3>Content-Language</h3>
<p>CB application views can be multi-lingual. By default, the language served to the client is chosen by comparing the incoming Accept-Language header to the available translations in a given view (see <a href="https://github.com/evanmiller/ChicagoBoss/wiki/How-Chicago-Boss-Chooses-Which-Language-To-Serve">"How Chicago Boss Chooses Which Language To Serve"</a>. This can be overridden in two ways:</p>
<ol>
<li>Returning [{"Content-Language", Lang}] from each action in your controller
<li>Defining a <code>lang_</code> function in your controller which returns the chosen language
</ol>
<p>The <code>lang_</code> function will be passed the name of the current action, and optionally the result of the <code>before_</code> filter. This function should return one of:</p>
<ul>
<li><code>auto</code> - automatically choose a language based on the Accept-Language header
<li>A string indicating the language choice ("en", "fr", etc.)
</ul>
<br />
<a name="filter"></a>
<h3>Post-processing</h3>
<p>If it exists, a function called <code>after_</code> in your controller will be passed the result that is about to be returned to the client. The 'after_' function takes two or three arguments:</p>
<ol>
<li>The action name, as a string</li>
<li>The HTTP result tuple</li>
<li>The result of the before_ function, provided one exists</li>
</ol>
<p>The <code>after_</code> function should return a (possibly) modified HTTP result tuple. Result tuples may be one of:</p>
<div class="example row1">
<div class="code spec">
{redirect, Location<span class="typevar">::string()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Performs a 302 HTTP redirect to <code>Location</code> and sets additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{ok, Payload<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Returns a 200 OK response to the client with <code>Payload</code> as the HTTP body, and sets additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{unauthorized, Payload<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Returns a 401 Unauthorized response to the client with <code>Payload</code> as the HTTP body, and sets additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row2">
<div class="code spec">
{not_found, Payload<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Returns a 404 Not Found response to the client with <code>Payload</code> as the HTTP body, and sets additional HTTP <code>Headers</code>.</p>
</div>
<div class="example row1">
<div class="code spec">
{error, Payload<span class="typevar">::iolist()</span>, Headers<span class="typevar">::proplist()</span>}
</div>
<p>Returns a 500 Internal Error response to the client with <code>Payload</code> as the HTTP body, and sets additional HTTP <code>Headers</code>.</p>
</div>
<br />
<a name="simplebridge"></a>
<h3>SimpleBridge</h3>
<p>Controller functions are passed a SimpleBridge request object (slightly modified for Boss's purposes). Useful functions in the request object include:</p>
<div class="example row1">
<div class="code spec">
request_method() -> atom()
</div>
<p>Get the request method, e.g. GET, POST, etc.</p>
</div>
<div class="example row2">
<div class="code spec">
protocol() -> http | https
</div>
<p>Get the request protocol (HTTP or HTTPS).</p>
</div>
<div class="example row2">
<div class="code spec">
peer_ip() -> tuple()
</div>
<p>Get the IP Address of the connected client as a 4-tuple for IPv4 or an 8-tuple for IPv6 (e.g. <code>{127, 0, 0, 1}</code>)</p>
</div>
<div class="example row1">
<div class="code spec">
query_param( Key<span class="typevar">::string()</span> ) -> string() | undefined<br />
query_param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -> string() | Default Value
</div>
<p>Get the value of a given query string parameter (e.g. "?id=1234")</p>
</div>
<div class="example row2">
<div class="code spec">
post_param( Key<span class="typevar">::string()</span> ) -> string() | undefined<br />
post_param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -> string() | Default Value
</div>
<p>Get the value of a given POST parameter.</p>
</div>
<div class="example row1">
<div class="code spec">
param( Key<span class="typevar">::string()</span> ) -> string() | undefined<br />
param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -> string() | Default Value
</div>
<p>Get the value of a given POST/GET parameter.</p>
</div>
<div class="example row2">
<div class="code spec">
deep_post_param( [ Path<span class="typevar">::string()</span> ] ) -> DeepParam | undefined
</div>
<p>Get the value of a given "deep" POST parameter. This function parses parameters that have numerical or labeled indices, such as "widget[4][name]", and returns either a value or a set of nested lists (for numerical indices) and proplists (for string indices).</p>
</div>
<div class="example row1">
<div class="code spec">
header( Header<span class="typevar">::string()</span> | atom() ) -> string() | undefined
</div>
<p>Get the value of a given HTTP request header. Valid <code>Header</code> values are strings or one of these atoms:</p>
<ul>
<li><code>accept</code></li>
<li><code>accept_language</code></li>
<li><code>accept_ranges</code></li>
<li><code>authorization</code></li>
<li><code>connection</code></li>
<li><code>content_encoding</code></li>
<li><code>content_length</code></li>
<li><code>content_type</code></li>
<li><code>cookie</code></li>
<li><code>host</code></li>
<li><code>if_match</code></li>
<li><code>if_modified_since</code></li>
<li><code>if_none_match</code></li>
<li><code>if_unmodified_since</code></li>
<li><code>keep_alive</code></li>
<li><code>location</code></li>
<li><code>range</code></li>
<li><code>referer</code></li>
<li><code>transfer_encoding</code></li>
<li><code>user_agent</code></li>
<li><code>x_forwarded_for</code></li>
</ul>
</div>
<div class="example row2">
<div class="code spec">
cookie( Key<span class="typevar">::string()</span> ) -> string() | undefined
</div>
<p>Get the value of a given cookie.</p>
</div>
{% endblock %}